Skip to content

Warn on .editorconfig settings Fantomas cannot use - #3401

Merged
nojaf merged 11 commits into
fsprojects:mainfrom
nojaf:validate-editorconfig-keys
Aug 22, 2026
Merged

nojaf merged 11 commits into
fsprojects:mainfrom
nojaf:validate-editorconfig-keys

Conversation

@nojaf

@nojaf nojaf commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor
image

A setting carrying the fsharp_ prefix that is not a Fantomas setting was silently ignored, so a misspelling like fsharp_multiline_brackets_style simply never applied. #3321 covered the other half of this, a known setting carrying a value that cannot be parsed, but it added an arm to a loop over the FormatConfig record, which an unknown setting never enters.

The warning now lists every setting the running version supports, which is authoritative rather than a guess and puts the correct spelling in front of the reader. This also catches prefixing one of the four settings editorconfig itself defines: fsharp_max_line_length never worked, max_line_length is the one that does. Settings without the prefix belong to other tools and are left alone.

Every problem in one .editorconfig is reported together, once per run rather than once per formatted file, which was already wrong for the value warning from #3321. It is emitted as a single message because files are formatted in parallel, and two separately written blocks could be interleaved by another thread and come out in the wrong order.

The daemon sends the same information to its client as a fantomas/configurationWarning notification after every format request, with an empty list when nothing is wrong so an editor can clear what it showed earlier. This is additive to the JSON-RPC contract: a client that does not handle the method ignores it. Fantomas.Client surfaces it as the new FantomasService.ConfigurationWarnings event, which is source-breaking only for code implementing that interface. The payload deliberately avoids F# options, which travel as {"Case":"Some","Fields":[...]} and vanish when None, so that a client in another language can read it.

Also adds a documentation page on using Fantomas.Client, a warning level to the logger, and drops let private from files that have a signature file, where the signature is already the visibility boundary.

Comment thread src/Fantomas.Tests/EditorConfigurationTests.fs Fixed
Comment thread src/Fantomas.Tests/Integration/ConfigTests.fs Fixed
Comment thread src/Fantomas.Tests/Integration/ConfigTests.fs Fixed
Comment thread src/Fantomas.Client/FantomasToolLocator.fs Fixed
Comment thread src/Fantomas.Client/FantomasToolLocator.fs Fixed
Comment thread src/Fantomas.Client/FantomasToolLocator.fs Fixed
Comment thread src/Fantomas.Client.Tests/EndToEndTests.fs Fixed
A setting carrying the `fsharp_` prefix that is not a Fantomas setting was
silently ignored, so a misspelling like `fsharp_multiline_brackets_style`
simply never applied. The warning now lists every setting the running version
supports, which also catches prefixing one of the four settings editorconfig
itself defines: `fsharp_max_line_length` never worked, `max_line_length` is the
one that does. A value the spec defines but Fantomas cannot act on, `unset` and
`indent_size = tab` among them, is not a mistake and stays silent. Keys are
matched without regard to case, and every problem in one file is reported
together, once per run.

The daemon sends the same information to its client as a
`fantomas/configurationWarning` notification after every format request, with
an empty list when nothing is wrong so an editor can clear what it showed
earlier. Additive to the JSON-RPC contract: a client that does not handle the
method ignores it. `Fantomas.Client` surfaces it as the new
`FantomasService.ConfigurationWarnings` event.

The daemon never writes these to standard error. `Fantomas.Client` redirects
that stream, and until now only read it when the process failed to start, so
anything written there filled the pipe the two processes share and blocked the
daemon inside a request. The client drains it now, and the reports travel over
the wire instead.

Also fixes two daemon leaks the notification work uncovered: a second folder
pinning the same Fantomas started a second daemon and dropped the first from
the cache without disposing it, and a daemon whose handshake failed was left
running with nothing holding a reference to it.
@nojaf
nojaf force-pushed the validate-editorconfig-keys branch from 5d78edc to bc5226f Compare August 22, 2026 15:46
nojaf and others added 10 commits August 22, 2026 17:47
Both landed in "Warn on .editorconfig settings Fantomas cannot use", which is
about `.editorconfig` and says nothing about either file. Reverted here and
re-applied in the next commit, so that `git log` on either file points at a
commit whose message is about the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`getSyntaxErrorMessage` walked its lists twice where once would do:
`List.map |> List.map` is `List.map (f >> g)`, and `List.map |> List.filter`
is `List.choose`. That second one is IONIDE-010, which the analyzer run
enforces everywhere except here: `Fantomas.FCS` is in the excluded set in
`build.fsx`, so nothing would have said so.

The two active patterns in Context.fs carry the types they always had. The
analyzer does not ask for them; they are here because a partial active pattern
reads better with its result type written down.

Neither changes behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A value used to be tried against every parser in turn, so a value that meant
something to one setting decided the outcome for all of them.
`EndOfLineStyle.OfConfigString` raises on `cr` rather than answering, which made
`fsharp_max_record_width = cr` fail the whole run with a message about carriage
returns. Parsing is now decided by the type of the field the setting writes to,
so `cr` is a value only `end_of_line` is offered, and anything else carrying it
is reported as a value that setting does not accept. Values are matched without
regard to case, as keys already were.

Reporting moves out of `EditorConfig` into `EditorConfigReport`, which leaves the
configuration module with nothing to say and no Serilog dependency: the two docs
scripts that `#load` it no longer have to reference Serilog to do so. The record
of what has already been reported belongs to a reporter rather than to the
process, so what it does is a question a test can ask.

The report itself is shorter and more use. One unknown setting used to print
every setting Fantomas has, 43 lines for one typo. It now names the setting, and
where the intent is obvious it names the spelling that works:
`fsharp_multiline_brackets_style` suggests `fsharp_multiline_bracket_style`, and
`fsharp_max_line_length` suggests `max_line_length`. The full list moved to
`--verbosity d`, and is not offered at all for a value that could not be read,
where the name was right and the list answers nothing.

Reading an `.editorconfig` runs once per formatted file, so the reflection it
does is worked out once and the name lookup is a set rather than a list scan.

Fantomas.Client, three bugs:

  - Dropping a version from the daemon cache left every folder that resolved to
    it pinned to a version with no daemon behind it. That is the one state the
    cache cannot get out of: it answers
    `CompatibleVersionIsKnownButNoDaemonIsRunning` and changes nothing, so the
    next request answers the same way, for the rest of the session. A version is
    now forgotten along with the folders that pointed at it.
  - `HasExited` says the process is gone, not that its standard error has been
    read, so the lines explaining a failed handshake were usually missing from
    the message that reported it. `WaitForExit` with no timeout waits for the
    readers too.
  - `fantomas --version` prints `Fantomas v8.0.0+<commit>` where
    `dotnet tool list` prints `8.0.0`. Only the leading `v` was being taken off,
    so the same Fantomas resolved from a manifest and from the PATH still
    counted as two versions and still got two daemons.

The daemon sends its configuration warning for every format request rather than
after every one: the write starts before formatting and is awaited after it, so
it is off the way to a formatted document but still on the wire before the
response it belongs to. Reading the configuration can raise rather than come
back with a problem, and the client now hears about that too instead of being
left showing what the previous request gave it.

Tests: what a report says is 14 tests over a logger rather than four processes;
the daemon tests go through the `ReadConfiguration` and `FileSystem` seams the
daemon has rather than the real disk; and the daemon cache is seven deterministic
tests over an abstract daemon instead of one test that read the operating
system's process table, took five seconds, could be raced by a parallel test
project and was skipped outright on Windows.

`ConfigurationFile` wrote to `Path.GetTempPath()/.editorconfig`, one path shared
by every test that used it. `ConfiguredCodeSample` gives each test a folder of
its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`DaemonOperations` was two unrelated things in one record: how the cache reaches
the world, and what a daemon can tell you about itself. The second group is not
an effect and had no business being injected; passing `StartInfo`, `IsRunning`
and `Dispose` as functions is what left `'daemon` looking like it stood for
nothing.

Those three are now `IDaemon`, and `'daemon` is constrained by it, so the
expectations are written down in one place rather than implied by a record of
lambdas. `DaemonOperations` keeps only the two things the cache genuinely cannot
answer for itself.

`RunningFantomasTool` does not implement `IDaemon`; `CachedDaemon` wraps it.
`RunningFantomasTool` is part of the package and has no business growing a public
interface that exists to describe a cache, and the wrapper keeps `IDaemon`
internal to this module.

The type parameter stays because the cache has to hand a daemon back and the
mailbox needs the `JsonRpc` off it. Putting `RpcClient` on `IDaemon` would erase
the parameter, but the cache never touches it, and a test would then have to
conjure a `JsonRpc` it does not use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FormatDocumentAsync and FormatSelectionAsync each carried their own copy of
the same forty lines: resolve the configuration, start the notification, await
it, and fall back to an empty warning when the read raised. Two copies meant
two places to get the ordering right, and one of them did not. When formatting
threw, the notification that had already been started was abandoned rather than
awaited, so the contract stated in Contracts.fsi held only because
StreamJsonRpc happens to serialise its writes. Both methods now go through one
helper that owns the whole sequence, error path included.

A folder pinned to a version with no daemon behind it used to be a dead end.
The cache answered CompatibleVersionIsKnownButNoDaemonIsRunning and changed
nothing, so the next request answered the same, for the rest of the session.
It now forgets the version and resolves the tool again; nothing produces that
state any more, but a cache that has lost track of a daemon should cost a
lookup rather than the ability to format. The error case goes with it.

The version handshake with a freshly started daemon had no timeout. Daemons are
resolved on a single mailbox, so a process that started and then never answered
held up every later request with nothing to show for it. It now gets thirty
seconds, after which the existing cleanup kills it like any other failed
handshake.

Smaller things: BeginErrorReadLine drains the pipe with no subscriber attached,
so the handler that discarded the lines is gone; a report with no contributing
.editorconfig no longer names the F# file as the origin, which sent people
looking in the wrong place; and the client documentation now says the warning
is raised for every format request and before it answers, matching the
signature files.
A configuration warning carries a file path and nothing that says which request
it belongs to. Two format requests in flight for the same file could therefore
deliver their warnings in either order, with no way for a client to sort them
out, so an empty one could clear problems that were still current. Their
results raced each other too, and which one the caller ended up with was down
to timing. Requests for the same file now wait on each other. Requests for
different files do not, which is what a caller formatting a repository cares
about.

That ordering had in fact been holding, but for a reason nobody chose.
StreamJsonRpc only runs handlers concurrently when they are genuinely
asynchronous; one that blocks before its first await holds the message loop and
requests serialise instead. Reading an .editorconfig is disk work and it
happened ahead of any await, so the loop was held for the length of every
configuration read, the next request was not read until it finished, and the
notification was issued inside that serialised prefix. The loop is handed back
first now, which fixes the responsiveness of it and leaves the ordering resting
on the gate rather than on an accident that would have evaporated the moment
the configuration read became asynchronous.

Contracts.fsi and the client documentation state the guarantee: warnings for
one file arrive in the order the requests for it were made, however many are in
flight, and warnings for different files interleave freely with FilePath to
tell them apart.

Both tests were checked against a build with the gate removed. The same-file
one fails without it; the different-file one is there so the fix cannot
degenerate into discarding concurrency.
The gates added in the previous commit used the default ordinal comparer, so
one file reached through two spellings got two gates and the guarantee they
exist to provide silently did not hold between them. Windows and a default
macOS volume both make that reachable, and nothing canonicalises the path on
the way in: the client checks that it is absolute and no more. On a case
sensitive file system two genuinely different files can now share a gate, which
costs a little concurrency and nothing else.

A handshake that times out now says what it was waiting for and how long it was
given, rather than reporting whatever bare timeout message came out of the
exception. The next report of one should carry a number someone can argue
about.

Also documents two things that were decided rather than overlooked. The daemon
does not coalesce: several requests queued for one file each run to completion
in turn, so a burst grows latency with the length of the queue, though none of
them is served stale input because each carries its own source. And disposal
does not wait for requests in flight, because awaiting the connection would
hang shutdown on a stuck daemon, and the exception a mid-flight request may get
is read by nobody once the client is going away.
`max_line_lenght = 100` was ignored without a word, where the same slip in a
`fsharp_` setting was reported. It is the same mistake and it costs the author
the same silence, so an unprefixed key close enough to a setting Fantomas has is
now read as a misspelling of it.

Close enough means two edits, not the three a suggestion is offered at, and the
difference matters. `indent_style` is three edits from `indent_size` and sits in
very nearly every `.editorconfig` ever written, so the wider distance would have
put a false report in front of almost every user. Two reaches every realistic
typo and reaches nothing anyone meant to write. Settings belonging to other
tools stay silent, and there is a test naming the ones that turn up most often.

A negative number is no longer accepted for a setting that takes one. No setting
means anything below zero, and a run that took one formatted to nonsense widths
without saying so.

Three things in the client, all of which let the cache keep believing in a
daemon that could not serve:

  - Replacing a crashed daemon started it the way whichever folder noticed the
    crash resolves now, rather than the way the daemon being replaced was
    started, so a daemon shared by two folders could come back under a different
    working directory. The comment above that branch had been claiming the
    opposite for as long as the code existed.
  - Liveness was the process being up. A daemon whose connection had ended kept
    being handed out while its process lingered, and every request against it
    failed. It now means the process is up and the connection has not ended.
  - The version a manifest reports and the version `--version` prints are folded
    to one casing before being compared. Folding only the printed side, which is
    what the pre-branch code did, manufactures the divergence this is meant to
    remove; both producers have to agree.

Disposal closes the connection before killing the process, so a call still
pending faults on a closed connection rather than on a stream that vanished
under it.

AGENTS.md gains two style rules it did not state: shortest `match` arm first,
which is also the order `fsharp_experimental_keep_indent_in_branch` wants, and
never `<|`. CLAUDE.md is a symlink to AGENTS.md so there is one file to maintain
and it loads for every agent rather than only the ones that know to look. The
changelog entries added on this branch move to the end of their section, which
AGENTS.md has been asking for all along.
@nojaf
nojaf merged commit 7b98c0a into fsprojects:main Aug 22, 2026
6 checks passed
@nojaf
nojaf deleted the validate-editorconfig-keys branch August 28, 2026 06:10
github-actions Bot pushed a commit to Nimblesite/SharpLsp that referenced this pull request Sep 15, 2026
Updated [Fantomas.Core](https://github.com/fsprojects/fantomas) from
7.0.6 to 8.0.0.

<details>
<summary>Release notes</summary>

_Sourced from [Fantomas.Core's
releases](https://github.com/fsprojects/fantomas/releases)._

## 8.0.0

# 8.0.0 <sub>Satanized</sub> - 15/09/2026

### Changed
- Stable release.

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0](https://www.nuget.org/packages/fantomas/8.0.0)
    

## 8.0.0-beta-003

# 8.0.0-beta-003

### Changed
- Updated the packages the command line tool is built against:
`editorconfig` to `0.18.0`, `Serilog` to `4.4.0`, `Spectre.Console` to
`0.57.2` and `System.IO.Abstractions` to `22.2.0`. Only one of these is
visible in what Fantomas does: `editorconfig` `0.18.0` scopes its cache
of the `.editorconfig` files it has read to the parser that reads them,
where it used to share one statically. Fantomas keeps a single parser
for the process, so a file is still read once and no more.
`Fantomas.Core` asks for the same `FSharp.Core` it always did, so
nothing referencing it has to move.
[#​3468](https://github.com/fsprojects/fantomas/pull/3468)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-beta-003](https://www.nuget.org/packages/fantomas/8.0.0-beta-003)
    

## 8.0.0-beta-002

# 8.0.0-beta-002

### Fixed
- Unexpected symbol '<' in expression - Fantomas 8 regression. When the
left-hand side of `=`, `>`, `<`, `%` or `%%` spans several lines, the
operator takes a line of its own one level in, as `8.0.0-beta-001`
introduced. That level was measured from the enclosing indent rather
than from the column the left-hand side starts at. The two are the same
at the start of a binding, and differ after something like `&& (`, where
the operator then landed on the column of the left-hand side and was
offside: the parser read `<` as the start of a type application and
rejected the output. The layout now asks where its fresh line would land
and adds whole indent levels until that clears the left-hand side, the
way a chain's dots have cleared its head since `8.0.0-alpha-024`, so the
operator and the right-hand side sit one level to the right of it and
every column stays a multiple of the indent size. That shared step is
`indentPast` in `Context.fs`, the helper
[#​3446](https://github.com/fsprojects/fantomas/issues/3446) asked for
once a second caller turned up.
[#​3463](https://github.com/fsprojects/fantomas/issues/3463)

Special thanks to @​nojaf and @​shayanhabibi!


[https://www.nuget.org/packages/fantomas/8.0.0-beta-002](https://www.nuget.org/packages/fantomas/8.0.0-beta-002)
    

## 8.0.0-beta-001

# 8.0.0-beta-001

### Changed
- `=`, `>`, `<`, `%` and `%%` cannot start a line at the column of their
left-hand side: there the parser reads `=`, `>` and `<` as the `=` of a
binding, and reads `%` and `%%` inside a quotation as a splice. Fantomas
already kept them off that column, but did so with the layout that keeps
the right-hand side beside the operator whenever it is short enough to
fit, which needs the page width and a column count to predict and moves
a `0` around when an unrelated part of the line changes length. It now
follows the rule proposed in
[fsharp/fslang-design#​836](https://github.com/fsharp/fslang-design/issues/836):
the expression fits on one line, or it does not and the operator stays
with the left-hand side while the right-hand side moves one level in, or
the left-hand side itself spans several lines, in which case the
operator and the right-hand side each take a line of their own.
`fsharp_multiline_bracket_style = stroustrup` still overrides this for a
right-hand side that opens a bracket, which keeps hugging the operator.
This is the first beta of `8.0.0`: the style guide for this is not
settled yet, so give it a try and report back on [the design
issue](https://github.com/fsharp/fslang-design/issues/836) if something
reads worse than what it replaces.
[#​3434](https://github.com/fsprojects/fantomas/pull/3434)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-beta-001](https://www.nuget.org/packages/fantomas/8.0.0-beta-001)
    

## 8.0.0-alpha-026

# 8.0.0-alpha-026

### Changed
- `fsharp_multiline_bracket_style = stroustrup` now applies to object
expressions, so `{` stays on the line that opens the binding and `new T
with` moves below it. Up to now object expressions were printed by the
`aligned` branch whatever the setting said, which left `{ new T with` on
one line and made stroustrup the only bracket style that did not reach
every bracket it names. The [Microsoft style
guide](https://learn.microsoft.com/en-us/dotnet/fsharp/style-guide/formatting#formatting-object-expressions)
writes the form out under "Formatting object expressions" and Fantomas
now produces it character for character. This was left alone in the
belief that the layout was an offside error waiting to happen; it is
not, and the compiler accepts it in a binding, a list item, a function
argument, a lambda body, a match clause and a nested `let` alike. An
object expression under stroustrup now lands where a record already
landed in each of those positions. `aligned`, which is the default since
`8.0.0-alpha-002`, and `cramped` are untouched.
[#​2990](https://github.com/fsprojects/fantomas/issues/2990)
- `fsharp_multiline_bracket_style = stroustrup` now survives a binding
whose signature broke across lines, and a match clause under
`fsharp_experimental_keep_indent_in_branch`. Both were printed by
branches written before stroustrup existed and never given a
stroustrup-aware counterpart, so the setting was quietly overruled in
each: a `let` whose parameters wrapped left its `=` alone on a line and
indented the bracket below it, and a match clause under keep-indent did
the same below the arrow. They now read `= {` and `| _ -> {`, with the
items one level in and the closing bracket back at the column the
binding or the clause started from, which is what the same code already
produced when the signature fitted on one line or the setting was off.
This reaches every bracket the setting names, so records, update
records, anonymous records, anonymous structs, lists and arrays move
with the object expressions of the entry above rather than behind them.
The closing bracket of a match clause stays two columns right of the
bar, because on the bar's own column the `|` of the next clause no
longer parses.
[#​3450](https://github.com/fsprojects/fantomas/pull/3450)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-026](https://www.nuget.org/packages/fantomas/8.0.0-alpha-026)
    

## 8.0.0-alpha-025

# 8.0.0-alpha-025

### Fixed
- A `!` line of a `.fantomasignore` takes a path back out again even
when a line above it matched the folder holding that path, so `sub/*`
followed by `!sub/keep` formats and checks the files under `sub/keep`.
Since `8.0.0-alpha-016` a folder an ignore file names is never opened,
which is what a folder pattern should mean and is not what it can mean
here: closing `sub` decides that `sub/keep` is not there, and the line
that would have taken it back out is never reached. The files were not
reported as ignored, they were never found, so `--check` passed over a
scope smaller than the ignore file describes and `doctor`, which asks
about one file and answers correctly, disagreed with the run about that
same file. An ignore file that negates anything now leaves every folder
open and asks about the files inside one at a time, which is what every
version up to `8.0.0-alpha-015` did. An ignore file with no `!` line in
it still closes the folders it names.
[#​3447](https://github.com/fsprojects/fantomas/issues/3447)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-025](https://www.nuget.org/packages/fantomas/8.0.0-alpha-025)
    

## 8.0.0-alpha-024

# 8.0.0-alpha-024

### Fixed
- The dots of a chain that breaks are indented past the head of the
chain, instead of landing on the head's own column or to the left of it.
A dot on the head's column reads as a new item rather than as the chain
continuing, and inside a parenthesis the parser refuses the output
outright: `let v = ((someObject.First(a).Second(b).Third(c)))` at
two-space indentation produced code Fantomas would not accept from
itself, so the file was left unformatted with a bug report on the
console. The dots now step one indent level at a time until they clear
the head, which keeps every column a multiple of the indent size; a
chain whose dots already cleared its head is untouched. This replaces a
branch that anchored a parenthesised chain on its opening parenthesis,
written when only a real dot chain reached it. Since `8.0.0-alpha-014` a
dotted long identifier is a chain too, so it also fired for the likes of
`Seq.map`, which never breaks, and moved a lambda written behind such a
call three columns for no reason.
[#​3445](https://github.com/fsprojects/fantomas/pull/3445)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-024](https://www.nuget.org/packages/fantomas/8.0.0-alpha-024)
    

## 8.0.0-alpha-023

# 8.0.0-alpha-023

### Fixed
- An operator that starts with a star keeps a space on either side of it
inside the parentheses wherever its name is written, so `val inline ( *.
): ...` and `abstract member ( *. ): ...` survive formatting. Only `let
( *. ) a b = ...` was spaced before; a `val` in a signature file and an
abstract member in either kind of file came out as `(*.)`, which opens a
block comment and swallows the rest of the file. Multiplication is
unaffected, because `(*)` is a token the lexer knows.
[#​3443](https://github.com/fsprojects/fantomas/pull/3443)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-023](https://www.nuget.org/packages/fantomas/8.0.0-alpha-023)
    

## 8.0.0-alpha-022

# 8.0.0-alpha-022

### Added
- Extension members on tuple types, `type (int * int) with ...` and
`type struct (int * int) with ...`, format instead of failing. The
parser accepts them since
[dotnet/fsharp#​19602](https://github.com/dotnet/fsharp/pull/19602), and
a type definition's name is now any type in the Oak tree rather than
only an identifier. The tuple prints as written; no layout decision was
added for it. [#​3436](https://github.com/fsprojects/fantomas/pull/3436)

### Changed
- Update FCS to 'Rotate [<return: X>] attributes during binding
normalization', commit 74ec4f7df70717a162d6ffd23007603cf298fb8b
[#​3436](https://github.com/fsprojects/fantomas/pull/3436)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-022](https://www.nuget.org/packages/fantomas/8.0.0-alpha-022)
    

## 8.0.0-alpha-021

# 8.0.0-alpha-021

### Fixed
- A call in the middle of a chain keeps its parenthesis against the
member name when its lambda argument no longer fits, and breaks behind
it instead. Up to now the whole argument moved down a line, `(` and all,
which is what the last call of a chain does, but for an earlier one it
changes what the code means: `a.Foo (x).Bar()` passes `(x).Bar()` to
`Foo` rather than calling `Bar` on the result. With a call behind it the
compiler rejected the result with "This argument expression needs
parentheses"; with a plain member behind it, such as `.Value`, nothing
warned at all and the member quietly became part of the argument.
Hanging the parameters under `(fun` would keep the parenthesis in place
too, and the F# style guide rules that out, because the column they hang
from is the length of the member name. The closing `)` answers to
`fsharp_multi_line_lambda_closing_newline` as it did before, and the
last call of a chain is untouched, since nothing follows it to be
swallowed. [#​3432](https://github.com/fsprojects/fantomas/issues/3432)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-021](https://www.nuget.org/packages/fantomas/8.0.0-alpha-021)
    

## 8.0.0-alpha-020

# 8.0.0-alpha-020

### Fixed
- A list or an array on the right of `=`, `>`, `<`, `%` or `%%` is
indented from the binding again, so `let a = b = [ ... ]` breaks to `b =
[` with its items one level in and the `]` back at the column of `b`.
Since `8.0.0-alpha-003` both the items and the closing bracket sat a
further level to the right. The right-hand side of these operators is
indented so that a comment between the operator and that side lands
below the operator, which is what
[#​2944](https://github.com/fsprojects/fantomas/issues/2944) asked for,
and so that the lines under a chain or an application are not read as a
continuation of the left-hand side. A list or an array needs neither:
its bracket says where the right-hand side begins and indents its items
from the left-hand side by itself, so indenting it again pushed the
items and the closing bracket a level too far. This concerns `aligned`,
which is the default, and `stroustrup`. `cramped` lines the items up
under the opening bracket, so the indent never reached them and nothing
changes there. Right-hand sides that open a bracket in some other way,
such as `seq { }`, `<@ @>`, `begin ... end` and `new T(...)`, still
carry the extra level; where a no-break operator should put what follows
it is a wider question than this fix.
[#​3428](https://github.com/fsprojects/fantomas/issues/3428)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-020](https://www.nuget.org/packages/fantomas/8.0.0-alpha-020)
    

## 8.0.0-alpha-019

# 8.0.0-alpha-019

### Fixed
- A call on a constant receiver keeps its parenthesis tight, so
`"yow".Substring (0, 3)` and `3L.ToString ()` come out as
`"yow".Substring(0, 3)` and `3L.ToString()`. `8.0.0-alpha-018` gave a
constant receiver a space it was never meant to have. The rule there
says a call keeps the space only when the whole thing being called is a
plain dotted name, and the comment agreed at
[fslang-design#​648](https://github.com/fsharp/fslang-design/issues/648)
lists `"yow".Substring(0, 3)` under "a receiver that is not a name",
beside `(f x)` and `[ 1; 2 ]`. The alpha carved constants back out on
the reasoning that a constant is atomic rather than bracketed, which
went past what was agreed and was not deliberate. A constant is a value,
the rule asks for a name, so it goes tight. A type parameter really is a
name, so `'T.set_StaticProperty (3)` is unaffected.
[#​3426](https://github.com/fsprojects/fantomas/pull/3426)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-019](https://www.nuget.org/packages/fantomas/8.0.0-alpha-019)
    

## 8.0.0-alpha-018

# 8.0.0-alpha-018

### Added
- `doctor` names the `.fantomasignore` above the one that governs a
file, when a pattern in it would have skipped that file. Fantomas reads
the nearest ignore file at or above a file and no other, so a pattern
written at the root of a repository has no effect on a folder that has
an ignore file of its own beside it. Up to now the report named the
ignore file that did decide and said nothing about the one that did not,
which answers "which file" and leaves "then why did mine not" for the
reader to work out. It quotes the pattern that would have matched, with
its line number, the way it already quotes the line that decided.
`--json` carries every ignore file above the governing one under
`shadowed`, each with `wouldIgnore` and the lines of it that match,
whether or not it would have decided anything.
[#​3423](https://github.com/fsprojects/fantomas/pull/3423)

### Changed
- The settings that ask for a space before the parenthesis of a call,
`fsharp_space_before_uppercase_invocation` and
`fsharp_space_before_lowercase_invocation`, now get a say only when the
whole thing being called is a plain dotted name. Put a call, an index, a
bracketed receiver, or a type application anywhere in it, and the
parenthesis stays tight whatever the settings ask for. On default
settings `xs.map(fun a -> a + 1).filter (fun a -> a > 1)` becomes
`xs.map(fun a -> a + 1).filter(fun a -> a > 1)`, which is the report
this started from, and `Foo().bar ()` and `myList.[7].someFunction
(arg)` lose their space for the same reason. A plain dotted name is
untouched however long it gets, so `List.map (f)` and
`Fantomas.FCS.Text.Range.unionRanges (r1, r2)` keep the space they had,
and so is a literal or type parameter receiver, since `"yow".Substring
(0, 3)` and `'T.set_StaticProperty (3)` are atomic rather than
bracketed. Agreed at
[fslang-design#​648](https://github.com/fsharp/fslang-design/issues/648)
and written up under [Formatting chain
expressions](https://fsprojects.github.io/fantomas/docs/contributors/Chains.html).
[#​3425](https://github.com/fsprojects/fantomas/pull/3425)
- A type application now makes a call tight with no chain in sight,
which is the part of the change above most likely to catch you out,
because it reaches ordinary generic calls rather than fluent code. On
default settings `unbox<bool> (value)`, `f<int> (x)`, `List.map<int>
(f)` and `jsOptions<Vis.Options> (fun o -> ...)` all lose their space,
since `fsharp_space_before_lowercase_invocation` is `true` and each of
those is a lower-case name carrying type arguments. An upper-case
generic call such as `Dictionary<string, int>(x)` was already tight by
default and only moves if `fsharp_space_before_uppercase_invocation` is
on. Fantomas does not add parentheses, so the far more common form
without them, `unbox<int> obj`, is left exactly as written and never
comes into it.
[#​3425](https://github.com/fsprojects/fantomas/pull/3425)

### Fixed
- More expression forms count as open-ended, so a record, list, array or
tuple that holds one of them in a non-last position stays multiline
instead of collapsing onto a line that means something else.
[#​3279](https://github.com/fsprojects/fantomas/issues/3279) settled
`fun`, `if`, `match` and `try`, and left the forms that wrap one of
those or end in one: `lazy`, `yield`, `return`, `do`, `assert` and
`fixed`, an assignment such as `x.P <- fun y -> y`, and `let x = 1 in
body`. `{ A = 1; B = lazy fun x -> x; C = 3 }` reads back as a lambda
that swallowed `C = 3`. A wrapper around something that closes on its
own, `lazy a` or `x.P <- 1`, collapses as it always did. When this
happens, why, and how to get the single line back is written up under
[Open-ended
expressions](https://fsprojects.github.io/fantomas/docs/end-users/OpenEndedExpressions.html).
[#​3424](https://github.com/fsprojects/fantomas/pull/3424)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-018](https://www.nuget.org/packages/fantomas/8.0.0-alpha-018)
    

## 8.0.0-alpha-017

# 8.0.0-alpha-017

### Added
- `fantomas doctor <file>` walks one file through everything Fantomas
does to it and reports what happened at each step: whether the path is a
file Fantomas formats, which `.fantomasignore` governs it and which line
of that file decided, which settings apply and which `.editorconfig` set
each one, what formatting produced and where the result first parts from
the file, whether Fantomas accepts its own output, and whether
formatting that output again leaves it alone. It writes nothing, so it
is safe against a working tree you have not committed. Every question it
answers was answerable before only by reading two configuration formats
by hand and knowing the order Fantomas resolves them in. It takes one
file rather than a folder, because the answers differ per file, and it
exits 0 for a file it could diagnose whatever it found, 1 for a path it
cannot look at or a step that failed. `--json` writes the same walk as
one document with a key per step.
[#​3421](https://github.com/fsprojects/fantomas/pull/3421)
- The `.fantomasignore` line that matched a path is now knowable rather
than only the yes or no. `doctor` quotes it with its line number, the
way `git check-ignore -v` does, and names every line that matches when
more than one does, since a `!` pattern further down is exactly the case
nobody spots by eye.
[#​3421](https://github.com/fsprojects/fantomas/pull/3421)
- `doctor` decides whether a file would be rewritten the way a format
run decides it, by comparing the text as it is. A file whose line
endings are the only thing out of step is therefore reported as needing
formatting, where `fantomas check` deliberately overlooks that
difference and calls the same file already formatted. What it reports is
where the file and the result first part, rather than a count of the
lines that differ by position: that is not a count of edits, because
splitting one long line into five moves every line below it.
[#​3421](https://github.com/fsprojects/fantomas/pull/3421)
- A file under a folder a compiler or a package manager wrote, such as
`obj`, is named as such by `doctor`. A run over the tree above it never
opens that folder, whatever the ignore file says, and sending its owner
to read their `.fantomasignore` sends them to read the wrong file.
[#​3421](https://github.com/fsprojects/fantomas/pull/3421)

### Changed
- Breaking: output that Fantomas will not accept from itself is reported
as what it is, and shows you the part of that output it would not
accept. `Fantomas produced code that is not valid F#.` read as though
the file were at fault, and reaching it always means the opposite: the
input parsed, or a parse failure would have been reported instead. The
report now says that nothing was written and the file is untouched, that
this is a bug in Fantomas rather than a problem with the code, and where
to report it, in the shape a parse failure and an unmodellable construct
already print.
[#​3419](https://github.com/fsprojects/fantomas/pull/3419)
- Breaking: that report carries what the parser said about the rejected
output, and the lines of the output around it with a caret under the
failure, so there is a small reproduction to cut from it. Up to now the
run said only that something was invalid, and finding out what meant
running again with `--force` and reading the result. The diagnostics
carry no line and column of their own: the output they would count into
is written nowhere, so a position in it is a coordinate you cannot
follow, and a path an editor could open would take you to the wrong line
of the right file. The carets say where, and the report says out loud
that the lines below it are the output rather than your file. `--json`
carries the same diagnostics in the `diagnostics` array a parse failure
already uses. `fantomas check` reports all of it the same way, where it
used to run the whole explanation on after `could not be checked:`.
[#​3419](https://github.com/fsprojects/fantomas/pull/3419)
- The two reports that ask you to file a bug, for output Fantomas would
not accept and for a construct it cannot model, name one place to send
it rather than two. They used to add the issue tracker as an alternative
for a file too large for the online tool to carry, which offered a
choice at the point somebody least wants one.
[#​3419](https://github.com/fsprojects/fantomas/pull/3419)
- Breaking: `Fantomas.Core.CodeFormatter.IsValidFSharpCodeAsync` became
`ValidateFSharpCodeAsync` and answers with a `ValidationResult` rather
than a `bool`. Read `.IsValid` off it where the verdict was all you
wanted; `.Diagnostics` is what Fantomas refused, positioned, and is
empty exactly when the source is valid. The boolean discarded it, so
anything that had to say why had to parse the source a second time to
find out, which is why the tool could not show you its own bad output. A
warning Fantomas tolerates, such as
[#​3396](https://github.com/fsprojects/fantomas/issues/3396) on IWSAM
types, is not among them.
[#​3419](https://github.com/fsprojects/fantomas/pull/3419)

### Fixed
- A `[<return: ...>]` attribute written in front of an `extern`
declaration was dropped from the output. The parser moves such an
attribute out of the binding's attribute list and into its arity
information, and the `extern` path never looked there, so the line was
silently deleted. Bindings already put those attributes back; `extern`
now does the same. An attribute written on the return type itself,
`extern [<MarshalAs(UnmanagedType.I1)>] bool f(int options)`, is
reported in both places and stays where it was written.
[#​3420](https://github.com/fsprojects/fantomas/issues/3420)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-017](https://www.nuget.org/packages/fantomas/8.0.0-alpha-017)
    

## 8.0.0-alpha-016

# 8.0.0-alpha-016

### Added
- `fantomas check <paths>` and `fantomas daemon`, beside the `--check`
and `--daemon` flags that named them. Both flags keep working and are
not deprecated, so no pipeline and no editor integration has anything to
change; each means exactly what its command means. The commands exist
because a command can have a `--help` page of its own and a flag cannot:
`fantomas check --help` lists the flags checking has any use for and
leaves out the ones it would refuse. When standard error is a terminal,
the older spelling prints a one line note saying how it is spelled now;
a redirected stream never sees it, so it stays out of build logs and out
of the daemon an editor starts.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- `fantomas profile <paths>`, replacing the `--profile` flag. It formats
one file at a time so the timings can be compared, writes nothing,
reports every file slowest first with its line count and the number of
define combinations it had to format, and ends with a total. `--profile`
measured files formatted in parallel, so each stopwatch recorded wall
clock under contention: a six line signature file and a file of several
thousand lines came back with the same figure.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- A `--help` page for each command. `fantomas --help` is the overview,
with the commands, the flags, worked examples and links; `fantomas check
--help`, `fantomas profile --help` and `fantomas daemon --help` are
about one command each. Which flags a command's page lists is asked of
the same rule that refuses them at run time, so the page cannot come to
disagree with the tool. Every page names the tool the way this run was
started, so a local tool install is shown `dotnet fantomas` rather than
a command it does not have.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- `--flag=value` alongside `--flag value`, so `--out=build` and
`--verbosity=d` work. Only the space separated form was accepted before,
and `--out=build` was read as an input path that does not exist.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- `--` ends the flags, so a path beginning with a dash can be named
after it. It used to be read as an input path itself, which meant no
such path could be given at all.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- `--json` for `fantomas profile`, carrying the same files as the text
report with each file's line count, define combinations and
milliseconds, and the run's own total as `elapsedMilliseconds`. Ordered
by path rather than slowest first, since a reader that wants them by
time can sort them.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Running with no path at all formats the folder you are in, so
`fantomas` is `fantomas .`, and `check` and `profile` read a bare
invocation the same way. Formatting and checking used to refuse with `No
input path provided.` This is what `ruff format` and `dotnet format` do.
Note that it walks every F# file below the working directory, and that a
`.fantomasignore` in a repository below it governs only that
repository's files.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)

### Changed
- Breaking: `Fantomas.Core.ParseException` now derives from
`FormatException`, as the other exceptions the library raises already
did, so `:? FormatException` catches every way formatting can fail. It
could not before: it was declared with F#'s `exception` keyword, which
cannot name a base class. Its `Message` names the first error by
position instead of dumping every diagnostic record through `%A`, and
the records are reachable as `.Diagnostics` rather than only by matching
the exception pattern. Code matching `| ParseException diagnostics ->`
becomes `| :? ParseException as e -> e.Diagnostics`; raising and
constructing it are unchanged.
[#​3415](https://github.com/fsprojects/fantomas/pull/3415)
- A construct Fantomas cannot model is now reported through the same
mechanism as a parse failure, instead of as a `%A` dump of a syntax tree
node in the middle of the message. The report says in words what could
not be modelled, names the union case responsible, positions it in the
file being formatted rather than in the `tmp.fsx` the parser was handed,
and draws a snippet of the source with a caret under the construct, so
the enclosing declaration is visible without having to bisect the file
by hand. The syntax tree node is still reported, at `--verbosity d`,
where it serves whoever triages the issue rather than whoever files it.
This covers every such report, from a type the transformer has no Oak
node for to the chain and leading-keyword invariants behind it.
`--check` and the daemon position them the same way, so an editor shows
its user the snippet rather than a bare line and column.
[#​3415](https://github.com/fsprojects/fantomas/pull/3415)
- Breaking: everything a format or check run prints was rewritten. Each
file gets a sentence of its own opening with a status character, `+
src/A.fs was formatted.`, and the run ends with a line of counts, `2
files formatted, 30 unchanged.` This replaces the bordered table of
headings that a folder run printed, which bypassed the logger and so
carried no timestamp at `--verbosity d` while everything around it did.
A run over a single named file is answered on its own terms and has no
summary added to it. Any script reading this output needs updating;
`--json` exists for a caller that has to act on the result rather than
read it. [#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: `.fantomasignore` is resolved per file, the nearest one at
or above it, which is what the daemon has always done. The command line
used to resolve one file for the whole run from the directory it started
in, so an ignore file in a subfolder was honoured by an editor and
invisible to a pipeline: the same file was skipped in one and formatted
in the other. A nested ignore file that used to have no effect on a
command line run now has one.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: a folder that `.fantomasignore` names is no longer opened.
Up to now every file inside it was found and then rejected one at a
time; the folder is now passed over, so nothing inside it is read,
counted or reported. A folder pattern spelled with a trailing separator,
`vendor/`, closes the folder as `vendor` always did. Nothing is
formatted that was not formatted before.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: a run that skipped files no longer says how many. The count
could not be honest: an ignore pattern naming a file can be counted, and
one naming a folder cannot, because the folder is never opened and what
is inside it is unknown by design. A number right about the first and
blind to the second reads as though it covered both. A file named on the
command line still gets a line of its own, since a count is the only
other place a path could be accounted for and no count carries this one;
a file found by walking a folder, and the folder itself, are named at
`--verbosity d`.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: `--json` no longer names a file that `.fantomasignore`
matched, and `ignored` is no longer a status a file can carry. Nothing
counts them in its place, for the reason above: `files` is what the run
looked at. [#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: a token beginning with a dash that is not a flag Fantomas
has is reported as an unknown flag, with the nearest flag it could be a
misspelling of. `fantomas --chek src` said `Input path '--chek' not
found.` and now says `'--chek' is not a Fantomas flag. Did you mean
'--check'?` Every unrecognised token used to be taken for an input path,
so a misspelled flag was reported as a file that is not there.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: a malformed command line exits 1 rather than 2. The
documented exit codes have only ever been 0, 99 and 1; 2 came from the
argument parser and was never one Fantomas chose.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: repeating a flag is allowed and the last one wins, where it
used to be refused outright with `argument '--check' has been specified
more than once`. This is the Unix norm and stops a script that builds
its arguments up from failing on a duplicate. `--out` is the exception
and is still refused, because it decides where files are written and
choosing between two of them quietly is choosing where the work lands.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: a file that could not be parsed is reported as `src/A.fs
could not be parsed by Fantomas:` rather than `Fantomas could not parse
src/A.fs:`, and a construct that could not be modelled as `src/A.fs
could not be formatted by Fantomas:`. Every line of a report now begins
with the path it is about, so the column can be read down. A script
matching on the old text needs updating.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: `--check` no longer reports a file that will not parse
twice, once as an error and once as needing formatting. It is an error
and nothing else, which is also what its exit code has always said.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: `--force` announces invalid output as a warning on standard
error rather than on standard out, and says what happened: `src/A.fs was
formatted, but the result is not valid F# code. It was written because
--force was given.` It says Fantomas wrote F# it believes is not valid,
which is the opposite of the ordinary run of things standard out
carries. [#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- `--version` prints the commit hash trimmed to the short form the
`--help` page has always shown, and the whole of it at `--verbosity d`.
`Fantomas.Client` is unaffected: it cuts the version at `+` and drops
the build metadata whatever it holds.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- A failure says what went wrong at any verbosity. The exception's
message used to be kept for `--verbosity d` and nothing printed in its
place at normal, so an unreadable file reported only `A.fs could not be
formatted.` and left `Access to the path is denied` behind a flag nobody
knew to pass. Detailed verbosity now adds the type and the stack trace
below that line rather than instead of it.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Colour is used where the terminal takes it and dropped where it does
not, decided for standard out and standard error separately, so a run
whose output is piped keeps its colour on the diagnostics still going to
the terminal. The status character is `+`, `=`, `-`, `!` and `x` where
the output is redirected or the console is not UTF-8, and `✔`, `=`, `○`,
`!` and `✘` where it is; both carry the same five states, and the words
beside them say the same thing either way. `NO_COLOR` is honoured.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Files are reported in path order rather than in whichever order the
file system returned them, so two machines running one command print the
same thing. [#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: a run writing to `--out` reports what was written rather
than what changed, `32 files written to build, 2 reformatted.`, since
under `--out` every input produces an output file whether or not its
content changed. A single file named on the command line says where it
went, `src/A.fs was formatted and written to build/A.fs.`, where it used
to say only that it was formatted or unchanged and leave the destination
unmentioned. [#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- A run that looked at no file at all says so on standard error and
exits 0, which a check used to pass over in silence: `fantomas check` on
an empty folder, or on one an ignore file emptied, printed nothing and
read as a green build. Formatting and checking now report this the same
way, as they do everything else they both have to say.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)

### Fixed
- Getting a 'no Oak node is defined for this type' error when formatting
a p/invoke signature with nullability annotations.
[#​3414](https://github.com/fsprojects/fantomas/issues/3414)
- An `extern` declaration returning an array, such as `extern byte[]
f(int options)`, lost the element type of the array and was written as
``extern `[]` f(int options)``.
[#​3415](https://github.com/fsprojects/fantomas/pull/3415)
- A file whose formatted output is not valid F# named that file twice in
one line, `Failed to format file: A.fs : Formatting A.fs leads to
invalid F# code`, with a spaced colon in the middle.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- At `--verbosity d`, a single unchanged file was reported twice in two
different spellings, once as `'A.fs' was unchanged` and once as `A.fs
was unchanged.`
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)

### Removed
- Breaking: the `--profile` flag. `fantomas profile <paths>` replaces
it, and a run that names the flag is refused with `'--profile' is not a
Fantomas flag. 'profile' is a command: try 'fantomas profile <paths>'.`
and exits 1. It was not kept working as an alias, alone among the flags
that became commands: the flag wrote formatted files to disk as a side
effect of measuring them and the command deliberately writes nothing, so
an alias would have gone on being accepted and silently stopped writing,
which is worse than a run that stops and says what to type. `--profile
--check`, which was accepted and did nothing at all, no longer has
anything to combine.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-016](https://www.nuget.org/packages/fantomas/8.0.0-alpha-016)
    

## 8.0.0-alpha-015

# 8.0.0-alpha-015

### Added
- Support for the record spread syntax introduced in F# preview, [RFC
FS-1151](https://github.com/fsharp/fslang-design/pull/805). A spread can
appear in a record expression, `{ ...source; Field = value }`, in an
anonymous record expression, `{| ...source; Field = value |}`, and in
the record representation of a type definition, `type Target = {
...Source; Field: int }`, in both implementation and signature files.
[#​3400](https://github.com/fsprojects/fantomas/pull/3400)
- Interpolated strings with a negative alignment, `$"{value,-10}"`, now
format instead of failing with a parse error. Alignment and format
specifiers keep their existing layout, so `$"{value,10:N2}"` is
unaffected. [#​3400](https://github.com/fsprojects/fantomas/pull/3400)
- `--json` writes one JSON document to standard out describing what the
run did, instead of the usual messages, for a script or an agent that
has to act on the result rather than read it. Every file the run looked
at is named with a status of `formatted`, `unchanged`, `ignored`,
`needs-formatting` or `error`, and a file that failed to parse carries
the severity, code, message and one based position of every diagnostic.
Paths are reported as they were given, relative to the
`workingDirectory` the document carries once. Standard out carries the
document and nothing else, so it can be piped straight into a parser,
while warnings still go to standard error. Exit codes are unchanged and
the document repeats the one the process ends with. Works with
`--check`, and is refused alongside `--daemon`, where standard out
already carries the JSON-RPC protocol.
[#​3412](https://github.com/fsprojects/fantomas/pull/3412)

### Changed
- Breaking: warnings and errors are written to standard error instead of
standard out. Informational output stays on standard out, including
`--version` and the files `--check` reports as needing formatting, so a
caller can tell the tool's output apart from its diagnostics by stream.
Scripts that capture standard out to detect failures need to capture
standard error as well.
[#​3399](https://github.com/fsprojects/fantomas/pull/3399)
- Update FCS to 'Parser: recover on missing when conditions', commit
d05075e098278aedcea3379159504d664628a495
[#​3400](https://github.com/fsprojects/fantomas/pull/3400)
- Breaking: the `--help` page is written by Fantomas instead of by Argu.
It carries the version, worked examples, what an input path may be, and
links to the documentation, the F# Discord and the `llms.txt` files an
LLM can read. Colours are used when the terminal supports them and
dropped when standard out is redirected. `-h` is now accepted alongside
`--help`. An argument error reports the complaint on standard error
followed by a pointer to `--help`, where it used to print Argu's usage
block. [#​3402](https://github.com/fsprojects/fantomas/pull/3402)
- Breaking: a run over a single file reports the path it was given
instead of only the file name, so `fantomas src/A.fs` prints `src/A.fs
was formatted.` where it printed `A.fs was formatted.`. The same applies
to the unchanged, ignored and failure messages. A run over several files
already reported the path, so the two now agree.
[#​3404](https://github.com/fsprojects/fantomas/pull/3404)
- Breaking: a file that cannot be parsed is reported with the position
of every diagnostic the parser produced, one MSBuild style line each,
followed by a snippet of the source with two lines of context either
side and a caret under the offending range. This replaces `Could not
parse the file.` for a format run, the `%A` record dump and stack trace
for a `--check` run, and the same `%A` dump the daemon used to hand
editors. Diagnostics are ordered by position and columns are one based,
matching what the F# compiler prints for the same file.
[#​3405](https://github.com/fsprojects/fantomas/pull/3405)
- Breaking: the messages for an input path Fantomas cannot work with are
now the same whether the run formats or checks. `--check` used to report
`Input path 'x' is unsupported file type` and `Input path 'x' not found`
without a full stop where a format run ended both with one, and a run
with no input path at all said `No input path provided.` when checking
and `Input path is missing.` when formatting. Both now report `Input
path 'x' is an unsupported file type.`, `Input path 'x' not found.` and
`No input path provided. Call with --help for usage information.`
[#​3406](https://github.com/fsprojects/fantomas/pull/3406)
- Breaking: a single file that `.fantomasignore` matches is reported the
same way as an ignored file found while walking a folder. `fantomas
A.fs` on an ignored file printed nothing unless `--verbosity d` was
given, where `fantomas ./folder` containing only that file printed `A.fs
was ignored.` Both now print it. A `--check` run reports the files it
ignored at detailed verbosity whether they were named directly or found
in a folder, where before only a directly named file was reported.
[#​3406](https://github.com/fsprojects/fantomas/pull/3406)
- At detailed verbosity, a run writing to `--out` now notes a file it
found already formatted, as a run formatting in place always did. Both
paths decide what formatting came to in the same place, so they say the
same things about it.
[#​3406](https://github.com/fsprojects/fantomas/pull/3406)
- Breaking: `--daemon` no longer accepts the arguments that mean nothing
to a daemon. `--check`, `--out`, `--force`, `--profile`, `--json` and
input paths were all accepted and then silently ignored, so `fantomas
--daemon ./src` looked like it would format a folder and did not. Any of
them now reports which ones were refused and exits 1 without starting.
`--verbosity` is still accepted, since it sets the level the daemon logs
at, and `--version` wins outright. `Fantomas.Client` launches the daemon
with no other arguments, so no editor integration is affected.
[#​3412](https://github.com/fsprojects/fantomas/pull/3412)
- `--version` now answers whatever else is on the command line, and
before any of it is validated, so it can always be used to find out what
you are running. It used to be refused alongside `--daemon`, and
`fantomas --version -v bogus` exited 1 without printing a version. It is
also written straight to standard out instead of through the logger, so
it no longer picks up the timestamp and level prefix that `--verbosity
d` adds, and reads the same at any verbosity.
[#​3412](https://github.com/fsprojects/fantomas/pull/3412)
- Breaking: `Fantomas.Core` is no longer binary compatible with `v7`.
Several discriminated unions are structs now, which changes nothing
about how they are written or matched, but an assembly compiled against
`v7` has to be rebuilt.
[#​3407](https://github.com/fsprojects/fantomas/pull/3407)
- A setting in `.editorconfig` that carries the `fsharp_` prefix but is
not a Fantomas setting now warns instead of being silently ignored, and
where the intent is obvious the warning names the spelling that works.
This catches a misspelling such as `fsharp_multiline_brackets_style`,
and catches prefixing one of the four settings editorconfig itself
defines, where `fsharp_max_line_length` never applied and
`max_line_length` is the one that does. Settings without the `fsharp_`
prefix belong to other tools and are left alone. Every problem in one
`.editorconfig` is reported together and only once per run, rather than
once per formatted file, and `--verbosity d` writes out every setting
the running version supports.
[#​3401](https://github.com/fsprojects/fantomas/pull/3401)
- `.editorconfig` keys and values are both matched without regard to
case, as the editorconfig specification defines them.
`FSHARP_MAX_RECORD_WIDTH` and `fsharp_experimental_elmish = True` used
to be ignored.
[#​3401](https://github.com/fsprojects/fantomas/pull/3401)
- The daemon sends a `fantomas/configurationWarning` notification for
every format request, naming the settings in the resolved configuration
it could not act on, and sending an empty list when there are none so an
editor can clear what it showed earlier. Additive to the JSON-RPC
contract: a client that does not handle the method ignores it and keeps
working. See the [Fantomas.Client
changelog](https://github.com/fsprojects/fantomas/blob/main/src/Fantomas.Client/CHANGELOG.md)
for the client-side API.
[#​3401](https://github.com/fsprojects/fantomas/pull/3401)

### Fixed
- `--verbosity` with an unrecognised value exited with code 1 and
printed nothing, because the message was logged before the logger was
configured. It now reports `Invalid verbosity level` on standard error.
[#​3399](https://github.com/fsprojects/fantomas/pull/3399)
- `--out <folder>` flattened the input tree: every file landed directly
in the output folder, so two files with the same name in different
subfolders silently overwrote each other. The output folder now mirrors
the structure of the input folder, as the documentation already
promised. [#​3403](https://github.com/fsprojects/fantomas/pull/3403)
- `--out <file>` failed with `Failed to format file` and exit code 1
when the folder of the given path did not exist. Fantomas now creates
it, along with the subfolders that mirroring an input folder needs.
[#​3403](https://github.com/fsprojects/fantomas/pull/3403)
- `--out` naming the input folder under a different spelling, such as
`fantomas src --out ./src`, emptied every file it was given. The output
file was opened, and therefore truncated, before the input was read.
Nothing is written now until formatting has produced the text to write,
which also leaves the previous output in place when formatting fails.
[#​3403](https://github.com/fsprojects/fantomas/pull/3403)
- `--out` pointing inside the input folder, such as `fantomas src --out
src/formatted`, formatted the previous run's output again and nested it
one folder deeper on every run. The output folder is left out of the
input scan. [#​3403](https://github.com/fsprojects/fantomas/pull/3403)
- `fantomas src/ --out src` formatted nothing and reported an empty
table, where `fantomas src --out src` formatted the folder in place. A
trailing separator made the two paths compare as different places, so
every file below the input folder was taken for a previous run's output
and skipped. The same applied to `fantomas src --out src/`.
[#​3406](https://github.com/fsprojects/fantomas/pull/3406)
- A file whose extension is not lowercase, such as `A.FS`, was refused
as an unsupported file type, and one found while walking a folder was
skipped. The extension is now compared without case, which is what a
volume that ignores case, as macOS and Windows usually do, means by the
same file. [#​3406](https://github.com/fsprojects/fantomas/pull/3406)
- Given several input paths, a folder whose name contains a dot, such as
`fantomas my.stuff src`, was taken for a file and reported as `Failed to
format file`. A file with no extension was taken for a folder. Which one
a path is, is now asked of the file system rather than guessed from the
name. A single input path was already classified this way.
[#​3406](https://github.com/fsprojects/fantomas/pull/3406)
- A `.fantomasignore` pattern that cannot be matched against a path
reported the raw exception, with its stack trace, through `%A`. It now
names the file and the ignore file that could not be told apart, and
keeps the exception for detailed verbosity.
[#​3406](https://github.com/fsprojects/fantomas/pull/3406)
- `--profile` reported `Line count: 0` for a file whose line endings are
not the ones the platform uses, because it counted occurrences of the
platform's newline rather than line breaks. A file saved with line feeds
counted nothing on Windows, and one saved with carriage returns counted
nothing elsewhere.
[#​3406](https://github.com/fsprojects/fantomas/pull/3406)
- The daemon held its JSON-RPC message loop while reading an
`.editorconfig`, so a request that arrived during it was not read until
that finished. It hands the loop back first now, and serves one request
at a time per file so that the configuration warnings for a file still
arrive in the order the requests did.
[#​3401](https://github.com/fsprojects/fantomas/pull/3401)
- A value that meant something to one setting decided the outcome for
every other setting, because each value was tried against every parser
in turn. `fsharp_max_record_width = cr` failed the whole run with
`Carriage returns are not valid for F# code`; it is now reported as a
value that setting does not accept. A value is only read as the type its
own setting has.
[#​3401](https://github.com/fsprojects/fantomas/pull/3401)
- A misspelling of one of the four settings editorconfig itself defines
was silently ignored, where a misspelling of a `fsharp_` setting was
reported. `max_line_lenght = 100` now says so. Only names within two
edits of a setting Fantomas has are read this way, so settings belonging
to other tools, `indent_style` among them, stay silent.
[#​3401](https://github.com/fsprojects/fantomas/pull/3401)
- A negative number was accepted for any setting that takes one, so
`fsharp_max_record_width = -5` formatted to nonsense widths without
saying anything. It is now reported like any other value the setting
does not accept.
[#​3401](https://github.com/fsprojects/fantomas/pull/3401)

Special thanks to @​claude and @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-015](https://www.nuget.org/packages/fantomas/8.0.0-alpha-015)
    

## 8.0.0-alpha-014

# 8.0.0-alpha-014

### Changed
- Bump `StreamJsonRpc` to `2.25.29`. This clears the NuGet vulnerability
warnings coming from the transitive `MessagePack` and
`Nerdbank.MessagePack` dependencies.
[#​3393](https://github.com/fsprojects/fantomas/pull/3393)
- Breaking: `Expr.Chain` is redesigned around a head, a list of
`ChainSegment` and a `ChainTerminal`. `Expr.DotLambda`,
`Expr.DotIndexedGet`, `Expr.AppLongIdentAndSingleParenArg` and
`Expr.NestedIndexWithoutDot` are removed, and a dotted long identifier
now yields `Expr.Chain` instead of `Expr.OptVar` in expression position.
See the [upgrade
guide](https://fsprojects.github.io/fantomas/docs/end-users/UpgradeGuide.html)
for how to migrate.
[#​3385](https://github.com/fsprojects/fantomas/pull/3385)
- Chains are laid out by a documented set of rules, written up in full
under
[Chains](https://fsprojects.github.io/fantomas/docs/contributors/Chains.html).
They are a proposal for the F# style guide and may still change. A chain
that fits on one line is left alone. When one has to break, only a call
claims a line of its own and plain property access rides along in front
of it, where a comment between the steps previously put every step on
its own line. A long run of property access now wraps into balanced
lines instead of overflowing the margin.
[#​3385](https://github.com/fsprojects/fantomas/pull/3385)
- With `fsharp_multi_line_lambda_closing_newline = false` (the default),
a match lambda argument keeps `function` beside the `(` when the call is
reached through a dot, matching what a call without a receiver already
did. [#​3385](https://github.com/fsprojects/fantomas/pull/3385)
- With `fsharp_multi_line_lambda_closing_newline = true`, a lambda
argument whose opening line does not fit moves onto its own line instead
of hanging its parameters under the opening parenthesis. This applies to
calls with and without a receiver.
[#​3385](https://github.com/fsprojects/fantomas/pull/3385)
- Added `InvariantViolationException`, raised when Fantomas reaches a
state its own model says is impossible. It derives from
`FormatException`, carries the source range of the construct involved,
and points at the issue tracker.
[#​3385](https://github.com/fsprojects/fantomas/pull/3385)

### Fixed
- CLI refuses to format files containing IWSAM types (warning 3535 fails
output validation).
[#​3396](https://github.com/fsprojects/fantomas/issues/3396)
- A space was added before the parenthesis of a call inside a `_.`
shorthand lambda when `fsharp_space_before_uppercase_invocation` was
enabled, producing code that does not compile.
[#​3364](https://github.com/fsprojects/fantomas/issues/3364)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-014](https://www.nuget.org/packages/fantomas/8.0.0-alpha-014)
    

## 8.0.0-alpha-013

# 8.0.0-alpha-013

### Changed
- Improved formatting performance. Formatting is roughly 1.5 to 2 times
faster and allocates around 65% less memory, depending on hardware.
[#​3388](https://github.com/fsprojects/fantomas/pull/3388)
- The command line tool now runs with Server GC, which speeds up
formatting multiple files in parallel at the cost of a larger memory
footprint. [#​3388](https://github.com/fsprojects/fantomas/pull/3388)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-013](https://www.nuget.org/packages/fantomas/8.0.0-alpha-013)
    

## 8.0.0-alpha-012

# 8.0.0-alpha-012

### Changed
- Bump `FSharp.Core` to `10.0.100`.
[#​3353](https://github.com/fsprojects/fantomas/pull/3353)

### Fixed
- Chained `?` operator accesses (e.g. `x?a("")?b(t)`) no longer add a
space before parenthesised arguments, which previously changed how the
next `?member` was parsed. Detected during AST→Oak transformation and
represented as a new `Expr.DynamicChain` node so the printer can keep
the chain tight; lone `?` calls still respect
`SpaceBefore(Upper|Lower)caseInvocation`.
[#​3159](https://github.com/fsprojects/fantomas/issues/3159)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-012](https://www.nuget.org/packages/fantomas/8.0.0-alpha-012)
    

## 8.0.0-alpha-011

# 8.0.0-alpha-011

### Fixed
- Unicode strings with combining characters caused incorrect column
tracking, leading to wrong indentation decisions.
[#​2945](https://github.com/fsprojects/fantomas/issues/2945)

Special thanks to @​Copilot and @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-011](https://www.nuget.org/packages/fantomas/8.0.0-alpha-011)
    

## 8.0.0-alpha-010

# 8.0.0-alpha-010

### Fixed
- Struct constructor with unparenthesized parameter incorrectly renamed,
causing a compile error.
[#​3349](https://github.com/fsprojects/fantomas/issues/3349)

Special thanks to @​Copilot and @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-010](https://www.nuget.org/packages/fantomas/8.0.0-alpha-010)
    

## 8.0.0-alpha-009

# 8.0.0-alpha-009

### Fixed
- Comment without code after it in the current scope should retain their
"parent" indentation.
[#​2286](https://github.com/fsprojects/fantomas/issues/2286)
- Code comment after expression is assigned to parenthesis.
[#​1864](https://github.com/fsprojects/fantomas/issues/1864)
- Misaligned comment in MultiLineBlockBracket.
[#​1716](https://github.com/fsprojects/fantomas/issues/1716)
- Comment unindented after DU cases.
[#​2606](https://github.com/fsprojects/fantomas/issues/2606)
- Comment on last match case has wrong indentation.
[#​2653](https://github.com/fsprojects/fantomas/issues/2653)
- Commented line inside of record should be indented at the same level
as the fields
[#​2482](https://github.com/fsprojects/fantomas/issues/2482)
- Unindentation of comment before end of computation expression.
[#​2476](https://github.com/fsprojects/fantomas/issues/2476)
- Comments lost the indented after formatting.
[#​2362](https://github.com/fsprojects/fantomas/issues/2362)
- Comments can be lost or shifted when at the end of expressions.
[#​932](https://github.com/fsprojects/fantomas/issues/932)
- Should not move the starting point of a single-line comment.
[#​1233](https://github.com/fsprojects/fantomas/issues/1233)

Special thanks to @​Copilot and @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-009](https://www.nuget.org/packages/fantomas/8.0.0-alpha-009)
    

## 8.0.0-alpha-008

# 8.0.0-alpha-008

### Added
- Support for `#elif` preprocessor directive.
[#​3268](https://github.com/fsprojects/fantomas/issues/3268)

### Fixed
- Multiline `val` body in signature files was not indented correctly.
[#​3269](https://github.com/fsprojects/fantomas/pull/3269)
- `///` doc comment without associated declaration (e.g. at end of file)
was duplicated when formatting.
[#​2499](https://github.com/fsprojects/fantomas/issues/2499)
- NamedIndexedPropertySet with two SynLongIdent removed space.
[#​3273](https://github.com/fsprojects/fantomas/issues/3273)
- `%%` (double-percent) infix operator moved to new line, producing
invalid F#. [#​2107](https://github.com/fsprojects/fantomas/issues/2107)
- Indentation warning when formatting `match` with long anonymous record
discriminant.
[#​1903](https://github.com/fsprojects/fantomas/issues/1903)
- Index-without-dot with variable key followed by unit arguments added
spurious spaces, e.g. `dict[key] () ()` became `dict [ key ] () ()`.
[#​2519](https://github.com/fsprojects/fantomas/issues/2519)
- Open-ended expressions (lambda, if-then-else, match, ...) in non-last
positions of infix, tuple, list/array, and record expressions now stay
multiline to preserve semantics.
[#​3279](https://github.com/fsprojects/fantomas/issues/3279)
- Lambda in tuple in list on single line changes code meaning.
[#​3278](https://github.com/fsprojects/fantomas/issues/3278)
- Custom operator applied to lambda collapses to single line changing
semantics. [#​3274](https://github.com/fsprojects/fantomas/issues/3274)

Special thanks to @​Copilot and @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-008](https://www.nuget.org/packages/fantomas/8.0.0-alpha-008)
    

## 8.0.0-alpha-007

# 8.0.0-alpha-007

### Changed
- Update FCS to 'Treat warn directives as trivia', commit
ab1f6ceaaec997d2854ac1c07a6c0f107675d95c
[#​3263](https://github.com/fsprojects/fantomas/issues/3263)

### Fixed
- `#nowarn` in arbitrary places produces a parsing failure.
[#​3263](https://github.com/fsprojects/fantomas/issues/3263)

Special thanks to @​Copilot and @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-007](https://www.nuget.org/packages/fantomas/8.0.0-alpha-007)
    

## 8.0.0-alpha-006

# 8.0.0-alpha-006

### Fixed
- Space between uppercase function name and uppercase parameter was
dropped. [#​3264](https://github.com/fsprojects/fantomas/pull/3264)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-006](https://www.nuget.org/packages/fantomas/8.0.0-alpha-006)
    

## 8.0.0-alpha-005

# 8.0.0-alpha-005

### Fixed
- Closing `>` in nested multiline generic type applications violates
offside rule.
[#​3243](https://github.com/fsprojects/fantomas/issues/3243)
- Index-without-dot syntax followed by application arguments was
inconsistently formatted.
[#​3259](https://github.com/fsprojects/fantomas/pull/3259)

Special thanks to @​Copilot and @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-005](https://www.nuget.org/packages/fantomas/8.0.0-alpha-005)
    

## 8.0.0-alpha-004

# 8.0.0-alpha-004

### Changed
- Improved error message when conditional compilation directives produce
invalid syntax for some define combinations.
[#​563](https://github.com/fsprojects/fantomas/issues/563)

### Fixed
- Lambda in non-last record field is now parenthesized when record is
collapsed to single line to avoid producing invalid code.
[#​3246](https://github.com/fsprojects/fantomas/issues/3246)

Special thanks to @​Copilot, @​claude, @​nojaf and @​randrag!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-004](https://www.nuget.org/packages/fantomas/8.0.0-alpha-004)
    

## 8.0.0-alpha-003

# 8.0.0-alpha-003

### Fixed
- End of line comments after infix operators are preserved correctly.
[#​2287](https://github.com/fsprojects/fantomas/issues/2287)
- Lambda closing parenthesis in chained method calls is no longer placed
on its own line when `MultiLineLambdaClosingNewline` is enabled.
[#​2553](https://github.com/fsprojects/fantomas/issues/2553)
- Long `&` (AND) patterns now break across multiple lines to respect max
line length.
[#​1780](https://github.com/fsprojects/fantomas/issues/1780)
- Empty array with trivia inside now has correct indentation in
Stroustrup style.
[#​3098](https://github.com/fsprojects/fantomas/issues/3098)
- Multiline function type inside parentheses gets extra indentation to
avoid compiler error.
[#​3043](https://github.com/fsprojects/fantomas/issues/3043)
- Trivia around paren lambda with conditional compilation no longer
causes merge error.
[#​2844](https://github.com/fsprojects/fantomas/issues/2844)
- Type app closing angle bracket padded by previous expression length.
[#​3179](https://github.com/fsprojects/fantomas/issues/3179)
- Dynamic operator on result of qualified function call causes
formatting error.
[#​3135](https://github.com/fsprojects/fantomas/issues/3135)
- Long delegate type with generic args no longer breaks around arrow.
[#​2468](https://github.com/fsprojects/fantomas/issues/2468)
- Idempotency problem when formatting NUnit Assert.That with lambda
argument. [#​1740](https://github.com/fsprojects/fantomas/issues/1740)
- Comment between lines of no-break infix expression no longer loses
indentation.
[#​2944](https://github.com/fsprojects/fantomas/issues/2944)
- Hash directives around access modifier in module declaration no longer
lose indentation.
[#​3188](https://github.com/fsprojects/fantomas/issues/3188)

Special thanks to @, @​Copilot, @​dsyme and @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-003](https://www.nuget.org/packages/fantomas/8.0.0-alpha-003)
    

## 8.0.0-alpha-002

# 8.0.0-alpha-002

### Changed
- Breaking: change default of MultilineBracketStyle from Cramped to
Aligned. [#​3200](https://github.com/fsprojects/fantomas/issues/3200)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-002](https://www.nuget.org/packages/fantomas/8.0.0-alpha-002)
    

## 8.0.0-alpha-001

# 8.0.0-alpha-001

### Changed
- Update FCS to 'Remove LetOrUseKeyword from SynExprLetOrUseTrivia',
commit 43932b4c7984d6562e91e5f1484868cd4f5befcf
[#​3167](https://github.com/fsprojects/fantomas/pull/3167)

Special thanks to @​Martin521, @​Numpsy, @​Thorium, @​dawedawe,
@​karanveersp, @​mattgallagher92, @​nojaf and @​tboby!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-001](https://www.nuget.org/packages/fantomas/8.0.0-alpha-001)
    

Commits viewable in [compare
view](https://github.com/fsprojects/fantomas/commits/v8.0.0).
</details>

Updated [FSharp.Compiler.Service](https://github.com/dotnet/dotnet) from
43.12.400 to 43.12.401.

<de…
tonycknight added a commit to tonycknight/discorss that referenced this pull request Sep 21, 2026
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Updated [fantomas](https://github.com/fsprojects/fantomas) from 7.0.6 to
8.0.0.

<details>
<summary>Release notes</summary>

_Sourced from [fantomas's
releases](https://github.com/fsprojects/fantomas/releases)._

## 8.0.0

# 8.0.0 <sub>Satanized</sub> - 15/09/2026

### Changed
- Stable release.

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0](https://www.nuget.org/packages/fantomas/8.0.0)
    

## 8.0.0-beta-003

# 8.0.0-beta-003

### Changed
- Updated the packages the command line tool is built against:
`editorconfig` to `0.18.0`, `Serilog` to `4.4.0`, `Spectre.Console` to
`0.57.2` and `System.IO.Abstractions` to `22.2.0`. Only one of these is
visible in what Fantomas does: `editorconfig` `0.18.0` scopes its cache
of the `.editorconfig` files it has read to the parser that reads them,
where it used to share one statically. Fantomas keeps a single parser
for the process, so a file is still read once and no more.
`Fantomas.Core` asks for the same `FSharp.Core` it always did, so
nothing referencing it has to move.
[#​3468](https://github.com/fsprojects/fantomas/pull/3468)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-beta-003](https://www.nuget.org/packages/fantomas/8.0.0-beta-003)
    

## 8.0.0-beta-002

# 8.0.0-beta-002

### Fixed
- Unexpected symbol '<' in expression - Fantomas 8 regression. When the
left-hand side of `=`, `>`, `<`, `%` or `%%` spans several lines, the
operator takes a line of its own one level in, as `8.0.0-beta-001`
introduced. That level was measured from the enclosing indent rather
than from the column the left-hand side starts at. The two are the same
at the start of a binding, and differ after something like `&& (`, where
the operator then landed on the column of the left-hand side and was
offside: the parser read `<` as the start of a type application and
rejected the output. The layout now asks where its fresh line would land
and adds whole indent levels until that clears the left-hand side, the
way a chain's dots have cleared its head since `8.0.0-alpha-024`, so the
operator and the right-hand side sit one level to the right of it and
every column stays a multiple of the indent size. That shared step is
`indentPast` in `Context.fs`, the helper
[#​3446](https://github.com/fsprojects/fantomas/issues/3446) asked for
once a second caller turned up.
[#​3463](https://github.com/fsprojects/fantomas/issues/3463)

Special thanks to @​nojaf and @​shayanhabibi!


[https://www.nuget.org/packages/fantomas/8.0.0-beta-002](https://www.nuget.org/packages/fantomas/8.0.0-beta-002)
    

## 8.0.0-beta-001

# 8.0.0-beta-001

### Changed
- `=`, `>`, `<`, `%` and `%%` cannot start a line at the column of their
left-hand side: there the parser reads `=`, `>` and `<` as the `=` of a
binding, and reads `%` and `%%` inside a quotation as a splice. Fantomas
already kept them off that column, but did so with the layout that keeps
the right-hand side beside the operator whenever it is short enough to
fit, which needs the page width and a column count to predict and moves
a `0` around when an unrelated part of the line changes length. It now
follows the rule proposed in
[fsharp/fslang-design#​836](https://github.com/fsharp/fslang-design/issues/836):
the expression fits on one line, or it does not and the operator stays
with the left-hand side while the right-hand side moves one level in, or
the left-hand side itself spans several lines, in which case the
operator and the right-hand side each take a line of their own.
`fsharp_multiline_bracket_style = stroustrup` still overrides this for a
right-hand side that opens a bracket, which keeps hugging the operator.
This is the first beta of `8.0.0`: the style guide for this is not
settled yet, so give it a try and report back on [the design
issue](https://github.com/fsharp/fslang-design/issues/836) if something
reads worse than what it replaces.
[#​3434](https://github.com/fsprojects/fantomas/pull/3434)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-beta-001](https://www.nuget.org/packages/fantomas/8.0.0-beta-001)
    

## 8.0.0-alpha-026

# 8.0.0-alpha-026

### Changed
- `fsharp_multiline_bracket_style = stroustrup` now applies to object
expressions, so `{` stays on the line that opens the binding and `new T
with` moves below it. Up to now object expressions were printed by the
`aligned` branch whatever the setting said, which left `{ new T with` on
one line and made stroustrup the only bracket style that did not reach
every bracket it names. The [Microsoft style
guide](https://learn.microsoft.com/en-us/dotnet/fsharp/style-guide/formatting#formatting-object-expressions)
writes the form out under "Formatting object expressions" and Fantomas
now produces it character for character. This was left alone in the
belief that the layout was an offside error waiting to happen; it is
not, and the compiler accepts it in a binding, a list item, a function
argument, a lambda body, a match clause and a nested `let` alike. An
object expression under stroustrup now lands where a record already
landed in each of those positions. `aligned`, which is the default since
`8.0.0-alpha-002`, and `cramped` are untouched.
[#​2990](https://github.com/fsprojects/fantomas/issues/2990)
- `fsharp_multiline_bracket_style = stroustrup` now survives a binding
whose signature broke across lines, and a match clause under
`fsharp_experimental_keep_indent_in_branch`. Both were printed by
branches written before stroustrup existed and never given a
stroustrup-aware counterpart, so the setting was quietly overruled in
each: a `let` whose parameters wrapped left its `=` alone on a line and
indented the bracket below it, and a match clause under keep-indent did
the same below the arrow. They now read `= {` and `| _ -> {`, with the
items one level in and the closing bracket back at the column the
binding or the clause started from, which is what the same code already
produced when the signature fitted on one line or the setting was off.
This reaches every bracket the setting names, so records, update
records, anonymous records, anonymous structs, lists and arrays move
with the object expressions of the entry above rather than behind them.
The closing bracket of a match clause stays two columns right of the
bar, because on the bar's own column the `|` of the next clause no
longer parses.
[#​3450](https://github.com/fsprojects/fantomas/pull/3450)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-026](https://www.nuget.org/packages/fantomas/8.0.0-alpha-026)
    

## 8.0.0-alpha-025

# 8.0.0-alpha-025

### Fixed
- A `!` line of a `.fantomasignore` takes a path back out again even
when a line above it matched the folder holding that path, so `sub/*`
followed by `!sub/keep` formats and checks the files under `sub/keep`.
Since `8.0.0-alpha-016` a folder an ignore file names is never opened,
which is what a folder pattern should mean and is not what it can mean
here: closing `sub` decides that `sub/keep` is not there, and the line
that would have taken it back out is never reached. The files were not
reported as ignored, they were never found, so `--check` passed over a
scope smaller than the ignore file describes and `doctor`, which asks
about one file and answers correctly, disagreed with the run about that
same file. An ignore file that negates anything now leaves every folder
open and asks about the files inside one at a time, which is what every
version up to `8.0.0-alpha-015` did. An ignore file with no `!` line in
it still closes the folders it names.
[#​3447](https://github.com/fsprojects/fantomas/issues/3447)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-025](https://www.nuget.org/packages/fantomas/8.0.0-alpha-025)
    

## 8.0.0-alpha-024

# 8.0.0-alpha-024

### Fixed
- The dots of a chain that breaks are indented past the head of the
chain, instead of landing on the head's own column or to the left of it.
A dot on the head's column reads as a new item rather than as the chain
continuing, and inside a parenthesis the parser refuses the output
outright: `let v = ((someObject.First(a).Second(b).Third(c)))` at
two-space indentation produced code Fantomas would not accept from
itself, so the file was left unformatted with a bug report on the
console. The dots now step one indent level at a time until they clear
the head, which keeps every column a multiple of the indent size; a
chain whose dots already cleared its head is untouched. This replaces a
branch that anchored a parenthesised chain on its opening parenthesis,
written when only a real dot chain reached it. Since `8.0.0-alpha-014` a
dotted long identifier is a chain too, so it also fired for the likes of
`Seq.map`, which never breaks, and moved a lambda written behind such a
call three columns for no reason.
[#​3445](https://github.com/fsprojects/fantomas/pull/3445)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-024](https://www.nuget.org/packages/fantomas/8.0.0-alpha-024)
    

## 8.0.0-alpha-023

# 8.0.0-alpha-023

### Fixed
- An operator that starts with a star keeps a space on either side of it
inside the parentheses wherever its name is written, so `val inline ( *.
): ...` and `abstract member ( *. ): ...` survive formatting. Only `let
( *. ) a b = ...` was spaced before; a `val` in a signature file and an
abstract member in either kind of file came out as `(*.)`, which opens a
block comment and swallows the rest of the file. Multiplication is
unaffected, because `(*)` is a token the lexer knows.
[#​3443](https://github.com/fsprojects/fantomas/pull/3443)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-023](https://www.nuget.org/packages/fantomas/8.0.0-alpha-023)
    

## 8.0.0-alpha-022

# 8.0.0-alpha-022

### Added
- Extension members on tuple types, `type (int * int) with ...` and
`type struct (int * int) with ...`, format instead of failing. The
parser accepts them since
[dotnet/fsharp#​19602](https://github.com/dotnet/fsharp/pull/19602), and
a type definition's name is now any type in the Oak tree rather than
only an identifier. The tuple prints as written; no layout decision was
added for it. [#​3436](https://github.com/fsprojects/fantomas/pull/3436)

### Changed
- Update FCS to 'Rotate [<return: X>] attributes during binding
normalization', commit 74ec4f7df70717a162d6ffd23007603cf298fb8b
[#​3436](https://github.com/fsprojects/fantomas/pull/3436)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-022](https://www.nuget.org/packages/fantomas/8.0.0-alpha-022)
    

## 8.0.0-alpha-021

# 8.0.0-alpha-021

### Fixed
- A call in the middle of a chain keeps its parenthesis against the
member name when its lambda argument no longer fits, and breaks behind
it instead. Up to now the whole argument moved down a line, `(` and all,
which is what the last call of a chain does, but for an earlier one it
changes what the code means: `a.Foo (x).Bar()` passes `(x).Bar()` to
`Foo` rather than calling `Bar` on the result. With a call behind it the
compiler rejected the result with "This argument expression needs
parentheses"; with a plain member behind it, such as `.Value`, nothing
warned at all and the member quietly became part of the argument.
Hanging the parameters under `(fun` would keep the parenthesis in place
too, and the F# style guide rules that out, because the column they hang
from is the length of the member name. The closing `)` answers to
`fsharp_multi_line_lambda_closing_newline` as it did before, and the
last call of a chain is untouched, since nothing follows it to be
swallowed. [#​3432](https://github.com/fsprojects/fantomas/issues/3432)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-021](https://www.nuget.org/packages/fantomas/8.0.0-alpha-021)
    

## 8.0.0-alpha-020

# 8.0.0-alpha-020

### Fixed
- A list or an array on the right of `=`, `>`, `<`, `%` or `%%` is
indented from the binding again, so `let a = b = [ ... ]` breaks to `b =
[` with its items one level in and the `]` back at the column of `b`.
Since `8.0.0-alpha-003` both the items and the closing bracket sat a
further level to the right. The right-hand side of these operators is
indented so that a comment between the operator and that side lands
below the operator, which is what
[#​2944](https://github.com/fsprojects/fantomas/issues/2944) asked for,
and so that the lines under a chain or an application are not read as a
continuation of the left-hand side. A list or an array needs neither:
its bracket says where the right-hand side begins and indents its items
from the left-hand side by itself, so indenting it again pushed the
items and the closing bracket a level too far. This concerns `aligned`,
which is the default, and `stroustrup`. `cramped` lines the items up
under the opening bracket, so the indent never reached them and nothing
changes there. Right-hand sides that open a bracket in some other way,
such as `seq { }`, `<@ @>`, `begin ... end` and `new T(...)`, still
carry the extra level; where a no-break operator should put what follows
it is a wider question than this fix.
[#​3428](https://github.com/fsprojects/fantomas/issues/3428)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-020](https://www.nuget.org/packages/fantomas/8.0.0-alpha-020)
    

## 8.0.0-alpha-019

# 8.0.0-alpha-019

### Fixed
- A call on a constant receiver keeps its parenthesis tight, so
`"yow".Substring (0, 3)` and `3L.ToString ()` come out as
`"yow".Substring(0, 3)` and `3L.ToString()`. `8.0.0-alpha-018` gave a
constant receiver a space it was never meant to have. The rule there
says a call keeps the space only when the whole thing being called is a
plain dotted name, and the comment agreed at
[fslang-design#​648](https://github.com/fsharp/fslang-design/issues/648)
lists `"yow".Substring(0, 3)` under "a receiver that is not a name",
beside `(f x)` and `[ 1; 2 ]`. The alpha carved constants back out on
the reasoning that a constant is atomic rather than bracketed, which
went past what was agreed and was not deliberate. A constant is a value,
the rule asks for a name, so it goes tight. A type parameter really is a
name, so `'T.set_StaticProperty (3)` is unaffected.
[#​3426](https://github.com/fsprojects/fantomas/pull/3426)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-019](https://www.nuget.org/packages/fantomas/8.0.0-alpha-019)
    

## 8.0.0-alpha-018

# 8.0.0-alpha-018

### Added
- `doctor` names the `.fantomasignore` above the one that governs a
file, when a pattern in it would have skipped that file. Fantomas reads
the nearest ignore file at or above a file and no other, so a pattern
written at the root of a repository has no effect on a folder that has
an ignore file of its own beside it. Up to now the report named the
ignore file that did decide and said nothing about the one that did not,
which answers "which file" and leaves "then why did mine not" for the
reader to work out. It quotes the pattern that would have matched, with
its line number, the way it already quotes the line that decided.
`--json` carries every ignore file above the governing one under
`shadowed`, each with `wouldIgnore` and the lines of it that match,
whether or not it would have decided anything.
[#​3423](https://github.com/fsprojects/fantomas/pull/3423)

### Changed
- The settings that ask for a space before the parenthesis of a call,
`fsharp_space_before_uppercase_invocation` and
`fsharp_space_before_lowercase_invocation`, now get a say only when the
whole thing being called is a plain dotted name. Put a call, an index, a
bracketed receiver, or a type application anywhere in it, and the
parenthesis stays tight whatever the settings ask for. On default
settings `xs.map(fun a -> a + 1).filter (fun a -> a > 1)` becomes
`xs.map(fun a -> a + 1).filter(fun a -> a > 1)`, which is the report
this started from, and `Foo().bar ()` and `myList.[7].someFunction
(arg)` lose their space for the same reason. A plain dotted name is
untouched however long it gets, so `List.map (f)` and
`Fantomas.FCS.Text.Range.unionRanges (r1, r2)` keep the space they had,
and so is a literal or type parameter receiver, since `"yow".Substring
(0, 3)` and `'T.set_StaticProperty (3)` are atomic rather than
bracketed. Agreed at
[fslang-design#​648](https://github.com/fsharp/fslang-design/issues/648)
and written up under [Formatting chain
expressions](https://fsprojects.github.io/fantomas/docs/contributors/Chains.html).
[#​3425](https://github.com/fsprojects/fantomas/pull/3425)
- A type application now makes a call tight with no chain in sight,
which is the part of the change above most likely to catch you out,
because it reaches ordinary generic calls rather than fluent code. On
default settings `unbox<bool> (value)`, `f<int> (x)`, `List.map<int>
(f)` and `jsOptions<Vis.Options> (fun o -> ...)` all lose their space,
since `fsharp_space_before_lowercase_invocation` is `true` and each of
those is a lower-case name carrying type arguments. An upper-case
generic call such as `Dictionary<string, int>(x)` was already tight by
default and only moves if `fsharp_space_before_uppercase_invocation` is
on. Fantomas does not add parentheses, so the far more common form
without them, `unbox<int> obj`, is left exactly as written and never
comes into it.
[#​3425](https://github.com/fsprojects/fantomas/pull/3425)

### Fixed
- More expression forms count as open-ended, so a record, list, array or
tuple that holds one of them in a non-last position stays multiline
instead of collapsing onto a line that means something else.
[#​3279](https://github.com/fsprojects/fantomas/issues/3279) settled
`fun`, `if`, `match` and `try`, and left the forms that wrap one of
those or end in one: `lazy`, `yield`, `return`, `do`, `assert` and
`fixed`, an assignment such as `x.P <- fun y -> y`, and `let x = 1 in
body`. `{ A = 1; B = lazy fun x -> x; C = 3 }` reads back as a lambda
that swallowed `C = 3`. A wrapper around something that closes on its
own, `lazy a` or `x.P <- 1`, collapses as it always did. When this
happens, why, and how to get the single line back is written up under
[Open-ended
expressions](https://fsprojects.github.io/fantomas/docs/end-users/OpenEndedExpressions.html).
[#​3424](https://github.com/fsprojects/fantomas/pull/3424)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-018](https://www.nuget.org/packages/fantomas/8.0.0-alpha-018)
    

## 8.0.0-alpha-017

# 8.0.0-alpha-017

### Added
- `fantomas doctor <file>` walks one file through everything Fantomas
does to it and reports what happened at each step: whether the path is a
file Fantomas formats, which `.fantomasignore` governs it and which line
of that file decided, which settings apply and which `.editorconfig` set
each one, what formatting produced and where the result first parts from
the file, whether Fantomas accepts its own output, and whether
formatting that output again leaves it alone. It writes nothing, so it
is safe against a working tree you have not committed. Every question it
answers was answerable before only by reading two configuration formats
by hand and knowing the order Fantomas resolves them in. It takes one
file rather than a folder, because the answers differ per file, and it
exits 0 for a file it could diagnose whatever it found, 1 for a path it
cannot look at or a step that failed. `--json` writes the same walk as
one document with a key per step.
[#​3421](https://github.com/fsprojects/fantomas/pull/3421)
- The `.fantomasignore` line that matched a path is now knowable rather
than only the yes or no. `doctor` quotes it with its line number, the
way `git check-ignore -v` does, and names every line that matches when
more than one does, since a `!` pattern further down is exactly the case
nobody spots by eye.
[#​3421](https://github.com/fsprojects/fantomas/pull/3421)
- `doctor` decides whether a file would be rewritten the way a format
run decides it, by comparing the text as it is. A file whose line
endings are the only thing out of step is therefore reported as needing
formatting, where `fantomas check` deliberately overlooks that
difference and calls the same file already formatted. What it reports is
where the file and the result first part, rather than a count of the
lines that differ by position: that is not a count of edits, because
splitting one long line into five moves every line below it.
[#​3421](https://github.com/fsprojects/fantomas/pull/3421)
- A file under a folder a compiler or a package manager wrote, such as
`obj`, is named as such by `doctor`. A run over the tree above it never
opens that folder, whatever the ignore file says, and sending its owner
to read their `.fantomasignore` sends them to read the wrong file.
[#​3421](https://github.com/fsprojects/fantomas/pull/3421)

### Changed
- Breaking: output that Fantomas will not accept from itself is reported
as what it is, and shows you the part of that output it would not
accept. `Fantomas produced code that is not valid F#.` read as though
the file were at fault, and reaching it always means the opposite: the
input parsed, or a parse failure would have been reported instead. The
report now says that nothing was written and the file is untouched, that
this is a bug in Fantomas rather than a problem with the code, and where
to report it, in the shape a parse failure and an unmodellable construct
already print.
[#​3419](https://github.com/fsprojects/fantomas/pull/3419)
- Breaking: that report carries what the parser said about the rejected
output, and the lines of the output around it with a caret under the
failure, so there is a small reproduction to cut from it. Up to now the
run said only that something was invalid, and finding out what meant
running again with `--force` and reading the result. The diagnostics
carry no line and column of their own: the output they would count into
is written nowhere, so a position in it is a coordinate you cannot
follow, and a path an editor could open would take you to the wrong line
of the right file. The carets say where, and the report says out loud
that the lines below it are the output rather than your file. `--json`
carries the same diagnostics in the `diagnostics` array a parse failure
already uses. `fantomas check` reports all of it the same way, where it
used to run the whole explanation on after `could not be checked:`.
[#​3419](https://github.com/fsprojects/fantomas/pull/3419)
- The two reports that ask you to file a bug, for output Fantomas would
not accept and for a construct it cannot model, name one place to send
it rather than two. They used to add the issue tracker as an alternative
for a file too large for the online tool to carry, which offered a
choice at the point somebody least wants one.
[#​3419](https://github.com/fsprojects/fantomas/pull/3419)
- Breaking: `Fantomas.Core.CodeFormatter.IsValidFSharpCodeAsync` became
`ValidateFSharpCodeAsync` and answers with a `ValidationResult` rather
than a `bool`. Read `.IsValid` off it where the verdict was all you
wanted; `.Diagnostics` is what Fantomas refused, positioned, and is
empty exactly when the source is valid. The boolean discarded it, so
anything that had to say why had to parse the source a second time to
find out, which is why the tool could not show you its own bad output. A
warning Fantomas tolerates, such as
[#​3396](https://github.com/fsprojects/fantomas/issues/3396) on IWSAM
types, is not among them.
[#​3419](https://github.com/fsprojects/fantomas/pull/3419)

### Fixed
- A `[<return: ...>]` attribute written in front of an `extern`
declaration was dropped from the output. The parser moves such an
attribute out of the binding's attribute list and into its arity
information, and the `extern` path never looked there, so the line was
silently deleted. Bindings already put those attributes back; `extern`
now does the same. An attribute written on the return type itself,
`extern [<MarshalAs(UnmanagedType.I1)>] bool f(int options)`, is
reported in both places and stays where it was written.
[#​3420](https://github.com/fsprojects/fantomas/issues/3420)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-017](https://www.nuget.org/packages/fantomas/8.0.0-alpha-017)
    

## 8.0.0-alpha-016

# 8.0.0-alpha-016

### Added
- `fantomas check <paths>` and `fantomas daemon`, beside the `--check`
and `--daemon` flags that named them. Both flags keep working and are
not deprecated, so no pipeline and no editor integration has anything to
change; each means exactly what its command means. The commands exist
because a command can have a `--help` page of its own and a flag cannot:
`fantomas check --help` lists the flags checking has any use for and
leaves out the ones it would refuse. When standard error is a terminal,
the older spelling prints a one line note saying how it is spelled now;
a redirected stream never sees it, so it stays out of build logs and out
of the daemon an editor starts.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- `fantomas profile <paths>`, replacing the `--profile` flag. It formats
one file at a time so the timings can be compared, writes nothing,
reports every file slowest first with its line count and the number of
define combinations it had to format, and ends with a total. `--profile`
measured files formatted in parallel, so each stopwatch recorded wall
clock under contention: a six line signature file and a file of several
thousand lines came back with the same figure.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- A `--help` page for each command. `fantomas --help` is the overview,
with the commands, the flags, worked examples and links; `fantomas check
--help`, `fantomas profile --help` and `fantomas daemon --help` are
about one command each. Which flags a command's page lists is asked of
the same rule that refuses them at run time, so the page cannot come to
disagree with the tool. Every page names the tool the way this run was
started, so a local tool install is shown `dotnet fantomas` rather than
a command it does not have.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- `--flag=value` alongside `--flag value`, so `--out=build` and
`--verbosity=d` work. Only the space separated form was accepted before,
and `--out=build` was read as an input path that does not exist.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- `--` ends the flags, so a path beginning with a dash can be named
after it. It used to be read as an input path itself, which meant no
such path could be given at all.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- `--json` for `fantomas profile`, carrying the same files as the text
report with each file's line count, define combinations and
milliseconds, and the run's own total as `elapsedMilliseconds`. Ordered
by path rather than slowest first, since a reader that wants them by
time can sort them.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Running with no path at all formats the folder you are in, so
`fantomas` is `fantomas .`, and `check` and `profile` read a bare
invocation the same way. Formatting and checking used to refuse with `No
input path provided.` This is what `ruff format` and `dotnet format` do.
Note that it walks every F# file below the working directory, and that a
`.fantomasignore` in a repository below it governs only that
repository's files.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)

### Changed
- Breaking: `Fantomas.Core.ParseException` now derives from
`FormatException`, as the other exceptions the library raises already
did, so `:? FormatException` catches every way formatting can fail. It
could not before: it was declared with F#'s `exception` keyword, which
cannot name a base class. Its `Message` names the first error by
position instead of dumping every diagnostic record through `%A`, and
the records are reachable as `.Diagnostics` rather than only by matching
the exception pattern. Code matching `| ParseException diagnostics ->`
becomes `| :? ParseException as e -> e.Diagnostics`; raising and
constructing it are unchanged.
[#​3415](https://github.com/fsprojects/fantomas/pull/3415)
- A construct Fantomas cannot model is now reported through the same
mechanism as a parse failure, instead of as a `%A` dump of a syntax tree
node in the middle of the message. The report says in words what could
not be modelled, names the union case responsible, positions it in the
file being formatted rather than in the `tmp.fsx` the parser was handed,
and draws a snippet of the source with a caret under the construct, so
the enclosing declaration is visible without having to bisect the file
by hand. The syntax tree node is still reported, at `--verbosity d`,
where it serves whoever triages the issue rather than whoever files it.
This covers every such report, from a type the transformer has no Oak
node for to the chain and leading-keyword invariants behind it.
`--check` and the daemon position them the same way, so an editor shows
its user the snippet rather than a bare line and column.
[#​3415](https://github.com/fsprojects/fantomas/pull/3415)
- Breaking: everything a format or check run prints was rewritten. Each
file gets a sentence of its own opening with a status character, `+
src/A.fs was formatted.`, and the run ends with a line of counts, `2
files formatted, 30 unchanged.` This replaces the bordered table of
headings that a folder run printed, which bypassed the logger and so
carried no timestamp at `--verbosity d` while everything around it did.
A run over a single named file is answered on its own terms and has no
summary added to it. Any script reading this output needs updating;
`--json` exists for a caller that has to act on the result rather than
read it. [#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: `.fantomasignore` is resolved per file, the nearest one at
or above it, which is what the daemon has always done. The command line
used to resolve one file for the whole run from the directory it started
in, so an ignore file in a subfolder was honoured by an editor and
invisible to a pipeline: the same file was skipped in one and formatted
in the other. A nested ignore file that used to have no effect on a
command line run now has one.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: a folder that `.fantomasignore` names is no longer opened.
Up to now every file inside it was found and then rejected one at a
time; the folder is now passed over, so nothing inside it is read,
counted or reported. A folder pattern spelled with a trailing separator,
`vendor/`, closes the folder as `vendor` always did. Nothing is
formatted that was not formatted before.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: a run that skipped files no longer says how many. The count
could not be honest: an ignore pattern naming a file can be counted, and
one naming a folder cannot, because the folder is never opened and what
is inside it is unknown by design. A number right about the first and
blind to the second reads as though it covered both. A file named on the
command line still gets a line of its own, since a count is the only
other place a path could be accounted for and no count carries this one;
a file found by walking a folder, and the folder itself, are named at
`--verbosity d`.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: `--json` no longer names a file that `.fantomasignore`
matched, and `ignored` is no longer a status a file can carry. Nothing
counts them in its place, for the reason above: `files` is what the run
looked at. [#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: a token beginning with a dash that is not a flag Fantomas
has is reported as an unknown flag, with the nearest flag it could be a
misspelling of. `fantomas --chek src` said `Input path '--chek' not
found.` and now says `'--chek' is not a Fantomas flag. Did you mean
'--check'?` Every unrecognised token used to be taken for an input path,
so a misspelled flag was reported as a file that is not there.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: a malformed command line exits 1 rather than 2. The
documented exit codes have only ever been 0, 99 and 1; 2 came from the
argument parser and was never one Fantomas chose.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: repeating a flag is allowed and the last one wins, where it
used to be refused outright with `argument '--check' has been specified
more than once`. This is the Unix norm and stops a script that builds
its arguments up from failing on a duplicate. `--out` is the exception
and is still refused, because it decides where files are written and
choosing between two of them quietly is choosing where the work lands.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: a file that could not be parsed is reported as `src/A.fs
could not be parsed by Fantomas:` rather than `Fantomas could not parse
src/A.fs:`, and a construct that could not be modelled as `src/A.fs
could not be formatted by Fantomas:`. Every line of a report now begins
with the path it is about, so the column can be read down. A script
matching on the old text needs updating.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: `--check` no longer reports a file that will not parse
twice, once as an error and once as needing formatting. It is an error
and nothing else, which is also what its exit code has always said.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: `--force` announces invalid output as a warning on standard
error rather than on standard out, and says what happened: `src/A.fs was
formatted, but the result is not valid F# code. It was written because
--force was given.` It says Fantomas wrote F# it believes is not valid,
which is the opposite of the ordinary run of things standard out
carries. [#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- `--version` prints the commit hash trimmed to the short form the
`--help` page has always shown, and the whole of it at `--verbosity d`.
`Fantomas.Client` is unaffected: it cuts the version at `+` and drops
the build metadata whatever it holds.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- A failure says what went wrong at any verbosity. The exception's
message used to be kept for `--verbosity d` and nothing printed in its
place at normal, so an unreadable file reported only `A.fs could not be
formatted.` and left `Access to the path is denied` behind a flag nobody
knew to pass. Detailed verbosity now adds the type and the stack trace
below that line rather than instead of it.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Colour is used where the terminal takes it and dropped where it does
not, decided for standard out and standard error separately, so a run
whose output is piped keeps its colour on the diagnostics still going to
the terminal. The status character is `+`, `=`, `-`, `!` and `x` where
the output is redirected or the console is not UTF-8, and `✔`, `=`, `○`,
`!` and `✘` where it is; both carry the same five states, and the words
beside them say the same thing either way. `NO_COLOR` is honoured.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Files are reported in path order rather than in whichever order the
file system returned them, so two machines running one command print the
same thing. [#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- Breaking: a run writing to `--out` reports what was written rather
than what changed, `32 files written to build, 2 reformatted.`, since
under `--out` every input produces an output file whether or not its
content changed. A single file named on the command line says where it
went, `src/A.fs was formatted and written to build/A.fs.`, where it used
to say only that it was formatted or unchanged and leave the destination
unmentioned. [#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- A run that looked at no file at all says so on standard error and
exits 0, which a check used to pass over in silence: `fantomas check` on
an empty folder, or on one an ignore file emptied, printed nothing and
read as a green build. Formatting and checking now report this the same
way, as they do everything else they both have to say.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)

### Fixed
- Getting a 'no Oak node is defined for this type' error when formatting
a p/invoke signature with nullability annotations.
[#​3414](https://github.com/fsprojects/fantomas/issues/3414)
- An `extern` declaration returning an array, such as `extern byte[]
f(int options)`, lost the element type of the array and was written as
``extern `[]` f(int options)``.
[#​3415](https://github.com/fsprojects/fantomas/pull/3415)
- A file whose formatted output is not valid F# named that file twice in
one line, `Failed to format file: A.fs : Formatting A.fs leads to
invalid F# code`, with a spaced colon in the middle.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)
- At `--verbosity d`, a single unchanged file was reported twice in two
different spellings, once as `'A.fs' was unchanged` and once as `A.fs
was unchanged.`
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)

### Removed
- Breaking: the `--profile` flag. `fantomas profile <paths>` replaces
it, and a run that names the flag is refused with `'--profile' is not a
Fantomas flag. 'profile' is a command: try 'fantomas profile <paths>'.`
and exits 1. It was not kept working as an alias, alone among the flags
that became commands: the flag wrote formatted files to disk as a side
effect of measuring them and the command deliberately writes nothing, so
an alias would have gone on being accepted and silently stopped writing,
which is worse than a run that stops and says what to type. `--profile
--check`, which was accepted and did nothing at all, no longer has
anything to combine.
[#​3416](https://github.com/fsprojects/fantomas/pull/3416)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-016](https://www.nuget.org/packages/fantomas/8.0.0-alpha-016)
    

## 8.0.0-alpha-015

# 8.0.0-alpha-015

### Added
- Support for the record spread syntax introduced in F# preview, [RFC
FS-1151](https://github.com/fsharp/fslang-design/pull/805). A spread can
appear in a record expression, `{ ...source; Field = value }`, in an
anonymous record expression, `{| ...source; Field = value |}`, and in
the record representation of a type definition, `type Target = {
...Source; Field: int }`, in both implementation and signature files.
[#​3400](https://github.com/fsprojects/fantomas/pull/3400)
- Interpolated strings with a negative alignment, `$"{value,-10}"`, now
format instead of failing with a parse error. Alignment and format
specifiers keep their existing layout, so `$"{value,10:N2}"` is
unaffected. [#​3400](https://github.com/fsprojects/fantomas/pull/3400)
- `--json` writes one JSON document to standard out describing what the
run did, instead of the usual messages, for a script or an agent that
has to act on the result rather than read it. Every file the run looked
at is named with a status of `formatted`, `unchanged`, `ignored`,
`needs-formatting` or `error`, and a file that failed to parse carries
the severity, code, message and one based position of every diagnostic.
Paths are reported as they were given, relative to the
`workingDirectory` the document carries once. Standard out carries the
document and nothing else, so it can be piped straight into a parser,
while warnings still go to standard error. Exit codes are unchanged and
the document repeats the one the process ends with. Works with
`--check`, and is refused alongside `--daemon`, where standard out
already carries the JSON-RPC protocol.
[#​3412](https://github.com/fsprojects/fantomas/pull/3412)

### Changed
- Breaking: warnings and errors are written to standard error instead of
standard out. Informational output stays on standard out, including
`--version` and the files `--check` reports as needing formatting, so a
caller can tell the tool's output apart from its diagnostics by stream.
Scripts that capture standard out to detect failures need to capture
standard error as well.
[#​3399](https://github.com/fsprojects/fantomas/pull/3399)
- Update FCS to 'Parser: recover on missing when conditions', commit
d05075e098278aedcea3379159504d664628a495
[#​3400](https://github.com/fsprojects/fantomas/pull/3400)
- Breaking: the `--help` page is written by Fantomas instead of by Argu.
It carries the version, worked examples, what an input path may be, and
links to the documentation, the F# Discord and the `llms.txt` files an
LLM can read. Colours are used when the terminal supports them and
dropped when standard out is redirected. `-h` is now accepted alongside
`--help`. An argument error reports the complaint on standard error
followed by a pointer to `--help`, where it used to print Argu's usage
block. [#​3402](https://github.com/fsprojects/fantomas/pull/3402)
- Breaking: a run over a single file reports the path it was given
instead of only the file name, so `fantomas src/A.fs` prints `src/A.fs
was formatted.` where it printed `A.fs was formatted.`. The same applies
to the unchanged, ignored and failure messages. A run over several files
already reported the path, so the two now agree.
[#​3404](https://github.com/fsprojects/fantomas/pull/3404)
- Breaking: a file that cannot be parsed is reported with the position
of every diagnostic the parser produced, one MSBuild style line each,
followed by a snippet of the source with two lines of context either
side and a caret under the offending range. This replaces `Could not
parse the file.` for a format run, the `%A` record dump and stack trace
for a `--check` run, and the same `%A` dump the daemon used to hand
editors. Diagnostics are ordered by position and columns are one based,
matching what the F# compiler prints for the same file.
[#​3405](https://github.com/fsprojects/fantomas/pull/3405)
- Breaking: the messages for an input path Fantomas cannot work with are
now the same whether the run formats or checks. `--check` used to report
`Input path 'x' is unsupported file type` and `Input path 'x' not found`
without a full stop where a format run ended both with one, and a run
with no input path at all said `No input path provided.` when checking
and `Input path is missing.` when formatting. Both now report `Input
path 'x' is an unsupported file type.`, `Input path 'x' not found.` and
`No input path provided. Call with --help for usage information.`
[#​3406](https://github.com/fsprojects/fantomas/pull/3406)
- Breaking: a single file that `.fantomasignore` matches is reported the
same way as an ignored file found while walking a folder. `fantomas
A.fs` on an ignored file printed nothing unless `--verbosity d` was
given, where `fantomas ./folder` containing only that file printed `A.fs
was ignored.` Both now print it. A `--check` run reports the files it
ignored at detailed verbosity whether they were named directly or found
in a folder, where before only a directly named file was reported.
[#​3406](https://github.com/fsprojects/fantomas/pull/3406)
- At detailed verbosity, a run writing to `--out` now notes a file it
found already formatted, as a run formatting in place always did. Both
paths decide what formatting came to in the same place, so they say the
same things about it.
[#​3406](https://github.com/fsprojects/fantomas/pull/3406)
- Breaking: `--daemon` no longer accepts the arguments that mean nothing
to a daemon. `--check`, `--out`, `--force`, `--profile`, `--json` and
input paths were all accepted and then silently ignored, so `fantomas
--daemon ./src` looked like it would format a folder and did not. Any of
them now reports which ones were refused and exits 1 without starting.
`--verbosity` is still accepted, since it sets the level the daemon logs
at, and `--version` wins outright. `Fantomas.Client` launches the daemon
with no other arguments, so no editor integration is affected.
[#​3412](https://github.com/fsprojects/fantomas/pull/3412)
- `--version` now answers whatever else is on the command line, and
before any of it is validated, so it can always be used to find out what
you are running. It used to be refused alongside `--daemon`, and
`fantomas --version -v bogus` exited 1 without printing a version. It is
also written straight to standard out instead of through the logger, so
it no longer picks up the timestamp and level prefix that `--verbosity
d` adds, and reads the same at any verbosity.
[#​3412](https://github.com/fsprojects/fantomas/pull/3412)
- Breaking: `Fantomas.Core` is no longer binary compatible with `v7`.
Several discriminated unions are structs now, which changes nothing
about how they are written or matched, but an assembly compiled against
`v7` has to be rebuilt.
[#​3407](https://github.com/fsprojects/fantomas/pull/3407)
- A setting in `.editorconfig` that carries the `fsharp_` prefix but is
not a Fantomas setting now warns instead of being silently ignored, and
where the intent is obvious the warning names the spelling that works.
This catches a misspelling such as `fsharp_multiline_brackets_style`,
and catches prefixing one of the four settings editorconfig itself
defines, where `fsharp_max_line_length` never applied and
`max_line_length` is the one that does. Settings without the `fsharp_`
prefix belong to other tools and are left alone. Every problem in one
`.editorconfig` is reported together and only once per run, rather than
once per formatted file, and `--verbosity d` writes out every setting
the running version supports.
[#​3401](https://github.com/fsprojects/fantomas/pull/3401)
- `.editorconfig` keys and values are both matched without regard to
case, as the editorconfig specification defines them.
`FSHARP_MAX_RECORD_WIDTH` and `fsharp_experimental_elmish = True` used
to be ignored.
[#​3401](https://github.com/fsprojects/fantomas/pull/3401)
- The daemon sends a `fantomas/configurationWarning` notification for
every format request, naming the settings in the resolved configuration
it could not act on, and sending an empty list when there are none so an
editor can clear what it showed earlier. Additive to the JSON-RPC
contract: a client that does not handle the method ignores it and keeps
working. See the [Fantomas.Client
changelog](https://github.com/fsprojects/fantomas/blob/main/src/Fantomas.Client/CHANGELOG.md)
for the client-side API.
[#​3401](https://github.com/fsprojects/fantomas/pull/3401)

### Fixed
- `--verbosity` with an unrecognised value exited with code 1 and
printed nothing, because the message was logged before the logger was
configured. It now reports `Invalid verbosity level` on standard error.
[#​3399](https://github.com/fsprojects/fantomas/pull/3399)
- `--out <folder>` flattened the input tree: every file landed directly
in the output folder, so two files with the same name in different
subfolders silently overwrote each other. The output folder now mirrors
the structure of the input folder, as the documentation already
promised. [#​3403](https://github.com/fsprojects/fantomas/pull/3403)
- `--out <file>` failed with `Failed to format file` and exit code 1
when the folder of the given path did not exist. Fantomas now creates
it, along with the subfolders that mirroring an input folder needs.
[#​3403](https://github.com/fsprojects/fantomas/pull/3403)
- `--out` naming the input folder under a different spelling, such as
`fantomas src --out ./src`, emptied every file it was given. The output
file was opened, and therefore truncated, before the input was read.
Nothing is written now until formatting has produced the text to write,
which also leaves the previous output in place when formatting fails.
[#​3403](https://github.com/fsprojects/fantomas/pull/3403)
- `--out` pointing inside the input folder, such as `fantomas src --out
src/formatted`, formatted the previous run's output again and nested it
one folder deeper on every run. The output folder is left out of the
input scan. [#​3403](https://github.com/fsprojects/fantomas/pull/3403)
- `fantomas src/ --out src` formatted nothing and reported an empty
table, where `fantomas src --out src` formatted the folder in place. A
trailing separator made the two paths compare as different places, so
every file below the input folder was taken for a previous run's output
and skipped. The same applied to `fantomas src --out src/`.
[#​3406](https://github.com/fsprojects/fantomas/pull/3406)
- A file whose extension is not lowercase, such as `A.FS`, was refused
as an unsupported file type, and one found while walking a folder was
skipped. The extension is now compared without case, which is what a
volume that ignores case, as macOS and Windows usually do, means by the
same file. [#​3406](https://github.com/fsprojects/fantomas/pull/3406)
- Given several input paths, a folder whose name contains a dot, such as
`fantomas my.stuff src`, was taken for a file and reported as `Failed to
format file`. A file with no extension was taken for a folder. Which one
a path is, is now asked of the file system rather than guessed from the
name. A single input path was already classified this way.
[#​3406](https://github.com/fsprojects/fantomas/pull/3406)
- A `.fantomasignore` pattern that cannot be matched against a path
reported the raw exception, with its stack trace, through `%A`. It now
names the file and the ignore file that could not be told apart, and
keeps the exception for detailed verbosity.
[#​3406](https://github.com/fsprojects/fantomas/pull/3406)
- `--profile` reported `Line count: 0` for a file whose line endings are
not the ones the platform uses, because it counted occurrences of the
platform's newline rather than line breaks. A file saved with line feeds
counted nothing on Windows, and one saved with carriage returns counted
nothing elsewhere.
[#​3406](https://github.com/fsprojects/fantomas/pull/3406)
- The daemon held its JSON-RPC message loop while reading an
`.editorconfig`, so a request that arrived during it was not read until
that finished. It hands the loop back first now, and serves one request
at a time per file so that the configuration warnings for a file still
arrive in the order the requests did.
[#​3401](https://github.com/fsprojects/fantomas/pull/3401)
- A value that meant something to one setting decided the outcome for
every other setting, because each value was tried against every parser
in turn. `fsharp_max_record_width = cr` failed the whole run with
`Carriage returns are not valid for F# code`; it is now reported as a
value that setting does not accept. A value is only read as the type its
own setting has.
[#​3401](https://github.com/fsprojects/fantomas/pull/3401)
- A misspelling of one of the four settings editorconfig itself defines
was silently ignored, where a misspelling of a `fsharp_` setting was
reported. `max_line_lenght = 100` now says so. Only names within two
edits of a setting Fantomas has are read this way, so settings belonging
to other tools, `indent_style` among them, stay silent.
[#​3401](https://github.com/fsprojects/fantomas/pull/3401)
- A negative number was accepted for any setting that takes one, so
`fsharp_max_record_width = -5` formatted to nonsense widths without
saying anything. It is now reported like any other value the setting
does not accept.
[#​3401](https://github.com/fsprojects/fantomas/pull/3401)

Special thanks to @​claude and @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-015](https://www.nuget.org/packages/fantomas/8.0.0-alpha-015)
    

## 8.0.0-alpha-014

# 8.0.0-alpha-014

### Changed
- Bump `StreamJsonRpc` to `2.25.29`. This clears the NuGet vulnerability
warnings coming from the transitive `MessagePack` and
`Nerdbank.MessagePack` dependencies.
[#​3393](https://github.com/fsprojects/fantomas/pull/3393)
- Breaking: `Expr.Chain` is redesigned around a head, a list of
`ChainSegment` and a `ChainTerminal`. `Expr.DotLambda`,
`Expr.DotIndexedGet`, `Expr.AppLongIdentAndSingleParenArg` and
`Expr.NestedIndexWithoutDot` are removed, and a dotted long identifier
now yields `Expr.Chain` instead of `Expr.OptVar` in expression position.
See the [upgrade
guide](https://fsprojects.github.io/fantomas/docs/end-users/UpgradeGuide.html)
for how to migrate.
[#​3385](https://github.com/fsprojects/fantomas/pull/3385)
- Chains are laid out by a documented set of rules, written up in full
under
[Chains](https://fsprojects.github.io/fantomas/docs/contributors/Chains.html).
They are a proposal for the F# style guide and may still change. A chain
that fits on one line is left alone. When one has to break, only a call
claims a line of its own and plain property access rides along in front
of it, where a comment between the steps previously put every step on
its own line. A long run of property access now wraps into balanced
lines instead of overflowing the margin.
[#​3385](https://github.com/fsprojects/fantomas/pull/3385)
- With `fsharp_multi_line_lambda_closing_newline = false` (the default),
a match lambda argument keeps `function` beside the `(` when the call is
reached through a dot, matching what a call without a receiver already
did. [#​3385](https://github.com/fsprojects/fantomas/pull/3385)
- With `fsharp_multi_line_lambda_closing_newline = true`, a lambda
argument whose opening line does not fit moves onto its own line instead
of hanging its parameters under the opening parenthesis. This applies to
calls with and without a receiver.
[#​3385](https://github.com/fsprojects/fantomas/pull/3385)
- Added `InvariantViolationException`, raised when Fantomas reaches a
state its own model says is impossible. It derives from
`FormatException`, carries the source range of the construct involved,
and points at the issue tracker.
[#​3385](https://github.com/fsprojects/fantomas/pull/3385)

### Fixed
- CLI refuses to format files containing IWSAM types (warning 3535 fails
output validation).
[#​3396](https://github.com/fsprojects/fantomas/issues/3396)
- A space was added before the parenthesis of a call inside a `_.`
shorthand lambda when `fsharp_space_before_uppercase_invocation` was
enabled, producing code that does not compile.
[#​3364](https://github.com/fsprojects/fantomas/issues/3364)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-014](https://www.nuget.org/packages/fantomas/8.0.0-alpha-014)
    

## 8.0.0-alpha-013

# 8.0.0-alpha-013

### Changed
- Improved formatting performance. Formatting is roughly 1.5 to 2 times
faster and allocates around 65% less memory, depending on hardware.
[#​3388](https://github.com/fsprojects/fantomas/pull/3388)
- The command line tool now runs with Server GC, which speeds up
formatting multiple files in parallel at the cost of a larger memory
footprint. [#​3388](https://github.com/fsprojects/fantomas/pull/3388)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-013](https://www.nuget.org/packages/fantomas/8.0.0-alpha-013)
    

## 8.0.0-alpha-012

# 8.0.0-alpha-012

### Changed
- Bump `FSharp.Core` to `10.0.100`.
[#​3353](https://github.com/fsprojects/fantomas/pull/3353)

### Fixed
- Chained `?` operator accesses (e.g. `x?a("")?b(t)`) no longer add a
space before parenthesised arguments, which previously changed how the
next `?member` was parsed. Detected during AST→Oak transformation and
represented as a new `Expr.DynamicChain` node so the printer can keep
the chain tight; lone `?` calls still respect
`SpaceBefore(Upper|Lower)caseInvocation`.
[#​3159](https://github.com/fsprojects/fantomas/issues/3159)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-012](https://www.nuget.org/packages/fantomas/8.0.0-alpha-012)
    

## 8.0.0-alpha-011

# 8.0.0-alpha-011

### Fixed
- Unicode strings with combining characters caused incorrect column
tracking, leading to wrong indentation decisions.
[#​2945](https://github.com/fsprojects/fantomas/issues/2945)

Special thanks to @​Copilot and @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-011](https://www.nuget.org/packages/fantomas/8.0.0-alpha-011)
    

## 8.0.0-alpha-010

# 8.0.0-alpha-010

### Fixed
- Struct constructor with unparenthesized parameter incorrectly renamed,
causing a compile error.
[#​3349](https://github.com/fsprojects/fantomas/issues/3349)

Special thanks to @​Copilot and @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-010](https://www.nuget.org/packages/fantomas/8.0.0-alpha-010)
    

## 8.0.0-alpha-009

# 8.0.0-alpha-009

### Fixed
- Comment without code after it in the current scope should retain their
"parent" indentation.
[#​2286](https://github.com/fsprojects/fantomas/issues/2286)
- Code comment after expression is assigned to parenthesis.
[#​1864](https://github.com/fsprojects/fantomas/issues/1864)
- Misaligned comment in MultiLineBlockBracket.
[#​1716](https://github.com/fsprojects/fantomas/issues/1716)
- Comment unindented after DU cases.
[#​2606](https://github.com/fsprojects/fantomas/issues/2606)
- Comment on last match case has wrong indentation.
[#​2653](https://github.com/fsprojects/fantomas/issues/2653)
- Commented line inside of record should be indented at the same level
as the fields
[#​2482](https://github.com/fsprojects/fantomas/issues/2482)
- Unindentation of comment before end of computation expression.
[#​2476](https://github.com/fsprojects/fantomas/issues/2476)
- Comments lost the indented after formatting.
[#​2362](https://github.com/fsprojects/fantomas/issues/2362)
- Comments can be lost or shifted when at the end of expressions.
[#​932](https://github.com/fsprojects/fantomas/issues/932)
- Should not move the starting point of a single-line comment.
[#​1233](https://github.com/fsprojects/fantomas/issues/1233)

Special thanks to @​Copilot and @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-009](https://www.nuget.org/packages/fantomas/8.0.0-alpha-009)
    

## 8.0.0-alpha-008

# 8.0.0-alpha-008

### Added
- Support for `#elif` preprocessor directive.
[#​3268](https://github.com/fsprojects/fantomas/issues/3268)

### Fixed
- Multiline `val` body in signature files was not indented correctly.
[#​3269](https://github.com/fsprojects/fantomas/pull/3269)
- `///` doc comment without associated declaration (e.g. at end of file)
was duplicated when formatting.
[#​2499](https://github.com/fsprojects/fantomas/issues/2499)
- NamedIndexedPropertySet with two SynLongIdent removed space.
[#​3273](https://github.com/fsprojects/fantomas/issues/3273)
- `%%` (double-percent) infix operator moved to new line, producing
invalid F#. [#​2107](https://github.com/fsprojects/fantomas/issues/2107)
- Indentation warning when formatting `match` with long anonymous record
discriminant.
[#​1903](https://github.com/fsprojects/fantomas/issues/1903)
- Index-without-dot with variable key followed by unit arguments added
spurious spaces, e.g. `dict[key] () ()` became `dict [ key ] () ()`.
[#​2519](https://github.com/fsprojects/fantomas/issues/2519)
- Open-ended expressions (lambda, if-then-else, match, ...) in non-last
positions of infix, tuple, list/array, and record expressions now stay
multiline to preserve semantics.
[#​3279](https://github.com/fsprojects/fantomas/issues/3279)
- Lambda in tuple in list on single line changes code meaning.
[#​3278](https://github.com/fsprojects/fantomas/issues/3278)
- Custom operator applied to lambda collapses to single line changing
semantics. [#​3274](https://github.com/fsprojects/fantomas/issues/3274)

Special thanks to @​Copilot and @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-008](https://www.nuget.org/packages/fantomas/8.0.0-alpha-008)
    

## 8.0.0-alpha-007

# 8.0.0-alpha-007

### Changed
- Update FCS to 'Treat warn directives as trivia', commit
ab1f6ceaaec997d2854ac1c07a6c0f107675d95c
[#​3263](https://github.com/fsprojects/fantomas/issues/3263)

### Fixed
- `#nowarn` in arbitrary places produces a parsing failure.
[#​3263](https://github.com/fsprojects/fantomas/issues/3263)

Special thanks to @​Copilot and @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-007](https://www.nuget.org/packages/fantomas/8.0.0-alpha-007)
    

## 8.0.0-alpha-006

# 8.0.0-alpha-006

### Fixed
- Space between uppercase function name and uppercase parameter was
dropped. [#​3264](https://github.com/fsprojects/fantomas/pull/3264)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-006](https://www.nuget.org/packages/fantomas/8.0.0-alpha-006)
    

## 8.0.0-alpha-005

# 8.0.0-alpha-005

### Fixed
- Closing `>` in nested multiline generic type applications violates
offside rule.
[#​3243](https://github.com/fsprojects/fantomas/issues/3243)
- Index-without-dot syntax followed by application arguments was
inconsistently formatted.
[#​3259](https://github.com/fsprojects/fantomas/pull/3259)

Special thanks to @​Copilot and @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-005](https://www.nuget.org/packages/fantomas/8.0.0-alpha-005)
    

## 8.0.0-alpha-004

# 8.0.0-alpha-004

### Changed
- Improved error message when conditional compilation directives produce
invalid syntax for some define combinations.
[#​563](https://github.com/fsprojects/fantomas/issues/563)

### Fixed
- Lambda in non-last record field is now parenthesized when record is
collapsed to single line to avoid producing invalid code.
[#​3246](https://github.com/fsprojects/fantomas/issues/3246)

Special thanks to @​Copilot, @​claude, @​nojaf and @​randrag!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-004](https://www.nuget.org/packages/fantomas/8.0.0-alpha-004)
    

## 8.0.0-alpha-003

# 8.0.0-alpha-003

### Fixed
- End of line comments after infix operators are preserved correctly.
[#​2287](https://github.com/fsprojects/fantomas/issues/2287)
- Lambda closing parenthesis in chained method calls is no longer placed
on its own line when `MultiLineLambdaClosingNewline` is enabled.
[#​2553](https://github.com/fsprojects/fantomas/issues/2553)
- Long `&` (AND) patterns now break across multiple lines to respect max
line length.
[#​1780](https://github.com/fsprojects/fantomas/issues/1780)
- Empty array with trivia inside now has correct indentation in
Stroustrup style.
[#​3098](https://github.com/fsprojects/fantomas/issues/3098)
- Multiline function type inside parentheses gets extra indentation to
avoid compiler error.
[#​3043](https://github.com/fsprojects/fantomas/issues/3043)
- Trivia around paren lambda with conditional compilation no longer
causes merge error.
[#​2844](https://github.com/fsprojects/fantomas/issues/2844)
- Type app closing angle bracket padded by previous expression length.
[#​3179](https://github.com/fsprojects/fantomas/issues/3179)
- Dynamic operator on result of qualified function call causes
formatting error.
[#​3135](https://github.com/fsprojects/fantomas/issues/3135)
- Long delegate type with generic args no longer breaks around arrow.
[#​2468](https://github.com/fsprojects/fantomas/issues/2468)
- Idempotency problem when formatting NUnit Assert.That with lambda
argument. [#​1740](https://github.com/fsprojects/fantomas/issues/1740)
- Comment between lines of no-break infix expression no longer loses
indentation.
[#​2944](https://github.com/fsprojects/fantomas/issues/2944)
- Hash directives around access modifier in module declaration no longer
lose indentation.
[#​3188](https://github.com/fsprojects/fantomas/issues/3188)

Special thanks to @, @​Copilot, @​dsyme and @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-003](https://www.nuget.org/packages/fantomas/8.0.0-alpha-003)
    

## 8.0.0-alpha-002

# 8.0.0-alpha-002

### Changed
- Breaking: change default of MultilineBracketStyle from Cramped to
Aligned. [#​3200](https://github.com/fsprojects/fantomas/issues/3200)

Special thanks to @​nojaf!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-002](https://www.nuget.org/packages/fantomas/8.0.0-alpha-002)
    

## 8.0.0-alpha-001

# 8.0.0-alpha-001

### Changed
- Update FCS to 'Remove LetOrUseKeyword from SynExprLetOrUseTrivia',
commit 43932b4c7984d6562e91e5f1484868cd4f5befcf
[#​3167](https://github.com/fsprojects/fantomas/pull/3167)

Special thanks to @​Martin521, @​Numpsy, @​Thorium, @​dawedawe,
@​karanveersp, @​mattgallagher92, @​nojaf and @​tboby!


[https://www.nuget.org/packages/fantomas/8.0.0-alpha-001](…
This was referenced Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants