﻿---
title: F# through Fantomas
description: curb format and curb check also format F#, by handing it to the Fantomas your repository pins in its tool manifest. How to turn it on, what Curb guarantees on top of Fantomas, caching, line endings and the build integration.
url: https://docs-v3-preview.elastic.dev/workflow/fsharp
---

# F# through Fantomas
Curb formats the F# in a repository as well as the C#, from the same command and the same
`.editorconfig`. It does not format F# itself: it hands the files to [Fantomas](https://fsprojects.github.io/fantomas/),
the version your repository pins, and keeps everything around it its own: which files, the cache, the
write, the byte-order mark, the exit code and the build integration. Nothing changes until a tool
manifest declares Fantomas.

## Turning it on

Declare Fantomas as a local tool, and commit the manifest:
```sh
dotnet new tool-manifest     
dotnet tool install fantomas
```

From then on `curb format` and `curb check` cover `.fs`, `.fsi` and `.fsx` files, and CI needs a
`dotnet tool restore` before it runs Curb, as it would before running Fantomas.
Curb finds the manifest the way the SDK does: from each file's folder upwards, reading
`.config/dotnet-tools.json` and `dotnet-tools.json`, and stopping at one marked `"isRoot": true`. The
nearest manifest that declares `fantomas` (or the older `fantomas-tool`) decides the version, so two
parts of a repository can pin two versions. Curb never installs anything and never uses a global
Fantomas.

| Situation                                                                       | What happens                                                                                                                                                        |
|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| No manifest declares Fantomas                                                   | The F# files are not read and not counted. One line, `fsharp: N file(s) skipped — no Fantomas in a tool manifest`, says so. Exit code unaffected.                   |
| A manifest the SDK would refuse (not valid JSON, or Fantomas without a version) | The files it governs fail with the parse error and the manifest's path: exit 3, nothing written. A check does not pass by checking nothing.                         |
| Declared, but not restored                                                      | Exit 3, with a message to run `dotnet tool restore`. Detected without starting anything. A failure in `check` too: a check that could not check has passed nothing. |
| The Fantomas that starts is not the version the manifest pins                   | Exit 3, with the same message. Build metadata (`+9b5cde1`) is ignored.                                                                                              |

Each manifest's files succeed or fail on their own: one broken or unrestored manifest does not stop the
files under another from being formatted.

| Fantomas version          | What works                                                                                                                            |
|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------|
| 8.0.0-alpha-016 and later | The daemon, and the command line fallback (`fantomas check --json`). Tested with 8.1.0.                                               |
| Earlier                   | The daemon only (started with `--daemon`). If it cannot be used, the files fail with a message naming the version the fallback needs. |


## What Curb guarantees on top of Fantomas

Fantomas decides the layout, entirely. What Curb adds is the same rule it holds C# to: nothing is
written that was not checked first.
The Fantomas daemon does not validate what it produces. Run over the 518 F# files in dotnet/fsharp's `src/`,
it returned 6 results that do not parse and 4 that change again when formatted a second time, all reported
as formatted. (The Fantomas command line rejects the 6 and writes the 4.) So Curb sends every
formatted result back to Fantomas, and writes it only if the answer is "unchanged". On that run every one
of the 136 files Curb wrote was "unchanged" to `fantomas check` afterwards.

| Fantomas says                            | Curb reports                    | The file                                |
|------------------------------------------|---------------------------------|-----------------------------------------|
| Unchanged                                | unchanged                       | Left alone, and remembered in the cache |
| Formatted, and unchanged when sent back  | changed (`check`: would change) | Written by Curb                         |
| Formatted, and the result does not parse | failed: invalid output          | Left alone                              |
| Formatted, and the result changes again  | failed: unstable                | Left alone                              |
| Error                                    | unparsable                      | Left alone                              |
| Ignored (`.fantomasignore`)              | skipped                         | Left alone                              |

The second request costs time only for files that needed formatting; a file that is already formatted is
sent once. `check` applies the same rule, so `check` and `format` never disagree about a file.

## Settings

Fantomas reads its settings from `.editorconfig` itself (the `fsharp_*` keys and the core keys such as
`indent_size` and `max_line_length`), and Curb passes nothing over the top. Curb reads only
`charset`, to decide the byte-order mark, as it does for C#. A setting Fantomas does not recognise is
reported once at the end of the run.
`generated_code = true` skips a file, and so does an `<auto-generated>` marker in the comments a file
opens with, which is what `AssemblyInfo.fs` and similar files carry.

### Line endings

Without `end_of_line`, Fantomas writes the line endings of the machine it runs on. A repository checked out
with LF on one machine and formatted on Windows changes every line. Set it for F# files:
```ini
[*.{fs,fsi,fsx}]
end_of_line = lf
```


## Caching

With `--cache` (always on in the build integration) a file is skipped when its bytes and its resolved
settings match a run that watched Fantomas leave it unchanged. For F# the key also holds the exact
Fantomas version the manifest pins, the file's extension, and the content of the `.fantomasignore` that
governs it. Upgrading Fantomas re-checks the F# files and nothing else. Editing `.fantomasignore`
re-checks the files under it.
A run whose F# files all come from the cache starts no process at all.

## How it runs

One Fantomas daemon per run, started on the first F# file that misses the cache and stopped at the end,
with about one request per processor in flight. Curb starts it with `dotnet exec` on the tool's own
`.dll`, found from the SDK's tool resolver cache or the NuGet packages folder, which saves the extra
process `dotnet tool run` would cost.
If the daemon cannot be started or does not answer, Curb falls back to the Fantomas command line:
one `fantomas check --json` per few thousand files, never one per file. To format, the command line has to
write in place, so Curb lets it, checks the result a second time, and puts every file it handed over
back to its original bytes, whatever happened in between, before writing the stable results itself. A file
that cannot be put back is failed and reported. Set `CURB_FANTOMAS_DAEMON=false` to use the command line
from the start.
Nothing waits on Fantomas for ever. Each request to the daemon gets 60 seconds per 256 KB of source, and
each command line run 60 seconds per MB; set `CURB_FANTOMAS_TIMEOUT` to a number of seconds to change the
base. When time runs out, or the run is cancelled with Ctrl+C, Fantomas is stopped, none of that
manifest's files are written, and the run exits 3.
`curb format --language fsharp` formats only F#.

## What it costs

Measured on an M2 Pro (12 cores) with the native binary and Fantomas 8.1.0, medians of 5 to 9 runs. The
clean set is Fantomas's own `src/` (150 files, repeated to 1,000 and 10.8 MB); the set that needs
formatting is the 140 files in dotnet/fsharp's `src/` that Fantomas 8.1.0 would change, repeated to 1,000.

|                                                               | Wall time                 |
|---------------------------------------------------------------|---------------------------|
| `curb check --cache`, 1,000 F# files, all cached              | 34 ms, no process started |
| The same tree with no manifest declaring Fantomas             | 19 ms                     |
| `curb check`, 1 / 100 / 1,000 formatted files, nothing cached | 0.51 s / 0.72 s / 2.3 s   |
| `curb format`, 1 / 100 / 1,000 files that need formatting     | 0.61 s / 3.9 s / 17.6 s   |
| The same with the command line fallback                       | 1.01 s / 4.4 s / 28.2 s   |
| `curb check` on 10.8 MB of C#, for comparison                 | 0.51 s                    |

Fantomas does the formatting, so F# runs at Fantomas's speed: about 5 MB/s of already formatted source,
against about 21 MB/s for Curb's own C#. The cache is what keeps it off the common path.

## The build integration

The `curb` package adds a `CurbFSharp` target to F# projects. It runs before `CoreCompile` with the same
`Curb_Check`, `Curb_Bypass`, `Curb_Cache` and `Curb_LogLevel` properties as the C# target, and raises
the same CURB0001 and CURB0002 diagnostics. Set `Curb_FSharp` to `false` to leave F# projects alone.
The target hands over the project's `Compile` items, minus anything under `obj/` (the SDK generates
`AssemblyAttributes.fs` there with no header). It is skipped as up to date unless one of these changed
since the last clean run: a source, the project file, a tool manifest, a `.fantomasignore`, or an
`.editorconfig` above the project or beside a source. An unchanged project starts no process.
A project with no manifest declaring Fantomas builds as before; the target says why it did nothing at
`Curb_LogLevel`. A manifest that is not restored fails the build with CURB0002.

## Upgrading

Nothing changes for a repository whose manifests do not declare Fantomas, except one line in the
summary when the run finds F# files, saying they were skipped and why. Once Fantomas is declared,
`curb check` fails on F# files Fantomas would change, so run `curb format .` once and commit the result,
or set `Curb_FSharp=false` in the build until you are ready.