Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# Changelog

## [Unreleased]
## [8.0.5] - 2026-09-25

### Changed

- Improved formatting performance. Formatting the source files of dotnet/fsharp allocates around 40% less memory and is roughly 1.4 times faster than `8.0.4`, depending on hardware. The fields of `WriterEvent`, returned by `CodeFormatter.GetWriterEventsAsync`, are renamed so that cases share them: `text` for the strings, `amount` for `IndentBy` and `UnIndentBy`, `column` for the indent and column cases, and `nodeType` and `range` for `NodeEnd`. [#3499](https://github.com/fsprojects/fantomas/pull/3499)
- Update FCS to 'Record the range of the `exception`in SynExceptionDefnRepr', commit 349d6aaf1e4685a72e43b7f4a0708205e4104b4c [#3500](https://github.com/fsprojects/fantomas/pull/3500)

### Fixed

- Comment between doc comment and exception keyword gets shifted. The syntax tree has no range for the `exception` keyword, so a comment between the XML doc or attributes and the keyword was placed in front of the exception name, after the keyword. The keyword is now looked up in the source text and the comment stays in front of it. [#3483](https://github.com/fsprojects/fantomas/issues/3483)
- Comment between doc comment and exception keyword gets shifted. The syntax tree had no range for the `exception` keyword, so a comment between the XML doc or attributes and the keyword was placed in front of the exception name, after the keyword. The parser now records that range, and the comment stays in front of the keyword. [#3483](https://github.com/fsprojects/fantomas/issues/3483)

## [8.0.4] - 2026-09-23

Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Some common use cases include:

<!-- Versions -->
<PropertyGroup>
<FCSCommitHash>74ec4f7df70717a162d6ffd23007603cf298fb8b</FCSCommitHash>
<FCSCommitHash>349d6aaf1e4685a72e43b7f4a0708205e4104b4c</FCSCommitHash>
</PropertyGroup>

<PropertyGroup>
Expand Down
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
<PackageVersion Include="FsLexYacc" Version="13.0.0" />

<!-- Hosts the vendored FSharpEmbedResourceText MSBuild task, see Fantomas.FCS.BuildTasks. -->
<PackageVersion Include="Microsoft.Build.Framework" Version="18.0.2" />
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="18.0.2" />
<PackageVersion Include="Microsoft.Build.Framework" Version="18.10.1" />
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="18.10.1" />

<PackageVersion Include="Ionide.KeepAChangelog.Tasks" Version="0.3.3"/>
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="2.0.5"/>
Expand Down
2 changes: 2 additions & 0 deletions build.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@ pipeline "Init" {
// Not a compiler source. This is the MSBuild task that turns FSComp.txt into the SR
// module. Since dotnet/fsharp#20097 the generated diagnostic accessors return RichText
// instead of string, and the task shipped in the .NET SDK cannot generate those yet.
// Since dotnet/fsharp#20506 it resolves its paths through TaskEnvironmentPaths.
"src/FSharp.Build/TaskEnvironmentPaths.fs"
"src/FSharp.Build/FSharpEmbedResourceText.fs"
"src/Compiler/FSComp.txt"
"src/Compiler/FSStrings.resx"
Expand Down
131 changes: 13 additions & 118 deletions src/Fantomas.Core/ASTTransformer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,6 @@ let longIdentText (sli: SynLongIdent) : string =
type CreationAide =
{
SourceText: ISourceText option
/// The ranges of the line and block comments in the source.
/// Only `mkExceptionKeyword` needs these, to find the `exception` keyword in the source text.
/// Remove this field once the syntax tree has a range for that keyword.
CodeComments: range list
}

member x.TextFromSource fallback range =
Expand Down Expand Up @@ -2502,99 +2498,22 @@ let mkModuleName (SynComponentInfo(synType = synType; range = m) as info) : Iden
| Some(SynType.LongIdent(SynLongIdent(lid, _, _))) -> mkLongIdent lid
| _ -> invariantViolationAbout m info "module name is not an identifier"

/// The `exception` keyword of an exception definition.
///
/// The syntax tree has no range for it, and the range of the definition starts at its XML doc.
/// It is looked up in the source text, starting after the XML doc and attributes and skipping over
/// the comments the parser found in between, so that a comment there stays in front of the keyword.
/// Without source text, or when anything else comes first, it is placed where the search started.
let mkExceptionKeyword
(creationAide: CreationAide)
(declRange: range)
(xmlDoc: XmlDocNode option)
(attributes: MultipleAttributeListNode option)
(nextStart: pos)
: SingleTextNode
=
let keyword: string = "exception"

let searchStart: pos =
match attributes, xmlDoc with
| Some attributes, _ -> attributes.Range.End
| None, Some xmlDoc -> xmlDoc.Range.End
| None, None -> declRange.Start

let comments: range list =
creationAide.CodeComments
|> List.filter (fun m -> Position.posGeq m.Start searchStart && Position.posLt m.Start nextStart)

let isIdentChar (c: char) : bool =
System.Char.IsLetterOrDigit c || c = '_' || c = '\''

let isKeywordAt (text: string) (column: int) : bool =
let after: int = column + keyword.Length

System.String.CompareOrdinal(text, column, keyword, 0, keyword.Length) = 0
&& (after = text.Length || not (isIdentChar text[after]))

let find (sourceText: ISourceText) : pos =
let rec scan (p: pos) : pos =
if p.Line > sourceText.GetLineCount() || Position.posGeq p nextStart then
// reached the accessibility or the name without finding the keyword
searchStart
else

let text: string = sourceText.GetLineString(p.Line - 1)

if p.Column >= text.Length then
// end of the line, continue on the next one
scan (Position.mkPos (p.Line + 1) 0)
elif System.Char.IsWhiteSpace text[p.Column] then
// skip whitespace
scan (Position.mkPos p.Line (p.Column + 1))
else

match List.tryFind (fun (m: range) -> Position.posEq m.Start p) comments with
// jump over the comment
| Some comment -> scan comment.End
// the first code after the attributes or XML doc has to be the keyword
| None -> if isKeywordAt text p.Column then p else searchStart

scan searchStart

let start: pos =
match creationAide.SourceText with
| None -> searchStart
| Some sourceText -> find sourceText

stn keyword (mkRange declRange.FileName start (Position.mkPos start.Line (start.Column + keyword.Length)))

let mkModuleDecl (creationAide: CreationAide) (decl: SynModuleDecl) =
let declRange = decl.Range

match decl with
| SynModuleDecl.Expr(e, _) -> mkExpr creationAide e |> ModuleDecl.DeclExpr
| SynModuleDecl.Exception(SynExceptionDefn(SynExceptionDefnRepr(attrs, caseName, _, xmlDoc, vis, _),
| SynModuleDecl.Exception(SynExceptionDefn(SynExceptionDefnRepr(attrs, caseName, _, xmlDoc, vis, _, trivia),
withKeyword,
ms,
_),
_) ->
let xmlDoc: XmlDocNode option = mkXmlDoc xmlDoc
let attributes: MultipleAttributeListNode option = mkAttributes creationAide attrs
let accessibility: SingleTextNode option = mkSynAccess vis
let unionCase: UnionCaseNode = mkSynUnionCase creationAide caseName

let nextStart: pos =
accessibility
|> Option.map (fun (n: SingleTextNode) -> n.Range.Start)
|> Option.defaultValue unionCase.Range.Start

ExceptionDefnNode(
xmlDoc,
attributes,
mkExceptionKeyword creationAide declRange xmlDoc attributes nextStart,
accessibility,
unionCase,
mkXmlDoc xmlDoc,
mkAttributes creationAide attrs,
stn "exception" trivia.ExceptionKeyword,
mkSynAccess vis,
mkSynUnionCase creationAide caseName,
Option.map (stn "with") withKeyword,
List.map (mkMemberDefn creationAide) ms,
declRange
Expand Down Expand Up @@ -3978,27 +3897,17 @@ let mkModuleSigDecl (creationAide: CreationAide) (decl: SynModuleSigDecl) =
let declRange = decl.Range

match decl with
| SynModuleSigDecl.Exception(SynExceptionSig(SynExceptionDefnRepr(attrs, caseName, _, xmlDoc, vis, _),
| SynModuleSigDecl.Exception(SynExceptionSig(SynExceptionDefnRepr(attrs, caseName, _, xmlDoc, vis, _, trivia),
withKeyword,
ms,
_),
_) ->
let xmlDoc: XmlDocNode option = mkXmlDoc xmlDoc
let attributes: MultipleAttributeListNode option = mkAttributes creationAide attrs
let accessibility: SingleTextNode option = mkSynAccess vis
let unionCase: UnionCaseNode = mkSynUnionCase creationAide caseName

let nextStart: pos =
accessibility
|> Option.map (fun (n: SingleTextNode) -> n.Range.Start)
|> Option.defaultValue unionCase.Range.Start

ExceptionDefnNode(
xmlDoc,
attributes,
mkExceptionKeyword creationAide declRange xmlDoc attributes nextStart,
accessibility,
unionCase,
mkXmlDoc xmlDoc,
mkAttributes creationAide attrs,
stn "exception" trivia.ExceptionKeyword,
mkSynAccess vis,
mkSynUnionCase creationAide caseName,
Option.map (stn "with") withKeyword,
List.map (mkMemberSig creationAide) ms,
declRange
Expand Down Expand Up @@ -4414,21 +4323,7 @@ let mkFullTreeRange ast =
includeTrivia astRange trivia

let mkOak (sourceText: ISourceText option) (ast: ParsedInput) =
let codeComments: range list =
match ast with
| ParsedInput.ImplFile(ParsedImplFileInput(trivia = { CodeComments = codeComments }))
| ParsedInput.SigFile(ParsedSigFileInput(trivia = { CodeComments = codeComments })) -> codeComments
|> List.map (
function
| CommentTrivia.LineComment m
| CommentTrivia.BlockComment m -> m
)

let creationAide: CreationAide =
{
SourceText = sourceText
CodeComments = codeComments
}
let creationAide: CreationAide = { SourceText = sourceText }

let fullRange = mkFullTreeRange ast

Expand Down
19 changes: 19 additions & 0 deletions src/Fantomas.FCS.BuildTasks/FSharpEnvironment.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Internal.Utilities

open System.Runtime.InteropServices

/// The part of the upstream FSharpEnvironment (src/Compiler/Facilities/CompilerLocation.fs) that the
/// vendored TaskEnvironmentPaths.fs refers to. The real module needs the generated UtilsStrings and
/// FSharp.BuildProperties, which this project does not have.
module internal FSharpEnvironment =

let isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)

/// Only TaskEnvironmentPaths.defaultCompilerToolPath calls this, for the Fsc and Fsi tasks, and
/// FSharpEmbedResourceText never does.
let BinFolderOfDefaultFSharpCompilerUsingEnvironment
(_getEnvironmentVariable: string -> string | null)
(_probePoint: string option)
: string option
=
None
10 changes: 9 additions & 1 deletion src/Fantomas.FCS.BuildTasks/Fantomas.FCS.BuildTasks.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
The task shipped in the .NET SDK is older than the compiler sources we vendor: since
dotnet/fsharp#20097 the generated diagnostic accessors return RichText instead of string, and the
SDK's task cannot generate those. Upstream solves this by bootstrapping its own FSharp.Build, we
solve it by vendoring the one file and compiling it here.
solve it by vendoring the task, and the files it depends on, and compiling them here.
FSharpEnvironment.fs stands in for the upstream module of that name, see the file.

This is build tooling. It is never packed and never referenced at runtime.
-->
Expand All @@ -27,6 +28,13 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="..\..\.deps\$(FCSCommitHash)\src\Compiler\Utilities\NullHelpers.fs">
<Link>NullHelpers.fs</Link>
</Compile>
<Compile Include="FSharpEnvironment.fs" />
<Compile Include="..\..\.deps\$(FCSCommitHash)\src\FSharp.Build\TaskEnvironmentPaths.fs">
<Link>TaskEnvironmentPaths.fs</Link>
</Compile>
<Compile Include="..\..\.deps\$(FCSCommitHash)\src\FSharp.Build\FSharpEmbedResourceText.fs">
<Link>FSharpEmbedResourceText.fs</Link>
</Compile>
Expand Down
Loading