Skip to content

Commit ca0c167

Browse files
authored
Merge pull request #3248 from nojaf/fix-3243
Fix closing > in nested multiline generic type applications
2 parents f9b435f + b57a441 commit ca0c167

File tree

5 files changed

+128
-38
lines changed

5 files changed

+128
-38
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
### Fixed
1010

11+
- Closing `>` in nested multiline generic type applications violates offside rule. [#3243](https://github.com/fsprojects/fantomas/issues/3243)
1112
- Lambda in non-last record field is now parenthesized when record is collapsed to single line to avoid producing invalid code. [#3246](https://github.com/fsprojects/fantomas/issues/3246)
1213

1314
## [8.0.0-alpha-003] - 2026-03-03

scripts/format.fsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,16 @@ let format (input: string) (isSignature: bool) (config: FormatConfig) =
88
async {
99
try
1010
let! result = CodeFormatter.FormatDocumentAsync(isSignature, input, config)
11-
return result.Code
11+
let formattedCode = result.Code
12+
13+
// Check for diagnostics in the formatted output
14+
let sourceText = Fantomas.FCS.Text.SourceText.ofString formattedCode
15+
let _, diagnostics = Fantomas.FCS.Parse.parseFile isSignature sourceText []
16+
17+
for d in diagnostics do
18+
eprintfn "Diagnostic: %A %A %s %A" d.Severity d.ErrorNumber d.Message d.Range
19+
20+
return formattedCode
1221
with ex ->
1322
return $"Error while formatting: %A{ex}"
1423
}

src/Fantomas.Core.Tests/TypeAnnotationTests.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ type CancellableTaskResultBuilderBase with
134134
'TOverall,
135135
'Error,
136136
'TResult2
137-
>)
137+
>)
138138
) : bool =
139139
true
140140
"""

src/Fantomas.Core.Tests/TypeAppTests.fs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,3 +502,88 @@ let bv =
502502
>
503503
bf
504504
"""
505+
506+
[<Test>]
507+
let ``nested multiline type app should not produce invalid code, 3243`` () =
508+
formatSourceString
509+
"""
510+
let f (x: Outer<Map<string, (A * B * C * D)>, Z>) = ()
511+
"""
512+
{ config with MaxLineLength = 30 }
513+
|> prepend newline
514+
|> should
515+
equal
516+
"""
517+
let f
518+
(x:
519+
Outer<
520+
Map<
521+
string,
522+
(A * B * C * D)
523+
>,
524+
Z
525+
>)
526+
=
527+
()
528+
"""
529+
530+
[<Test>]
531+
let ``nested multiline type app in function signature should not produce invalid code, 3243`` () =
532+
formatSourceString
533+
"""
534+
let makePipeline''
535+
(mode: ExecutionMode<Marker>)
536+
(resolver: IResolver option Marker)
537+
(args: Arguments Marker)
538+
(logging: Marker<ExtraLogging>)
539+
(processing:
540+
(PipelineStep<
541+
Map<string,
542+
((Table * IStepMetadata)
543+
* Operation seq
544+
* FileInfo ResultKind seq
545+
* (NodeInfo<string, string> * DayLogs) seq)>,
546+
PipelineCrate>)
547+
->
548+
(Map<string,
549+
((Table * IStepMetadata)
550+
* Operation seq
551+
* FileInfo ResultKind seq
552+
* (NodeInfo<string, string> * DayLogs) seq)> Marker)
553+
->
554+
PipelineStep<unit>)
555+
=
556+
()
557+
"""
558+
config
559+
|> prepend newline
560+
|> should
561+
equal
562+
"""
563+
let makePipeline''
564+
(mode: ExecutionMode<Marker>)
565+
(resolver: IResolver option Marker)
566+
(args: Arguments Marker)
567+
(logging: Marker<ExtraLogging>)
568+
(processing:
569+
(PipelineStep<
570+
Map<
571+
string,
572+
((Table * IStepMetadata) *
573+
Operation seq *
574+
FileInfo ResultKind seq *
575+
(NodeInfo<string, string> * DayLogs) seq)
576+
>,
577+
PipelineCrate
578+
>)
579+
-> (Map<
580+
string,
581+
((Table * IStepMetadata) *
582+
Operation seq *
583+
FileInfo ResultKind seq *
584+
(NodeInfo<string, string> * DayLogs) seq)
585+
> Marker)
586+
-> PipelineStep<unit>)
587+
=
588+
()
589+
"""

src/Fantomas.Core/CodePrinter.fs

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,19 +1193,20 @@ let genExpr (e: Expr) =
11931193
else
11941194
indentSepNlnUnindent f ctx
11951195

1196-
let genFuncExpr =
1197-
match node.FunctionExpr with
1198-
| Expr.TypeApp node -> genTypeApp true node
1199-
| _ -> genExpr node.FunctionExpr
1200-
1201-
(genFuncExpr
1196+
(genExpr node.FunctionExpr
12021197
+> ensureArgumentsAreNotAlignedWithFunctionName (col sepNln node.Arguments genExpr))
12031198
ctx
12041199

12051200
expressionFitsOnRestOfLine shortExpression longExpression ctx
12061201

12071202
|> genNode node
1208-
| Expr.TypeApp node -> expressionFitsOnRestOfLine (genTypeApp false node) (genTypeApp true node)
1203+
| Expr.TypeApp node ->
1204+
genPrefixApp
1205+
(genExpr node.Identifier)
1206+
node.LessThan
1207+
(colGenericTypeParameters node.TypeParameters)
1208+
node.GreaterThan
1209+
|> genNode node
12091210
| Expr.TryWithSingleClause node ->
12101211
let genClause =
12111212
let clauseNode = node.Clause
@@ -2093,26 +2094,6 @@ let genAppSingleParenArgExpr (addSpace: Context -> Context) (node: ExprAppSingle
20932094

20942095
expressionFitsOnRestOfLine short long |> genNode node
20952096

2096-
/// When called from `SynExpr.App` we need to ensure the node.GreaterThan is placed one space further than the start column.
2097-
/// This is to ensure the application remains an application.
2098-
let genTypeApp (addAdditionalColumnOffset: bool) (node: ExprTypeAppNode) (ctx: Context) : Context =
2099-
genNode
2100-
node
2101-
(fun ctx ->
2102-
// Capture startColumn inside genNode (after leading trivia/newlines are written),
2103-
// so we get the column on the actual line, not a stale column from a previous line. See #3179.
2104-
let startColumn = ctx.Column + (if addAdditionalColumnOffset then 1 else 0)
2105-
2106-
(genExpr node.Identifier
2107-
+> genSingleTextNode node.LessThan
2108-
+> colGenericTypeParameters node.TypeParameters
2109-
// we need to make sure each expression in the function application has offset at least greater than
2110-
// See: https://github.com/fsprojects/fantomas/issues/1611
2111-
+> addFixedSpaces startColumn
2112-
+> genSingleTextNode node.GreaterThan)
2113-
ctx)
2114-
ctx
2115-
21162097
let genClauses (clauses: MatchClauseNode list) =
21172098
let lastIndex = clauses.Length - 1
21182099

@@ -2377,6 +2358,24 @@ let colGenericTypeParameters typeParameters =
23772358
| [ Type.StaticConstant(Constant.FromText textNode) ] when textNode.Text.Contains("\n") -> short
23782359
| _ -> expressionFitsOnRestOfLine short long
23792360

2361+
/// In F#, a closing `>` on a new line is ambiguous with the comparison operator.
2362+
/// Formats `identifier< typeParameters >` while ensuring the closing `>` satisfies F#'s offside rule.
2363+
let genPrefixApp
2364+
(identifier: Context -> Context)
2365+
(lessThan: SingleTextNode)
2366+
(typeParameters: Context -> Context)
2367+
(greaterThan: SingleTextNode)
2368+
(ctx: Context)
2369+
: Context =
2370+
let startColumn = ctx.Column
2371+
2372+
(identifier
2373+
+> genSingleTextNode lessThan
2374+
+> typeParameters
2375+
+> addFixedSpaces (startColumn + 1)
2376+
+> genSingleTextNode greaterThan)
2377+
ctx
2378+
23802379
let genFunctionNameWithMultilineLids (trailing: Context -> Context) (longIdent: IdentListNode) =
23812380
match longIdent.Content with
23822381
| IdentifierOrDot.Ident identNode :: t ->
@@ -3254,15 +3253,11 @@ let genType (t: Type) =
32543253
| Type.Var node :: _ when String.startsWithOrdinal "^" node.Text -> sepSpace
32553254
| t :: _ -> addSpaceIfSynTypeStaticConstantHasAtSignBeforeString t
32563255

3257-
genType node.Identifier
3258-
+> optSingle genIdentListNodeWithDot node.PostIdentifier
3259-
+> genSingleTextNode node.LessThen
3260-
+> addExtraSpace
3261-
+> leadingExpressionIsMultiline (colGenericTypeParameters node.Arguments) (fun isMultiline ->
3262-
onlyIf isMultiline (!-" "))
3263-
+> addExtraSpace
3264-
// TODO: I think we need to add a space here
3265-
+> genSingleTextNode node.GreaterThan
3256+
genPrefixApp
3257+
(genType node.Identifier +> optSingle genIdentListNodeWithDot node.PostIdentifier)
3258+
node.LessThen
3259+
(addExtraSpace +> colGenericTypeParameters node.Arguments +> addExtraSpace)
3260+
node.GreaterThan
32663261
|> genNode node
32673262
| Type.StructTuple node ->
32683263
genSingleTextNode node.Keyword

0 commit comments

Comments
 (0)