Skip to content

Commit 628409c

Browse files
authored
Merge pull request #2 from AgricoZA/bugfix/2416-fantomas-invalid-fsharp-alignment
fsprojects#2416 Fix Agrico alignment invalid output
2 parents 1d6ac25 + 635dd5c commit 628409c

6 files changed

Lines changed: 435 additions & 253 deletions

File tree

AGENTS.md

Lines changed: 230 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,49 @@
11
# Fantomas
22

3-
F# source code formatter. Parses F# to an untyped AST (via vendored FCS), transforms it to an intermediate representation called Oak (`SyntaxOak.fs`), then prints it back via writer events (`CodePrinter.fs` + `Context.fs`).
3+
This file provides canonical guidance to coding agents working in this repository.
44

5-
## Build & Test
5+
## Issue Tracking
6+
7+
Issues for this repository are tracked at: https://github.com/AgricoZA/Ops/issues
8+
9+
## Build Commands
610

711
```bash
8-
dotnet build fantomas.slnx
9-
dotnet test src/Fantomas.Core.Tests/
12+
# Initialize repository (downloads FCS files from F# compiler)
13+
dotnet fsi build.fsx -p Init
14+
15+
# Full build pipeline (format check, build, test, pack)
16+
dotnet fsi build.fsx
17+
18+
# Build only
19+
dotnet build -c Release
20+
21+
# Run all tests
22+
dotnet test
23+
24+
# Run specific test project
25+
dotnet test src/Fantomas.Core.Tests
26+
27+
# Run a specific test by name
28+
dotnet test src/Fantomas.Core.Tests --filter "FullyQualifiedName~TestClassName.TestName"
29+
30+
# Format all code
31+
dotnet fsi build.fsx -p FormatAll
32+
33+
# Format only changed files
34+
dotnet fsi build.fsx -p FormatChanged
35+
36+
# Check formatting without changing files
37+
dotnet fantomas src docs build.fsx --check
38+
39+
# Run benchmarks
40+
dotnet fsi build.fsx -p Benchmark
41+
42+
# Run analyzers
43+
dotnet fsi build.fsx -p Analyze
44+
45+
# Build and serve documentation locally
46+
dotnet fsi build.fsx -p Docs
1047
```
1148

1249
## Diagnostic Scripts
@@ -20,9 +57,95 @@ All scripts accept a file path or stdin, with optional `--signature` and `--edit
2057

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

23-
## Changelog
60+
## Architecture Overview
61+
62+
Fantomas is an opinionated F# source code formatter. It parses F# source into an AST, transforms it to a custom tree model (Oak), and reconstructs formatted source code.
63+
64+
### Project Structure
65+
66+
```
67+
Fantomas.FCS → Fantomas.Core → Fantomas (CLI)
68+
69+
Fantomas.Client (editor integration)
70+
```
71+
72+
- **Fantomas.FCS**: Custom fork of F# compiler exposing only the parser. Downloads files from `dotnet/fsharp` repo via `Init` pipeline.
73+
- **Fantomas.Core**: Core formatting library (netstandard2.0). Public API in `CodeFormatter.fsi`.
74+
- **Fantomas**: CLI tool handling `.editorconfig` and `.fantomasignore` files.
75+
- **Fantomas.Client**: LSP-based library for editor integration (not direct dependency on Core).
76+
77+
### Formatting Pipeline
78+
79+
1. **Parse**: `Fantomas.FCS.parseFile` → Untyped AST
80+
2. **Transform**: `ASTTransformer.fs` → Oak (custom tree model)
81+
3. **Collect Trivia**: `Trivia.fs` → Comments, blank lines, directives
82+
4. **Print**: `CodePrinter.fs` → Formatted source string
83+
84+
### Key Source Files
85+
86+
- `src/Fantomas.Core/SyntaxOak.fs` - Oak tree model definitions
87+
- `src/Fantomas.Core/ASTTransformer.fs` - AST to Oak transformation (uses partial active patterns extensively)
88+
- `src/Fantomas.Core/CodePrinter.fs` - Main formatting logic (~165KB, core of formatter)
89+
- `src/Fantomas.Core/Trivia.fs` - Comment and whitespace handling
90+
- `src/Fantomas.Core/Context.fs` - Formatting context and event sourcing for output
91+
- `src/Fantomas.Core/FormatConfig.fs` - Configuration options
92+
93+
### F# Patterns Used
94+
95+
- **Partial Active Patterns**: Heavy use in `ASTTransformer.fs` for pattern matching on AST nodes
96+
- **Custom Operators**: `!-` and `+>` in `CodePrinter.fs`
97+
- **Signature Files (.fsi)**: Define module boundaries and public API
98+
- **Type Extensions**: Extend AST types with range information
99+
- **Event Sourcing**: Instructions written to event list in `Context.fs` before final output
100+
101+
### Trivia Handling
102+
103+
Trivia (comments, blank lines, conditional directives) are not part of the AST. They're:
104+
1. Detected from `ParsedImplFileInputTrivia`/`ParsedSigFileInputTrivia` and `ISourceText`
105+
2. Inserted into Oak nodes as `ContentBefore`/`ContentAfter`
106+
107+
Common trivia bug fix: Use `sepNlnConsideringTriviaContentBeforeForMainNode` instead of `sepNln` in `CodePrinter.fs`.
108+
109+
## Testing
110+
111+
Tests use NUnit with FsUnit assertions. Core tests are in `src/Fantomas.Core.Tests/`.
112+
113+
### Test fixture style
114+
115+
Formatter tests should use neutral Fantomas-style sample code, not Agrico Ops domain names, business concepts, or naming conventions. When reducing a bug found in Ops, replace domain-specific names such as `Invoice`, `Docket`, `StockViewer`, `itemGuidO`, or `SuccessfulDomainEventU` with generic names while preserving only the syntactic shape needed to reproduce the formatter behaviour.
116+
117+
```bash
118+
# Run single test file's tests
119+
dotnet test src/Fantomas.Core.Tests --filter "FullyQualifiedName~CommentTests"
120+
121+
# Run specific test
122+
dotnet test src/Fantomas.Core.Tests --filter "TestName~my_test_name"
123+
```
124+
125+
## Online Tools
126+
127+
- AST Viewer: https://fsprojects.github.io/fantomas-tools/#/ast
128+
- Trivia Viewer: https://fsprojects.github.io/fantomas-tools/#/trivia
129+
- Fantomas Online: https://fsprojects.github.io/fantomas-tools/#/fantomas/preview
130+
- F# Tokens: https://fsprojects.github.io/fantomas-tools/#/tokens
131+
132+
## Style Guides
24133

25-
When updating `CHANGELOG.md`, add new entries to the **end** of the relevant section (e.g. `### Fixed`), not the top. One entry per issue.
134+
Fantomas implements:
135+
- Microsoft F# Style Guide: https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/formatting
136+
- G-Research Style Guide: https://github.com/G-Research/fsharp-formatting-conventions
137+
138+
Stylistic feature requests should be discussed on those repos first, not here.
139+
140+
## Code Quality
141+
142+
```xml
143+
<!-- Warnings as errors -->
144+
FS0025: Incomplete pattern matches
145+
FS1182: Unused variables
146+
```
147+
148+
Self-formatted using Fantomas 7.0.1.
26149

27150
## Post-task Steps
28151

@@ -41,3 +164,104 @@ dotnet fsi build.fsx -- -p Analyze
41164
```
42165

43166
Output goes to `analysis.sarif` in the repo root.
167+
168+
## Internal Publishing (AgricoZA Fork)
169+
170+
This is a fork of [fsprojects/fantomas](https://github.com/fsprojects/fantomas) with custom features (e.g., `LeadingTupleSeparator` for union case fields).
171+
172+
### Versioning Scheme
173+
174+
Versions follow the pattern `{upstream-version}-agrico-{NNN}`, e.g. `8.0.0-alpha-003-agrico-001`. This embeds the upstream version we're based on.
175+
176+
**Reset `NNN` to `001` each time we rebase onto a new upstream version.** Within a given upstream, `NNN` counts our own iterations.
177+
178+
### Package ID
179+
180+
This fork publishes under the upstream package id `fantomas`. The CLI command name is also `fantomas` (via `<ToolCommandName>fantomas</ToolCommandName>`).
181+
182+
A previous attempt (`agrico-003`) renamed the package id to `fantomas.agrico` to dodge SemVer collision with upstream Fantomas on nuget.org. That broke any tooling that looks Fantomas up by package id — most notably JetBrains Rider's *Settings → Languages & Frameworks → F# → Fantomas → Location → Local dotnet tool* detection, which silently fell back to its bundled Fantomas when the manifest entry was keyed on `fantomas.agrico`. Result: on-save formatting in Rider drifted from `dotnet fantomas` CLI output.
183+
184+
`agrico-004` reverted to package id `fantomas`. The collision with upstream Fantomas is now handled on the **consumer side** via NuGet `packageSourceMapping`: the `fantomas` package id is locked to the AgricoZA GitHub Packages feed (`<package pattern="fantomas" />` mapped to `github-agrico`), so `dotnet tool restore`/`update` never sees upstream's `fantomas` builds on nuget.org. See AgricoZA/Ops#2320 for the consumer-side change.
185+
186+
### CHANGELOG Constraints
187+
188+
Versions are extracted from `CHANGELOG.md` by `Ionide.KeepAChangelog.Tasks`. Subsection headings must be standard Keep a Changelog types (`Added`, `Changed`, `Fixed`, `Removed`, etc.) — custom headings like `### Upstream` will cause build failures.
189+
190+
### Publishing to GitHub Packages
191+
192+
Packages are published to the **AgricoZA GitHub Packages NuGet feed** (`https://nuget.pkg.github.com/AgricoZA/index.json`), which is configured as a source in the Ops repo's `NuGet.config`. Published versions are visible at https://github.com/orgs/AgricoZA/packages/nuget/package/fantomas.
193+
194+
**Steps to publish a new version:**
195+
196+
1. **Update version in CHANGELOG.md**:
197+
```markdown
198+
## [8.0.0-alpha-012-agrico-NNN] - YYYY-MM-DD
199+
200+
### Added
201+
- Description of new feature
202+
```
203+
204+
2. **Build and test:**
205+
```bash
206+
dotnet fsi build.fsx
207+
```
208+
Packages are output to `artifacts/package/release/`
209+
210+
3. **Push to GitHub Packages:**
211+
```bash
212+
dotnet nuget push artifacts/package/release/fantomas.8.0.0-alpha-012-agrico-NNN.nupkg \
213+
--source "https://nuget.pkg.github.com/AgricoZA/index.json" \
214+
--api-key $(gh auth token)
215+
```
216+
217+
4. **In Ops workspace, update the tool:**
218+
```bash
219+
cd ../ops4/Workspace
220+
dotnet tool update fantomas --version 8.0.0-alpha-012-agrico-NNN
221+
```
222+
223+
The Ops repo's `NuGet.config` files include a `<packageSourceMapping>` block that locks the `fantomas` package id to the AgricoZA feed, so this resolves to the fork even though both feeds carry packages named `fantomas`.
224+
225+
5. **Verify and commit** the updated `.config/dotnet-tools.json` in the Ops repo.
226+
227+
### Syncing Upstream Changes
228+
229+
The `custom` branch is rebased onto `upstream/main` to maintain a clean linear history. Our custom commits sit on top of upstream.
230+
231+
```bash
232+
git fetch upstream
233+
git rebase --onto upstream/main <old-upstream-head> custom
234+
```
235+
236+
### Minimising Upstream Merge Conflicts
237+
238+
When adding a fork-specific feature, arrange the code so that rebasing onto `upstream/main` produces at most a few trivial conflicts. The rules below apply to every Agrico feature (`LeadingTupleSeparator`, `RecordFieldAlignment`, anything we add next):
239+
240+
**Tests**
241+
- New tests go in their own file under `src/Fantomas.Core.Tests/Agrico/` with an `Agrico`-prefixed filename. Upstream never edits these files.
242+
- Register them at the bottom of the `Fantomas.Core.Tests.fsproj` `<ItemGroup>` under the comment `<!-- Agrico fork: keep custom tests isolated so upstream merges never conflict. -->`. Conflicts on this line are trivial to resolve.
243+
- Never extend an upstream-owned test file (`TupleTests.fs`, `LetBindingTests.fs`, etc.) with Agrico test cases — move them out.
244+
245+
**Config fields in `FormatConfig.fs`**
246+
- Append new fields at the end of the `FormatConfig` record and at the end of `FormatConfig.Default`. Both are touchpoints upstream also grows over time; end-of-record placement keeps the textual diff isolated and the merge trivial.
247+
248+
**Behaviour changes in `CodePrinter.fs`**
249+
- Large helpers (new `gen*` functions, policy predicates, grouping logic) live at the **tail of the file**, after `genField`, clustered together under a section-header comment block that names the feature. Upstream rarely modifies the tail. `module internal rec Fantomas.Core.CodePrinter` means these helpers can be called from anywhere earlier in the file without let-rec acrobatics.
250+
- The **inline edit** at the upstream call site must collapse to a **single function call** — not a multi-line conditional. Example for `RecordFieldAlignment` in the `TypeDefn.Record` branch:
251+
```fsharp
252+
+> indentSepNlnUnindent (genMaybeAlignedFieldList ctx.Config node.Fields)
253+
```
254+
Replaces one line of upstream code with one line of our code. If upstream refactors the surrounding function, the merge conflict is a single-line substitution.
255+
- Add a leading comment at the inline edit (`// Agrico: see <helper-name>.`) so a merger knows exactly what to preserve.
256+
- The helper should internally fall back to the upstream default when the feature is off, so inserting the call is behaviour-neutral without the config flag.
257+
258+
**Conflict triage when rebasing**
259+
- The predictable hot spots are: `FormatConfig.fs` (end of record + end of Default), `CodePrinter.fs` (the 1–3 inline call sites per feature), and `Fantomas.Core.Tests.fsproj` (the bottom `<Compile>` block). All other Agrico code lives in files upstream doesn't touch.
260+
- If upstream refactored around a call site, preserve **the call** (`+> indentSepNlnUnindent (genMaybeAligned...)`), not the surrounding boilerplate — let upstream's boilerplate win, keep our one-line hook.
261+
262+
### Custom Features in This Fork
263+
264+
- **LeadingTupleSeparator**: Also applies to discriminated union case fields (upstream only supports expressions, types, and patterns)
265+
- **RecordFieldAlignment**: gofmt-style alignment for Stroustrup records, with blank-line-delimited groups. Applies to two cases, both gated by the same flag:
266+
- **Type declarations**: aligns `:` across record fields. Long function-type field values wrap at each top-level `->` under the first argument's column; tuple-separator placement reuses the `LeadingTupleSeparator` flag.
267+
- **Construction expressions**: aligns `=` across record assignments. Stroustrup-style value expressions (nested records, lists) keep their normal layout; other long values (e.g. function applications) stay on the `=` line and wrap from there, mirroring the type-declaration wrap-from-colon behaviour.

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## [8.0.0-alpha-012-agrico-008] - 2026-05-01
4+
5+
### Changed
6+
7+
- Rebuilt the #2416 formatter fix from committed source so package metadata points at the implementation commit.
8+
9+
## [8.0.0-alpha-012-agrico-007] - 2026-05-01
10+
11+
### Fixed
12+
13+
- Agrico match-arrow alignment now falls back to a valid multiline arm when aligning the arrow would make an inline arm body wrap invalidly.
14+
- Agrico union-case alignment now avoids the inline-first-field layout for anonymous record payloads, preserving valid anonymous record syntax. See AgricoZA/Ops#2416.
15+
316
## [8.0.0-alpha-012-agrico-006] - 2026-05-01
417

518
### Fixed

0 commit comments

Comments
 (0)