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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [8.0.0-alpha-022] - 2026-08-27

### Added

- Extension members on tuple types, `type (int * int) with ...` and `type struct (int * int) with ...`, format instead of failing. The parser accepts them since [dotnet/fsharp#19602](https://github.com/dotnet/fsharp/pull/19602), and a type definition's name is now any type in the Oak tree rather than only an identifier. The tuple prints as written; no layout decision was added for it. [#3436](https://github.com/fsprojects/fantomas/pull/3436)

### Changed

- Update FCS to 'Rotate [<return: X>] attributes during binding normalization', commit 74ec4f7df70717a162d6ffd23007603cf298fb8b [#3436](https://github.com/fsprojects/fantomas/pull/3436)

## [8.0.0-alpha-021] - 2026-08-27

### Fixed
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>d05075e098278aedcea3379159504d664628a495</FCSCommitHash>
<FCSCommitHash>74ec4f7df70717a162d6ffd23007603cf298fb8b</FCSCommitHash>
</PropertyGroup>

<PropertyGroup>
Expand Down
25 changes: 25 additions & 0 deletions src/Fantomas.Core.Tests/SignatureTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,31 @@ type T with
member Foo: int
"""

[<Test>]
let ``tuple type extension member signature`` () =
formatSignatureString
"""namespace ExtensionParts

type (int * int) with
member Sum: int

type struct (int * int) with
member Sum: int
"""
config
|> prepend newline
|> should
equal
"""
namespace ExtensionParts

type (int * int) with
member Sum: int

type struct (int * int) with
member Sum: int
"""

[<Test>]
let ``comment above static member, 680`` () =
formatSignatureString
Expand Down
38 changes: 38 additions & 0 deletions src/Fantomas.Core.Tests/TypeDeclarationTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,44 @@ type System.Int32 with
member this.FromString(s: string) = System.Int32.Parse(s)
"""

[<Test>]
let ``tuple type extension`` () =
formatSourceString
"""
type (int * int) with
member this.Sum = fst this + snd this

type (string * int * bool) with
member this.Arity = 3
"""
config
|> prepend newline
|> should
equal
"""
type (int * int) with
member this.Sum = fst this + snd this

type (string * int * bool) with
member this.Arity = 3
"""

[<Test>]
let ``struct tuple type extension`` () =
formatSourceString
"""
type struct (int * int) with
member this.Sum = fst this + snd this
"""
config
|> prepend newline
|> should
equal
"""
type struct (int * int) with
member this.Sum = fst this + snd this
"""

[<Test>]
let ``auto property`` () =
formatSourceString
Expand Down
101 changes: 24 additions & 77 deletions src/Fantomas.Core/ASTTransformer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2227,73 +2227,11 @@ let (|OperatorWithStar|_|) (si: SynIdent) =
ValueSome(IdentifierOrDot.Ident(stn $"( %s{text} )" ident.idRange))
| _ -> ValueNone

/// The parser moves a `[<return: ...>]` attribute written in front of a binding out of the
/// binding's attribute list and into the arity information, where nothing prints it. Put those
/// attributes back in the list they were written in, otherwise they are dropped from the output.
let restoreRotatedReturnAttributes
(attributes: SynAttributes)
(SynValData(valInfo = SynValInfo(returnInfo = SynArgInfo(attributes = arityAttributes))))
(returnInfo: SynBindingReturnInfo option)
: SynAttributes
=
let sortAttributes (attributes: SynAttribute list) =
List.sortBy (fun (a: SynAttribute) -> a.Range.StartLine, a.Range.StartColumn) attributes

let sortAttributeLists (attributes: SynAttributes) =
List.sortBy (fun (al: SynAttributeList) -> al.Range.StartLine, al.Range.StartColumn) attributes

// A return attribute written on the return type, `let f x : [<return: Foo>] int = x`, ends up in
// the arity information as well. That one is printed by the return type node, so leave it there.
let returnTypeAttributes =
match returnInfo with
| None -> []
| Some(SynBindingReturnInfo(attributes = attributes)) ->
List.collect (fun (al: SynAttributeList) -> al.Attributes) attributes

let rotated =
arityAttributes
|> List.collect (fun al -> al.Attributes)
|> List.filter (fun a ->
not (List.exists (fun (rta: SynAttribute) -> equals rta.Range a.Range) returnTypeAttributes)
)

match rotated with
| [] -> attributes
| rotated ->
let wasWrittenIn (al: SynAttributeList) (a: SynAttribute) =
RangeHelpers.rangeContainsRange al.Range a.Range

let restored =
attributes
|> List.map (fun al ->
match List.filter (wasWrittenIn al) rotated with
| [] -> al
| inThisList ->
{ al with
Attributes = sortAttributes (al.Attributes @ inThisList)
}
)

// An attribute list that held nothing but return attributes was removed altogether, so it
// has to be recreated. Its own range is gone, the attribute range is the closest we have.
let recreated =
rotated
|> List.choose (fun a ->
if List.exists (fun al -> wasWrittenIn al a) attributes then
None
else
Some({ Attributes = [ a ]; Range = a.Range }: SynAttributeList)
)

sortAttributeLists (restored @ recreated)

let mkBinding
(creationAide: CreationAide)
(SynBinding(_, _, _, isMutable, attributes, xmlDoc, valData, pat, returnInfo, expr, _, _, trivia))
(SynBinding(_, _, _, isMutable, attributes, xmlDoc, _, pat, returnInfo, expr, _, _, trivia))
(inKeyword: SingleTextNode option)
=
let attributes = restoreRotatedReturnAttributes attributes valData returnInfo

let mkFunctionName (sli: SynLongIdent) : IdentListNode =
match sli.IdentsWithTrivia with
| [ prefix; OperatorWithStar operatorNode ] ->
Expand Down Expand Up @@ -2380,16 +2318,12 @@ let mkExternBinding
accessibility = accessibility
attributes = attributes
xmlDoc = xmlDoc
valData = valData
headPat = pat
returnInfo = returnInfo
range = range
trivia = trivia))
: ExternBindingNode
=
let attributes: SynAttributes =
restoreRotatedReturnAttributes attributes valData returnInfo

let m =
if not xmlDoc.IsEmpty then
unionRanges xmlDoc.Range pat.Range
Expand Down Expand Up @@ -2504,6 +2438,11 @@ let mkXmlDoc (px: PreXmlDoc) =
let lines = Array.map (sprintf "///%s") xmlDoc.UnprocessedLines
Some(XmlDocNode(lines, xmlDoc.Range))

let mkModuleName (SynComponentInfo(synType = synType; range = m) as info) : IdentListNode =
match synType with
| Some(SynType.LongIdent(SynLongIdent(lid, _, _))) -> mkLongIdent lid
| _ -> invariantViolationAbout m info "module name is not an identifier"

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

Expand Down Expand Up @@ -2535,7 +2474,7 @@ let mkModuleDecl (creationAide: CreationAide) (decl: SynModuleDecl) =
| SynModuleDecl.ModuleAbbrev(ident, lid, StartRange 6 (mModule, _)) ->
ModuleAbbrevNode(stn "module" mModule, mkIdent ident, mkLongIdent lid, declRange)
|> ModuleDecl.ModuleAbbrev
| SynModuleDecl.NestedModule(SynComponentInfo(ats, _, _, lid, px, _, ao, _),
| SynModuleDecl.NestedModule(SynComponentInfo(ats, _, _, _, px, _, ao, _) as info,
isRecursive,
decls,
_,
Expand All @@ -2550,7 +2489,7 @@ let mkModuleDecl (creationAide: CreationAide) (decl: SynModuleDecl) =
stn "module" mModule,
mkSynAccess ao,
isRecursive,
mkLongIdent lid,
mkModuleName info,
stn "=" mEq,
mkModuleDecls creationAide decls id,
declRange
Expand Down Expand Up @@ -3035,16 +2974,24 @@ let mkImplicitCtor
range
)

/// The name of a type definition. Besides an identifier this can be a tuple type, for
/// `type (int * int) with ...` extensions. The parser only leaves it out while recovering from a
/// missing name, and a parse error never reaches the transformation.
let mkComponentInfoName (creationAide: CreationAide) (SynComponentInfo(synType = synType; range = m) as info) : Type =
match synType with
| Some t -> mkType creationAide t
| None -> invariantViolationAbout m info "component info without a name"

let mkTypeDefn
(creationAide: CreationAide)
(SynTypeDefn(typeInfo, typeRepr, members, implicitConstructor, range, trivia))
: TypeDefn
=
let typeNameNode =
match typeInfo with
| SynComponentInfo(ats, tds, tcs, lid, px, _preferPostfix, ao, _) ->
let identifierNode = mkLongIdent lid
let mIdentifierNode = identifierNode.Range
| SynComponentInfo(ats, tds, tcs, _, px, _preferPostfix, ao, _) ->
let identifierNode = mkComponentInfoName creationAide typeInfo
let mIdentifierNode = (Type.Node identifierNode).Range

let leadingKeyword =
match trivia.LeadingKeyword with
Expand Down Expand Up @@ -3905,7 +3852,7 @@ let mkModuleSigDecl (creationAide: CreationAide) (decl: SynModuleSigDecl) =
| SynModuleSigDecl.ModuleAbbrev(ident, lid, StartRange 6 (mModule, _)) ->
ModuleAbbrevNode(stn "module" mModule, mkIdent ident, mkLongIdent lid, declRange)
|> ModuleDecl.ModuleAbbrev
| SynModuleSigDecl.NestedModule(SynComponentInfo(ats, _, _, lid, px, _, ao, _),
| SynModuleSigDecl.NestedModule(SynComponentInfo(ats, _, _, _, px, _, ao, _) as info,
isRecursive,
decls,
_,
Expand All @@ -3919,7 +3866,7 @@ let mkModuleSigDecl (creationAide: CreationAide) (decl: SynModuleSigDecl) =
stn "module" mModule,
mkSynAccess ao,
isRecursive,
mkLongIdent lid,
mkModuleName info,
stn "=" mEq,
mkModuleSigDecls creationAide decls id,
declRange
Expand All @@ -3931,9 +3878,9 @@ let mkModuleSigDecl (creationAide: CreationAide) (decl: SynModuleSigDecl) =
let mkTypeDefnSig (creationAide: CreationAide) (SynTypeDefnSig(typeInfo, typeRepr, members, range, trivia)) : TypeDefn =
let typeNameNode =
match typeInfo with
| SynComponentInfo(ats, tds, tcs, lid, px, _preferPostfix, ao, _) ->
let identifierNode = mkLongIdent lid
let mIdentifierNode = identifierNode.Range
| SynComponentInfo(ats, tds, tcs, _, px, _preferPostfix, ao, _) ->
let identifierNode = mkComponentInfoName creationAide typeInfo
let mIdentifierNode = (Type.Node identifierNode).Range

let leadingKeyword =
match trivia.LeadingKeyword with
Expand Down
6 changes: 3 additions & 3 deletions src/Fantomas.Core/CodePrinter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4291,7 +4291,7 @@ let genImplicitConstructor (node: ImplicitConstructorNode) =
)
node.Self

let hasTriviaAfterLeadingKeyword (identifier: IdentListNode) (accessibility: SingleTextNode option) =
let hasTriviaAfterLeadingKeyword (identifier: Node) (accessibility: SingleTextNode option) =
let beforeAccess =
match accessibility with
| Some n -> n.HasContentBefore
Expand All @@ -4310,7 +4310,7 @@ let genTypeDefn (td: TypeDefn) =

// Workaround for https://github.com/fsprojects/fantomas/issues/628
let hasTriviaAfterLeadingKeyword =
hasTriviaAfterLeadingKeyword typeName.Identifier typeName.Accessibility
hasTriviaAfterLeadingKeyword (Type.Node typeName.Identifier) typeName.Accessibility

genXml typeName.XmlDoc
+> onlyIfNot hasAndKeyword (genAttributes typeName.Attributes)
Expand All @@ -4319,7 +4319,7 @@ let genTypeDefn (td: TypeDefn) =
+> onlyIf hasAndKeyword (sepSpace +> genOnelinerAttributes typeName.Attributes)
+> sepSpace
+> genAccessOpt typeName.Accessibility
+> genTypeAndParam (genIdentListNode typeName.Identifier) typeName.TypeParameters
+> genTypeAndParam (genType typeName.Identifier) typeName.TypeParameters
+> onlyIfNot typeName.Constraints.IsEmpty (sepSpace +> genTypeConstraints typeName.Constraints)
+> onlyIf hasTriviaAfterLeadingKeyword unindent
+> leadingExpressionIsMultiline
Expand Down
4 changes: 2 additions & 2 deletions src/Fantomas.Core/SyntaxOak.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2598,7 +2598,7 @@ type TypeNameNode
attributes: MultipleAttributeListNode option,
leadingKeyword: SingleTextNode,
ao: SingleTextNode option,
identifier: IdentListNode,
identifier: Type,
typeParams: TyparDecls option,
constraints: TypeConstraint list,
implicitConstructor: ImplicitConstructorNode option,
Expand All @@ -2615,7 +2615,7 @@ type TypeNameNode
yield! noa attributes
yield leadingKeyword
yield! noa ao
yield identifier
yield Type.Node identifier
yield! noa (Option.map TyparDecls.Node typeParams)
yield! List.map TypeConstraint.Node constraints
yield! noa implicitConstructor
Expand Down