diff --git a/.claude/commands/chain.md b/.claude/commands/chain.md new file mode 100644 index 0000000000..9ad83916e4 --- /dev/null +++ b/.claude/commands/chain.md @@ -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] +``` + +Or pipe inline source via stdin: + +``` +echo '' | dotnet fsi scripts/chain.fsx [--signature] +``` + +$ARGUMENTS diff --git a/.claude/skills/style-guides/SKILL.md b/.claude/skills/style-guides/SKILL.md new file mode 100644 index 0000000000..d505ddb02b --- /dev/null +++ b/.claude/skills/style-guides/SKILL.md @@ -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: +- **G-Research**, the alternative style behind the G-Research settings. + Local: `.deps/style-guides/g-research-conventions.md` + Rendered: + +## 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 , 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. diff --git a/.claude/skills/upgrade-trial/SKILL.md b/.claude/skills/upgrade-trial/SKILL.md new file mode 100644 index 0000000000..87a5652698 --- /dev/null +++ b/.claude/skills/upgrade-trial/SKILL.md @@ -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 /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. diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 6330d0f089..71468c1251 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -17,7 +17,7 @@ "rollForward": false }, "fsdocs-tool": { - "version": "22.0.0", + "version": "22.1.0", "commands": [ "fsdocs" ], @@ -29,6 +29,13 @@ "fsharp-analyzers" ], "rollForward": false + }, + "dotnet-reportgenerator-globaltool": { + "version": "5.5.11", + "commands": [ + "reportgenerator" + ], + "rollForward": false } } -} \ No newline at end of file +} diff --git a/.gitignore b/.gitignore index 35fe30a540..b4e9a9cbe1 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 776e5e9467..58cec17498 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`). @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index d3e968d11f..f6f1653d54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Directory.Packages.props b/Directory.Packages.props index d2b6a2cbbe..89170cafc7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -38,5 +38,6 @@ + diff --git a/build.fsx b/build.fsx index 2b90ae7f1f..58f36961cc 100755 --- a/build.fsx +++ b/build.fsx @@ -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" @@ -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" } @@ -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 = @@ -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 } diff --git a/docs/_body.html b/docs/_body.html index 5dddc4cdcb..8793272d6c 100644 --- a/docs/_body.html +++ b/docs/_body.html @@ -7,7 +7,25 @@ })