Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e2cf221
Redesign ExprChain around a head, segments and a terminal call
nojaf Jul 31, 2026
cc0122d
Add struct to ChainCallPosition
nojaf Jul 31, 2026
4eb29c1
Clarify the two changes more obviously
nojaf Jul 31, 2026
c3106a6
Merge branch 'main' into fix-3364
nojaf Jul 31, 2026
031c03a
Clarify which parentheses stay tight in a chain
nojaf Jul 31, 2026
935d503
More usage of InvariantViolation
nojaf Jul 31, 2026
097eb3f
improve wording
nojaf Aug 4, 2026
b1c1f45
Merge branch 'main' into fix-3364
nojaf Aug 5, 2026
ca30bd3
Add code review feedback
nojaf Aug 5, 2026
a9a610b
Use regular mermaid syntax in markdown
nojaf Aug 6, 2026
334e270
Balance long navigation runs when breaking a chain
nojaf Aug 6, 2026
0d955c2
Merge branch 'main' into fix-3364
nojaf Aug 19, 2026
ab29d26
Update stale documentation
nojaf Aug 13, 2026
07766fb
Let trivia decide where a call's parenthesis sits
nojaf Aug 19, 2026
25e3a25
Harden the Fantomas.Client end to end tests
nojaf Aug 19, 2026
23215db
Merge branch 'main' into fix-3364
nojaf Aug 19, 2026
11dd4a8
Update chains doc to include samples with comments in terminal applic…
nojaf Aug 19, 2026
7639601
Stop reading the source layout of a match lambda argument
nojaf Aug 19, 2026
69ae0d4
Remove ChainCallPosition
nojaf Aug 19, 2026
d145d00
Merge branch 'main' into fix-3364
nojaf Aug 19, 2026
dbf6ca8
Ask whether a lambda opener fits before leaving it in place
nojaf Aug 19, 2026
a8aaeca
Add style guide skill
nojaf Aug 19, 2026
75f0d8a
Ask the same question when the closing newline setting is on
nojaf Aug 19, 2026
e04c542
Add upgrade skill
nojaf Aug 19, 2026
3af3ca1
Do local download of style guides
nojaf Aug 19, 2026
f88bffc
Ask whether the lambda broke before moving its closing parenthesis
nojaf Aug 19, 2026
2ab7663
Don't trust the configuration.fsx settings.
nojaf Aug 19, 2026
9069678
Download settings for GR
nojaf Aug 19, 2026
27cf0dd
Code base may have been drifting from the start.
nojaf Aug 20, 2026
afc0f1d
Merge branch 'main' into fix-3364
nojaf Aug 20, 2026
ca5475b
Find the comment after a receiver wherever it is attached
nojaf Aug 20, 2026
7763bc6
Add changelog and upgrade guide
nojaf Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .claude/commands/chain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
description: Show the ExprChain structure (head, segments, terminal) of F# source code
allowed-tools: Bash(dotnet fsi:*), Bash(echo:*), Bash(dotnet build:*)
---

First build the project: `dotnet build src/Fantomas.Core/Fantomas.Core.fsproj`

Then run the chain script. Pass a file path as argument:

```
dotnet fsi scripts/chain.fsx [--signature] <file>
```

Or pipe inline source via stdin:

```
echo '<source>' | dotnet fsi scripts/chain.fsx [--signature]
```

$ARGUMENTS
99 changes: 99 additions & 0 deletions .claude/skills/style-guides/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
name: style-guides
description: Look up what the F# style guides say about a formatting question. Use when deciding or defending a layout rule in Fantomas, when a Chains.md or design discussion needs a citation, or when the user asks what Microsoft or G-Research prescribe.
---

# F# style guides

Fantomas does not decide F# style, it implements it. Two documents are the authority, and a
layout decision that contradicts them needs a reason written down.

## Always work from the local copies

Both guides belong in `.deps/style-guides/`, which is gitignored, so keeping them costs nothing and
leaves no trace. Never answer from the rendered pages or from memory: grep the local file and quote
it. Local copies are also what makes the examples testable, since you can run them through
Fantomas.

Refresh them at the start of the task. Both raw URLs return an `ETag`, so a conditional request
downloads only when the document changed and is free otherwise:

```bash
mkdir -p .deps/style-guides
cd .deps/style-guides
fetch() { curl -sS --etag-compare "$2.etag" --etag-save "$2.etag" -o "$2" "$1"; }
fetch https://raw.githubusercontent.com/dotnet/docs/main/docs/fsharp/style-guide/formatting.md microsoft-formatting.md
fetch https://raw.githubusercontent.com/G-Research/fsharp-formatting-conventions/master/README.md g-research-conventions.md
fetch https://raw.githubusercontent.com/G-Research/fsharp-formatting-conventions/master/.editorconfig g-research.editorconfig
```

The `.etag` files next to the documents are what make the second run free; keep them. Verified that
a repeat run leaves the file byte for byte intact rather than truncating it.

## The sources

- **Microsoft**, the default style Fantomas follows.
Local: `.deps/style-guides/microsoft-formatting.md`
Rendered: <https://learn.microsoft.com/en-us/dotnet/fsharp/style-guide/formatting>
- **G-Research**, the alternative style behind the G-Research settings.
Local: `.deps/style-guides/g-research-conventions.md`
Rendered: <https://github.com/G-Research/fsharp-formatting-conventions>

## Which settings a guide means

Do not infer these from the prose, from the examples, or from our own documentation.

Microsoft is the Fantomas defaults. Nothing to configure, so format with `FormatConfig.Default`,
and a deviation is a Fantomas bug rather than a settings question.

G-Research ships its settings, and `.deps/style-guides/g-research.editorconfig` is the whole
answer. Use it verbatim.

One warning about `docs/docs/end-users/Configuration.fsx`. The `gr` badge marks a setting as part
of the G-Research set, but the value in the copy-to-clipboard box is the value that section
demonstrates, usually the non-default one. It is not a claim about what G-Research chose. Read
those boxes as G-Research values and you will get it wrong.

Style discussions themselves happen at <https://github.com/fsharp/fslang-design#style-guide>, not
in this repository. See `docs/docs/end-users/StyleGuide.md`.

## How to use them

The Microsoft document is large, tens of kilobytes, so grep the local copy for the section you need
rather than reading it whole. Its headings are stable and worth knowing:

- `Formatting application expressions`
- `Formatting lambda expressions`
- `Formatting function and member arguments`
- `Formatting pipeline expressions`
- `Formatting if expressions`
- `Formatting record expressions`

Quote verbatim, including the ✔️ and ❌ examples. The ❌ ones carry the reasoning, and the reason
is usually the part that settles an argument. One example: the guide rejects lambda parameters
aligned under an opening parenthesis, because the column then depends on the length of the
identifier in front of it.

## Checking our output against the examples

The code blocks are testable: extract them from the local copy and run them through Fantomas with
the settings that style implies. Three traps make a naive pass-rate meaningless, all seen for real:

- Some blocks mark the bad version by **variable name**, `let bad = ...`, rather than by a
`// Not OK` comment, so Fantomas rewriting it is the guide being obeyed, not broken.
- Many blocks are **fragments** that do not parse alone, or illustrate naming and structure rather
than layout. `if cond then e1 else e2` shown across four lines is not a claim about line breaks.
- The **settings have to be right**. Take them from `g-research.editorconfig`, never from the
examples or from our documentation. A hand-assembled set produces a number that says more about
the set than about Fantomas.

So classify a block before diffing it, and say what was excluded and why. A pass rate without that
classification says nothing.

## What to do with the answer

- Cite the guide in the design note or the doc change, with the rule quoted, so the next person
does not have to re-derive it.
- When both guides agree, say so; that is a strong signal.
- When Fantomas disagrees with the guide, that is a bug report or a proposal to fslang-design, not
a local preference to encode quietly.
102 changes: 102 additions & 0 deletions .claude/skills/upgrade-trial/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
name: upgrade-trial
description: Try a Fantomas upgrade against a real code base on this machine. Takes the path to a local repository, walks it from its pinned version up to latest stable, latest alpha, and a local build, formatting, checking and building at each step, committing on a test branch. Use to find out whether a change breaks or improves real projects before releasing it.
---

# Upgrade trial

Format a real code base with successively newer Fantomas versions and prove it still builds. This
is the check that catches what the test suite cannot: what our changes do to code nobody on this
team wrote.

**Input**: the path to a local git repository. Without one, ask for it rather than guessing.

## Never work on the branch they have checked out

This trial rewrites files across the whole repository and commits several times. Doing that on
someone's working branch is destructive, and the mess is theirs to clean up.

Before touching anything: record the current branch, confirm the working tree is clean, and create
a dedicated branch for the trial, `fantomas-upgrade-trial` or similar. If a branch by that name is
already there, reuse it only after checking it holds nothing but earlier trials. If the tree is
dirty, stop and say so; do not stash their work to get going. Leave the repository on the trial
branch when you finish and tell them which branch that is and which branch they were on, so
switching back is one command.

Never push. The commits are evidence for a decision, not something to publish.

## Before starting

Establish these, and report them back before running anything long:

- How to get back: the branch they started on.
- How it pins Fantomas: usually `dotnet tool restore` against `.config/dotnet-tools.json`. Note the
version already pinned; that is rung zero.
- How it formats: usually `dotnet fantomas .`. Check `DEVGUIDE.md`, `CONTRIBUTING.md` or the CI
yaml for the exact invocation, including which paths.
- How it builds: `./build.sh`, `build.cmd`, or a plain `dotnet build`.
- Whether it has a `.fantomasignore`. Large repos exclude a lot, and the exclusions tell you what
they already know is awkward.

Say up front that the builds are long and CPU heavy, and run them in the background one at a time.

## Rung zero: the base state must pass its own check

Before touching any version, run the pinned version's `--check` and establish the scope it must
pass. When the docs and CI disagree on which paths to format, trust CI: that is the scope the
project actually enforces, and the rest is drift they have chosen to live with. FsAutoComplete's
CONTRIBUTING says to format `src/ test/`, but its CI has only ever checked `build.fsx src`; its
`test/` tree holds intentionally unparseable fixtures and has never been formatted. Reformatting
that scope on their behalf is churn they did not ask for, and can break fixture-sensitive tests in
ways a build will not catch. Only widen beyond the CI scope when they clearly want it.

If the check fails on the enforced scope, the base state is dirty, and the first commit on the
trial branch is fixing that: format with the pinned version, build, and commit as the rung-zero
baseline. Do this before any version bump, so each rung's diff isolates the version's effect
instead of inheriting drift. If the base state already passes, say so; that is information too.

Some files in scope may be genuinely unformattable: fixtures that are intentionally incomplete F#,
or code in syntax the pinned parser rejects (F# 7 relaxed indentation is a live example). Record
that set explicitly — file by file, with the reason — and compare each rung's check against it,
rather than demanding exit 0 from a repo that cannot give it.

## The rungs

For each of: **latest stable**, **latest alpha**, **a local build of the branch under test**:

1. Point the repository at that version. For published versions, edit the tool manifest and
`dotnet tool restore`. For the local build, do **not** package and install it; build the CLI in
Release and invoke the dll directly:
`dotnet <fantomas>/artifacts/bin/Fantomas/release/fantomas.dll . --check`.
2. Confirm the version actually in use with `--version`. The commit hash in it is worth recording.
3. Format the repository.
4. Run the same command with `--check`. It must exit 0: the formatter has to agree with its own
output. A failure here is an idempotency bug, and worth reporting rather than working around.
5. Build. Skip this when formatting changed nothing, since there is nothing for it to prove; say so
rather than silently skipping.
6. Commit on the test branch: the manifest change together with whatever formatting changed. Never
push.

Report per rung: files reformatted out of files considered, ignored, errored, whether `--check`
passed, and the build result.

## Traps this hit for real

- **Feeds.** Repos that restore from mirrors, dnceng for instance, lag behind nuget.org, so a
freshly published version is not there yet. Add
`--add-source https://api.nuget.org/v3/index.json` to `dotnet tool restore` rather than editing
their `NuGet.config`, and note it in the commit message.
- **Package source mapping** makes `--add-source` fail outright. When installing to a
`--tool-path`, run the install from a neutral directory so no repository `NuGet.config` applies.
- **Read our own CHANGELOG before blaming the branch.** A large diff is usually a documented
breaking change in a setting default, not the work under test. Check whether the repository sets
that setting for `[*.fs]` but not `[*.fsi]`, which is a common way to be surprised.
- **A version bump with zero changes is a result**, not a failure to find anything.

## Timing, if asked

Measure with `--check` so nothing is written, discard a warmup run, and interleave the versions to
cancel drift. Report the **minimum** of several runs, and prefer **wall clock**: user CPU time
varies by nearly 2x run to run on a laptop and will swamp any real difference. If the ranges of two
versions overlap, say the measurement was inconclusive instead of quoting a ratio. For a real
number, use `dotnet fsi build.fsx -- -p Benchmark`, which runs BenchmarkDotNet.
11 changes: 9 additions & 2 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"rollForward": false
},
"fsdocs-tool": {
"version": "22.0.0",
"version": "22.1.0",
"commands": [
"fsdocs"
],
Expand All @@ -29,6 +29,13 @@
"fsharp-analyzers"
],
"rollForward": false
},
"dotnet-reportgenerator-globaltool": {
"version": "5.5.11",
"commands": [
"reportgenerator"
],
"rollForward": false
}
}
}
}
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -207,5 +207,7 @@ tests/.repositories/**
# LLM stuff
.claude/settings.local.json

# altcover
# Coverage reports (see the Coverage pipeline in build.fsx)
coverage.xml
coveragereport/
coverage.xml.*.acv
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ All scripts accept a file path or stdin, with optional `--signature` and `--edit
- `scripts/oak.fsx` — Oak tree
- `scripts/format.fsx` — format with local build
- `scripts/writer-events.fsx` — writer events produced during formatting
- `scripts/chain.fsx` - ExprChain structure (head, segments, terminal); ignores `--editorconfig`

Scripts require a debug build first (`dotnet build src/Fantomas/Fantomas.fsproj`).

Expand All @@ -31,7 +32,7 @@ Run these after completing a task, not during iterative development — analyzer
### Format

```bash
dotnet fantomas src docs build.fsx
dotnet fsi build.fsx -- -p FormatAll
```

### Analyzers
Expand Down
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
# Changelog

## [Unreleased]
## [8.0.0-alpha-014] - 2026-08-20

### 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)

## [8.0.0-alpha-013] - 2026-08-19

Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.1.1" />
<PackageVersion Include="FsCheck" Version="3.3.2" />
<PackageVersion Include="FsUnit" Version="7.1.1" />
<PackageVersion Include="AltCover" Version="9.0.102" />
</ItemGroup>
</Project>
47 changes: 45 additions & 2 deletions build.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ let artifactsDir = __SOURCE_DIRECTORY__ </> "artifacts"
let binDir = artifactsDir </> "bin"
let packagesDir = artifactsDir </> "package" </> "release"
let analysisReportsDir = __SOURCE_DIRECTORY__ </> "analysisreports"
let coverageReportDir = __SOURCE_DIRECTORY__ </> "coveragereport"

let coverageXml =
__SOURCE_DIRECTORY__ </> "src" </> "Fantomas.Core.Tests" </> "coverage.xml"

let benchmarkAssembly =
binDir </> "Fantomas.Benchmarks" </> "release" </> "Fantomas.Benchmarks.dll"
Expand Down Expand Up @@ -82,7 +86,7 @@ pipeline "Build" {
workingDir __SOURCE_DIRECTORY__
stage "RestoreTools" { run "dotnet tool restore" }
stage "Clean" { run (cleanFolders [| analysisReportsDir; artifactsDir |]) }
stage "CheckFormat" { run "dotnet fantomas src docs build.fsx --check" }
stage "CheckFormat" { run "dotnet fantomas src docs scripts build.fsx --check" }
stage "Build" { run "dotnet build -c Release --tl" }
stage "UnitTests" { run "dotnet test -c Release --tl" }
stage "Pack" { run "dotnet pack --no-restore -c Release --tl" }
Expand All @@ -104,6 +108,45 @@ pipeline "Benchmark" {
runIfOnlySpecified true
}

// Line and branch coverage for Fantomas.Core, via AltCover's MSBuild integration.
//
// The filter is a negative lookahead: instrument Fantomas.Core and nothing else, which keeps the
// generated Fantomas.FCS parser (and the test assembly itself) out of the report and makes the
// run fast. AltCover writes OpenCover XML, which is for tooling rather than reading, so
// ReportGenerator turns it into a browsable HTML report afterwards.
//
// Produces:
// src/Fantomas.Core.Tests/coverage.xml raw OpenCover XML
// coveragereport/index.html browsable report, per file and per line
pipeline "Coverage" {
workingDir __SOURCE_DIRECTORY__
stage "RestoreTools" { run "dotnet tool restore" }
stage "Clean" { run (cleanFolders [| coverageReportDir |]) }

stage "Coverage" {
run
@"dotnet test src/Fantomas.Core.Tests/Fantomas.Core.Tests.fsproj -c Release /p:AltCover=true ""/p:AltCoverAssemblyFilter=^(?!Fantomas\.Core$)"""
}

stage "Report" {
run
$"dotnet reportgenerator -reports:{coverageXml} -targetdir:{coverageReportDir} -reporttypes:Html;TextSummary"
run (fun _ ->
async {
let summary = coverageReportDir </> "Summary.txt"
let index = coverageReportDir </> "index.html"

if File.Exists summary then
printfn "%s" (File.ReadAllText summary)

printfn $"Browse the full report at {index}"
return 0
})
}

runIfOnlySpecified true
}

let runGitCommand (arguments: string) =
async {
let! result =
Expand Down Expand Up @@ -192,7 +235,7 @@ pipeline "Docs" {

pipeline "FormatAll" {
workingDir __SOURCE_DIRECTORY__
stage "Fantomas" { run "dotnet fantomas src docs build.fsx" }
stage "Fantomas" { run "dotnet fantomas src docs scripts build.fsx" }
runIfOnlySpecified true
}

Expand Down
Loading
Loading