Skip to content

Commit 7fb3a0d

Browse files
authored
Merge pull request #3241 from nojaf/fix-563
Document conditional compilation define limitations
2 parents 333cb03 + 123447c commit 7fb3a0d

File tree

5 files changed

+141
-17
lines changed

5 files changed

+141
-17
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Changed
6+
7+
- Improved error message when conditional compilation directives produce invalid syntax for some define combinations. [#563](https://github.com/fsprojects/fantomas/issues/563)
8+
59
## [8.0.0-alpha-003] - 2026-03-03
610

711
### Fixed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
category: End-users
3+
categoryindex: 1
4+
index: 13
5+
---
6+
# Conditional Compilation Directives
7+
8+
Fantomas supports formatting F# code that contains conditional compilation directives (`#if`, `#else`, `#endif`).
9+
However, there is an important limitation to be aware of.
10+
11+
## How Fantomas handles directives
12+
13+
Fantomas needs to parse your code into an abstract syntax tree (AST) before it can format it.
14+
The F# parser processes `#if` / `#else` / `#endif` directives at parse time, meaning it picks one branch based on which defines are active and ignores the other.
15+
16+
To handle this, Fantomas:
17+
18+
1. Parses your code without any defines to discover all conditional directives.
19+
2. Determines every possible combination of defines.
20+
3. Parses and formats the code once for each combination.
21+
4. Merges the results back together.
22+
23+
## The limitation: all define combinations must produce valid syntax
24+
25+
Because Fantomas parses your code under **every** define combination, **each combination must result in a valid syntax tree**.
26+
27+
For example, the following code **cannot** be formatted:
28+
29+
```fsharp
30+
module F =
31+
let a: string =
32+
#if FOO
33+
""
34+
#endif
35+
#if BAR
36+
"a"
37+
#endif
38+
39+
let baz: unit = ()
40+
```
41+
42+
When neither `FOO` nor `BAR` is defined, the code becomes:
43+
44+
```fsharp
45+
module F =
46+
let a: string =
47+
48+
let baz: unit = ()
49+
```
50+
51+
This is not valid F# — `let a` has no body — so the parser raises an error and Fantomas cannot proceed.
52+
53+
## How to fix it
54+
55+
Make sure that every combination of defines still produces valid F# code. The most common fix is to add an `#else` branch:
56+
57+
```fsharp
58+
module F =
59+
let a: string =
60+
#if FOO
61+
""
62+
#else
63+
"a"
64+
#endif
65+
66+
let baz: unit = ()
67+
```
68+
69+
Now, regardless of whether `FOO` is defined, the parser always sees a complete `let` binding.
70+
71+
## Using `.fantomasignore`
72+
73+
If you cannot restructure the directives (e.g. because the code is generated or must match a particular pattern), you can exclude the file from formatting using a [`.fantomasignore`](https://fsprojects.github.io/fantomas/docs/end-users/IgnoreFiles.html) file.
74+
75+
<fantomas-nav previous="{{fsdocs-previous-page-link}}" next="{{fsdocs-next-page-link}}"></fantomas-nav>

src/Fantomas.Core/CodeFormatterImpl.fs

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,47 @@ let parse (isSignature: bool) (source: ISourceText) : Async<(ParsedInput * Defin
3333
| hashDirectives ->
3434
let defineCombinations = Defines.getDefineCombination hashDirectives
3535

36-
defineCombinations
37-
|> List.map (fun defineCombination ->
38-
async {
39-
let untypedTree, diagnostics =
40-
Fantomas.FCS.Parse.parseFile isSignature source defineCombination.Value
41-
42-
let errors =
43-
diagnostics
44-
|> List.filter (fun d -> d.Severity = FSharpDiagnosticSeverity.Error)
45-
46-
if not errors.IsEmpty then
47-
raise (ParseException diagnostics)
48-
49-
return (untypedTree, defineCombination)
50-
})
51-
|> Async.Parallel
36+
async {
37+
let! results =
38+
defineCombinations
39+
|> List.map (fun defineCombination ->
40+
async {
41+
let untypedTree, diagnostics =
42+
Fantomas.FCS.Parse.parseFile isSignature source defineCombination.Value
43+
44+
let errors =
45+
diagnostics
46+
|> List.filter (fun d -> d.Severity = FSharpDiagnosticSeverity.Error)
47+
48+
if errors.IsEmpty then
49+
return Ok(untypedTree, defineCombination)
50+
else
51+
let defineNames =
52+
if defineCombination.Value.IsEmpty then
53+
"no defines"
54+
else
55+
defineCombination.Value |> String.concat ", "
56+
57+
return Error defineNames
58+
})
59+
|> Async.Parallel
60+
61+
let failures =
62+
results
63+
|> Array.choose (function
64+
| Error name -> Some name
65+
| _ -> None)
66+
|> Array.toList
67+
68+
if not failures.IsEmpty then
69+
raise (DefineParseException(failures))
70+
71+
return
72+
results
73+
|> Array.choose (function
74+
| Ok result -> Some result
75+
| _ -> None)
76+
}
5277

5378
/// Format an abstract syntax tree using given config
5479
let formatAST

src/Fantomas.Core/FormatConfig.fs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,24 @@ open System
44
open System.ComponentModel
55
open Fantomas.FCS.Parse
66

7+
/// Raised when the F# parser produces errors for source code without conditional directives.
78
exception ParseException of diagnostics: FSharpParserDiagnostic list
89

10+
/// Raised when Fantomas encounters a problem during formatting.
911
type FormatException(msg: string) =
1012
inherit Exception(msg)
1113

14+
/// Raised when one or more conditional compilation define combinations produce invalid syntax trees.
15+
type DefineParseException(combinations: string list) =
16+
inherit
17+
FormatException(
18+
let joined = combinations |> String.concat ", "
19+
$"Parsing failed for define combination(s): %s{joined}."
20+
)
21+
22+
/// The define combinations that failed to parse.
23+
member _.Combinations = combinations
24+
1225
type Num = int
1326

1427
type MultilineFormatterType =

src/Fantomas/Program.fs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,14 @@ Join our Discord community: https://discord.gg/Cpq9vf8BJH
382382
match verbosity with
383383
| VerbosityLevel.Normal ->
384384
match exn with
385-
| :? ParseException -> "Could not parse file."
385+
| :? ParseException -> "Could not parse the file."
386+
| :? DefineParseException as dpe ->
387+
let combinations =
388+
dpe.Combinations
389+
|> List.map (fun c -> if c = "no defines" then "no defines" else $"[%s{c}]")
390+
|> String.concat ", "
391+
392+
$"When Fantomas encounters #if directives in a file, it tries to format all possible combinations of defines and will merge all different versions back into one.\nFor %s{combinations}, however, we were not able to parse the file.\nWhile you may not use this combination in your project, Fantomas requires it to produce valid code.\nConsider fixing the code or ignoring this file.\nFor more information see: https://fsprojects.github.io/fantomas/docs/end-users/ConditionalCompilationDirectives.html"
386393
| :? FormatException as fe -> fe.Message
387394
| _ -> ""
388395
| VerbosityLevel.Detailed -> $"%A{exn}"

0 commit comments

Comments
 (0)