Conditional Compilation Directives
Fantomas supports formatting F# code that contains conditional compilation directives (#if, #else, #endif).
However, there is an important limitation to be aware of.
How Fantomas handles directives
Fantomas needs to parse your code into an abstract syntax tree (AST) before it can format it.
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.
To handle this, Fantomas:
- Parses your code without any defines to discover all conditional directives.
- Determines every possible combination of defines.
- Parses and formats the code once for each combination.
- Merges the results back together.
Limitations
All define combinations must produce valid syntax
Because Fantomas parses your code under every define combination, each combination must result in a valid syntax tree.
For example, the following code cannot be formatted:
module F =
let a: string =
#if FOO
""
#endif
#if BAR
"a"
#endif
let baz: unit = ()
When neither FOO nor BAR is defined, the code becomes:
module F =
let a: string =
let baz: unit = ()
This is not valid F# — let a has no body — so the parser raises an error and Fantomas cannot proceed.
How to fix it
Make sure that every combination of defines still produces valid F# code. The most common fix is to add an #else branch:
module F =
let a: string =
#if FOO
""
#else
"a"
#endif
let baz: unit = ()
Now, regardless of whether FOO is defined, the parser always sees a complete let binding.
Interweaved documentation
If you interweave conditional directives with xml documentation or other trivia you will encounter issues:
type Foo =
/// Will give you an option.
#if DEBUG
/// Default returns Some
#else
/// Default returns None
#endif
static member bar (?giveSome: bool) =
let giveSome =
#if DEBUG
true
#else
false
#endif
if giveSome then Some() else None
How to fix it
Wrap the documentation/trivia in its entirety.
type Foo =
#if DEBUG
/// Will give you an option.
/// Default returns Some
#else
/// Will give you an option.
/// Default returns None
#endif
static member bar (?giveSome: bool) =
let giveSome =
#if DEBUG
true
#else
false
#endif
if giveSome then Some() else None
Nesting that depends on a define
Fantomas merges the formatted combinations back together line by line, so the code after an #endif
gets one indentation, whichever branch was active. When a construct that opens a body appears in only
one branch, the code after #endif is nested inside that construct in one combination and not in the
other, and no single indentation is right for both:
let traverse entity =
seq {
#if !NO_TYPEPROVIDERS
if not entity.IsProvided then
#endif
yield! children entity
}
With NO_TYPEPROVIDERS defined, yield! is a statement of the seq; without it, it is the body of the
if. Fantomas cannot format this.
How to fix it
Move the condition into a value, so that the code after it is nested the same way in every combination:
let traverse entity =
seq {
let isProvided =
#if !NO_TYPEPROVIDERS
entity.IsProvided
#else
false
#endif
if not isProvided then
yield! children entity
}
Using .fantomasignore
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 file.
from ConditionalCompilationDirectives
val string: value: 'T -> string
--------------------
type string = System.String
static member bar: ?giveSome: bool -> unit option
val seq: sequence: 'T seq -> 'T seq
--------------------
type 'T seq = System.Collections.Generic.IEnumerable<'T>
fantomas