Visitar URL original
Mixed open and module (alias) statements introduce newlines · Issue #3193 · fsprojects/fantomas · GitHub
Skip to content

Mixed open and module (alias) statements introduce newlines #3193

Open

Description

@deyanp

Issue created from fantomas-online

Code

open System
open System.Threading
module IO = System.IO
open System.Regular
open System.Regular.Text

Result

open System
open System.Threading

module IO = System.IO

open System.Regular
open System.Regular.Text

Problem description

There should be no newline

Extra information

  • The formatted result breaks my code.
  • The formatted result gives compiler warnings.
  • I or my company would be willing to help fix this.
  • I would like a release if this problem is solved.

Options

Fantomas main branch at 2025-09-08T06:57:30Z - 31029b1

Default Fantomas configuration

Did you know that you can ignore files when formatting by using a .fantomasignore file?
PS: It's unlikely that someone else will solve your specific issue, as it's something that you have a personal stake in.

Activity

  1. nojaf commented on Oct 29, 2025

    @nojaf
    Contributor

    This is only a problem from a certain point of view. What happens here makes sense to me according to Fantomas' internal rules. Open statements are grouped into an open list; module aliases are not part of that grouping. Thus we end up with three Oak nodes, two of which are multiline, which explains the newlines between the module aliases.

  2. nojaf commented on Feb 26, 2026

    @nojaf
    Contributor

    /repo-assist elaborate on a technical level why the result is what it is.
    Ensure you keep it high level so the reporter can grasp it.
    I'm not sure this is worth extending the Oak model for OpenList to also capture this.

  3. github-actions commented on Feb 26, 2026

    @github-actions
    Contributor

    🤖 This is an automated response from Repo Assist.

    Here is a high-level technical explanation of why this happens.


    What's going on under the hood

    Fantomas formats code in two main phases:

    Phase 1 — AST → Oak (intermediate representation)

    After parsing, ASTTransformer.fs walks the flat list of module-level declarations and uses an active pattern called OpenL to greedily collect consecutive open statements into a single OpenListNode. The key word is consecutive: the pattern stops collecting the moment it encounters anything that is not an open — including a module alias (SynModuleDecl.ModuleAbbrev).

    So this input:

    open System             // → collected into first OpenList
    open System.Threading   // → collected into first OpenList
    module IO = System.IO   // → ModuleAbbrev node (breaks the run)
    open System.Regular     // → starts a new OpenList
    open System.Regular.Text

    produces three separate Oak nodes:

    1. OpenList [ open System; open System.Threading ]
    2. ModuleAbbrev (module IO = System.IO)
    3. OpenList [ open System.Regular; open System.Regular.Text ]

    Phase 2 — Printing with blank-line logic

    When printing module-level declarations, Fantomas uses colWithNlnWhenItemIsMultiline. This function adds a blank line between two adjacent declarations when either of them spans more than one source line (is "multiline").

    Because node #1 is multiline, a blank line is inserted before module IO = .... Because node #3 is multiline, a blank line is inserted before it too. Both blank lines are a direct consequence of this general rule.


    Is it worth extending Oak's OpenListNode to also capture module aliases?

    As @nojaf notes, it's not obvious that this is worth the complexity. The OpenListNode exists to group import declarations so they print without blank lines between them. A module alias is semantically different (it's a type-level name binding, not an import). Merging them into a single node would blur that distinction in the Oak model and complicate the printer and transformer for what amounts to a rare mixed-style pattern.

    The blank lines Fantomas adds are not wrong — they follow the established rule that multiline blocks deserve visual breathing room. The formatting would look different from the source, but that's expected when Fantomas re-groups declarations. A fix would require either widening what OpenL captures, or teaching the printer to suppress blank lines in a very specific case — both of which carry risk of subtle regressions elsewhere.

    Generated by Repo Assist for issue #3193

    To install this workflow, run gh aw add githubnext/agentics/workflows/repo-assist.md@828ac109efb43990f59475cbfce90ede5546586c. View source at https://github.com/githubnext/agentics/tree/828ac109efb43990f59475cbfce90ede5546586c/workflows/repo-assist.md.

  4. deyanp commented on Mar 2, 2026

    @deyanp
    Author

    @nojaf just a note from a team that has been using F# for building more than 150+ microservices in the past 6 years - open/module statements are the same, diff is only that you "rename" or alias a reference, so that it is easier to use it. However, both should participate in the same global ordering, because it is usually important which one is listed first, especially when the references contain the types with the same names, etc. So I can assure you, that for real life usage, this request is very justified ...

    Do you mean you are rejecting this because it is too complicated to implement?

  5. nojaf commented on Mar 2, 2026

    @nojaf
    Contributor

    I closed this because it is kinda niche.
    Real life usage of one company, only highlights a habit I'd say.
    I can reopen but would like to see some more community interest.

  6. reopened this on Mar 2, 2026
  7. deyanp commented on Mar 13, 2026

    @deyanp
    Author

    @nojaf , the focus of my statement above was actually on

    open/module statements are the same, diff is only that you "rename" or alias a reference, so that it is easier to use it. However, both should participate in the same global ordering, because it is usually important which one is listed first, especially when the references contain the types with the same names, etc.

    let me know if you disagree with that one

    It should not matter if you open or rename a module, both should participate in the same global ondering, because as you know in F# the last open/module statement shadows the previous one, in case a type is in both ...

  8. nojaf commented on Mar 13, 2026

    @nojaf
    Contributor

    Thanks for the clarification.

    From a maintainer perspective this is still a difficult change to justify. While I understand the reasoning behind grouping open statements and module aliases together, it is not clear whether this reflects a broader community practice or a convention used within a specific codebase.

    Fantomas tends to be conservative when introducing stylistic exceptions because changes in formatting behaviour can easily trigger the opposite request from users who rely on the current output. In other words, changing this may resolve the issue for one group while causing others to report that Fantomas no longer respects the behaviour they expect.

    It is also worth noting that Fantomas follows the style described in the documentation:

    https://fsprojects.github.io/fantomas/docs/end-users/StyleGuide.html

    In that model, open statements are treated as a group of imports, while other declarations such as module aliases are separate constructs. The current formatting behaviour reflects that distinction.

    For now I would prefer to wait for broader community interest before introducing a special rule for this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions