From 24c2341335237bedc0bca8555957b835b86f88a6 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 10 Apr 2026 10:23:50 +0200 Subject: [PATCH 01/14] Fix recursive module do-binding leaking compiler-generated val in signature Filter out compiler-generated vals with '@' in their LogicalName from inferred signature printing. In 'module rec' contexts, 'do ()' bindings are compiled as TMDefRec with vals named like 'doval@3' that leaked into generated signatures. Fixes #13832 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/NicePrint.fs | 2 +- .../Signatures/ModuleOrNamespaceTests.fs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Compiler/Checking/NicePrint.fs b/src/Compiler/Checking/NicePrint.fs index 62c95f6bbf7..6db4d0b5737 100644 --- a/src/Compiler/Checking/NicePrint.fs +++ b/src/Compiler/Checking/NicePrint.fs @@ -2551,7 +2551,7 @@ module InferredSigPrinting = let rec imdefsL denv x = aboveListL (x |> List.map (imdefL denv)) and imdefL denv x = - let filterVal (v: Val) = not v.IsCompilerGenerated && Option.isNone v.MemberInfo + let filterVal (v: Val) = not v.IsCompilerGenerated && Option.isNone v.MemberInfo && not (v.LogicalName.Contains("@")) let filterExtMem (v: Val) = v.IsExtensionMember match x with diff --git a/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs b/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs index f7575dc7d0f..2d344ea6bb1 100644 --- a/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs @@ -183,6 +183,18 @@ do () namespace Foo namespace Bar""" +// https://github.com/dotnet/fsharp/issues/13832 +[] +let ``Recursive module with do binding`` () = + FSharp + """ +module rec Foobar + +do () +""" + |> printSignatures + |> assertEqualIgnoreLineEnding "\nmodule Foobar" + [] let ``Empty namespace module`` () = FSharp From 7fc0698bc2d2333f839e5a54b359cd468f95ee7b Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 10 Apr 2026 10:58:23 +0200 Subject: [PATCH 02/14] Fix literal values in attribute arguments during signature generation When generating signatures, attribute arguments that reference [] values now preserve the literal identifier name instead of showing the evaluated constant value. For example, [] is preserved instead of being reduced to []. The fix works by recovering literal val references from the syntax tree during attribute checking: TcVal inlines literals to Expr.Const, but we now look up the original identifier in the name resolution environment and store Expr.Val as the source expression in AttribExpr. The Expr.Val case is then handled in layoutAttribArg in NicePrint.fs to display the literal name. Fixes #13810 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/CheckDeclarations.fs | 2 +- .../Checking/Expressions/CheckExpressions.fs | 27 +++++++++++++- src/Compiler/Checking/NicePrint.fs | 8 +++-- .../Signatures/ModuleOrNamespaceTests.fs | 36 +++++++++++++++++++ 4 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/Compiler/Checking/CheckDeclarations.fs b/src/Compiler/Checking/CheckDeclarations.fs index 3d80f056de0..149f8217e96 100644 --- a/src/Compiler/Checking/CheckDeclarations.fs +++ b/src/Compiler/Checking/CheckDeclarations.fs @@ -5900,7 +5900,7 @@ let CheckOneImplFile // Warn on version attributes. topAttrs.assemblyAttrs |> List.iter (function - | Attrib(tref, _, [ AttribExpr(Expr.Const (Const.String version, range, _), _) ], _, _, _, _) -> + | Attrib(tref, _, [ AttribExpr(_, Expr.Const (Const.String version, range, _)) ], _, _, _, _) -> let attrName = tref.CompiledRepresentationForNamedType.FullName let isValid() = try parseILVersion version |> ignore; true diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index ff9f3c89bf0..2271cbe0b6b 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -11628,8 +11628,33 @@ and TcAttributeEx canFail (cenv: cenv) (env: TcEnv) attrTgt attrEx (synAttr: Syn UnifyTypes cenv env mAttr ty (tyOfExpr g expr) + // Collect literal val references from the syntax expression to recover original names. + // TcVal inlines literal vals to Expr.Const, losing the original val reference. + // We recover it here by matching ranges from the syntax tree against the checked expression. + let literalIdents = + let rec collect (synExpr: SynExpr) acc = + match synExpr with + | SynExpr.Ident ident -> + match env.NameEnv.eUnqualifiedItems |> Map.tryFind ident.idText with + | Some(Item.Value vref) when vref.LiteralValue.IsSome -> + (ident.idRange, vref) :: acc + | _ -> acc + | SynExpr.Paren(expr = inner) -> collect inner acc + | SynExpr.Tuple(exprs = exprs) -> List.fold (fun a e -> collect e a) acc exprs + | _ -> acc + + collect arg [] + let mkAttribExpr e = - AttribExpr(e, EvalLiteralExprOrAttribArg g e) + let sourceExpr = + match e with + | Expr.Const(_, m, _) -> + match literalIdents |> List.tryFind (fun (r, _) -> Range.equals r m) with + | Some(_, vref) -> Expr.Val(vref, NormalValUse, m) + | None -> e + | _ -> e + + AttribExpr(sourceExpr, EvalLiteralExprOrAttribArg g e) let checkPropSetterAttribAccess m (pinfo: PropInfo) = let setterMeth = pinfo.SetterMethod diff --git a/src/Compiler/Checking/NicePrint.fs b/src/Compiler/Checking/NicePrint.fs index 6db4d0b5737..d13ccba9df1 100644 --- a/src/Compiler/Checking/NicePrint.fs +++ b/src/Compiler/Checking/NicePrint.fs @@ -561,8 +561,10 @@ module PrintTypes = /// Layout a single attribute arg, following the cases of 'gen_attr_arg' in ilxgen.fs /// This is the subset of expressions we display in the NicePrint pretty printer /// See also dataExprL - there is overlap between these that should be removed - let rec layoutAttribArg denv arg = - match arg with + let rec layoutAttribArg denv arg = + match arg with + | Expr.Val (vref, _, _) when vref.LiteralValue.IsSome -> wordL (tagLocal vref.DisplayName) + | Expr.Const (c, _, ty) -> if isEnumTy denv.g ty then WordL.keywordEnum ^^ angleL (layoutType denv ty) ^^ bracketL (layoutConst denv.g ty c) @@ -2551,7 +2553,7 @@ module InferredSigPrinting = let rec imdefsL denv x = aboveListL (x |> List.map (imdefL denv)) and imdefL denv x = - let filterVal (v: Val) = not v.IsCompilerGenerated && Option.isNone v.MemberInfo && not (v.LogicalName.Contains("@")) + let filterVal (v: Val) = not v.IsCompilerGenerated && Option.isNone v.MemberInfo && not (IsCompilerGeneratedName v.LogicalName) let filterExtMem (v: Val) = v.IsExtensionMember match x with diff --git a/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs b/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs index 2d344ea6bb1..5cf5389d23b 100644 --- a/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs @@ -262,3 +262,39 @@ namespace FSharp.MyStuff module Library = val batch: s: FSharp.Control.AsyncSeq<'t> -> FSharp.Control.AsyncSeq<'t>""" + +// https://github.com/dotnet/fsharp/issues/13810 +[] +let ``Literal value in attribute uses literal name`` () = + FSharp + """ +module Maybe + +open System.ComponentModel + +[] +let A = "A" + +module SubModule = + type Foo() = + [] + member this.Meh () = () +""" + |> printSignatures + |> prependNewline + |> assertEqualIgnoreLineEnding + """ + +module Maybe + +[] +val A: string = "A" + +module SubModule = + + type Foo = + + new: unit -> Foo + + [] + member Meh: unit -> unit""" From b1eb92d989e6cb33e5fc1f5367560cb1f58047a7 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 10 Apr 2026 12:48:27 +0200 Subject: [PATCH 03/14] Fix struct signature missing NoComparison/NoEquality attributes (#15339) When generating signatures for struct types whose fields prevent comparison/equality augmentation (e.g., fields of type obj), the generated signature now includes [] and [] attributes. Without these, the signature fails to compile with FS0293. The fix detects when a struct type is a candidate for comparison/equality augmentation but augmentation was not generated (GeneratedCompareToValues or GeneratedHashAndEqualsValues is None), and injects synthetic attributes into the signature output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/NicePrint.fs | 45 ++++++++++++++- .../Signatures/TypeTests.fs | 57 ++++++++++++++++++- 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/src/Compiler/Checking/NicePrint.fs b/src/Compiler/Checking/NicePrint.fs index d13ccba9df1..1a9e137d3be 100644 --- a/src/Compiler/Checking/NicePrint.fs +++ b/src/Compiler/Checking/NicePrint.fs @@ -1988,6 +1988,47 @@ module TastDefinitionPrinting = let isMeasure = (tycon.TypeOrMeasureKind = TyparKind.Measure) let ty = generalizedTyconRef g tcref + // Augment tycon.Attribs with synthetic [] / [] when the type + // is a candidate for comparison/equality augmentation but augmentation was not generated + // (e.g. struct with non-comparable fields). This ensures generated signatures compile. (#15339) + let augmentedAttribs = + let isTrueFSharpStruct = + tycon.IsFSharpStructOrEnumTycon && not tycon.IsFSharpEnumTycon + + // Only structs need synthetic NoComparison/NoEquality in signatures. + // Reference types (records, unions) compile fine without them. + let canBeAugmentedWithCompare = isTrueFSharpStruct + let canBeAugmentedWithEquals = isTrueFSharpStruct + + let mkSyntheticCoreAttrib (attrName: string) = + let fsharpCorePath = [| "Microsoft"; "FSharp"; "Core" |] + let attrTcref = mkNonLocalTyconRef (mkNonLocalEntityRef g.fslibCcu fsharpCorePath) attrName + + let ilTypeRef = + ILTypeRef.Create(g.ilg.fsharpCoreAssemblyScopeRef, [], "Microsoft.FSharp.Core." + attrName) + + let ilMethodRef = + ILMethodRef.Create(ilTypeRef, ILCallingConv.Instance, ".ctor", 0, [], ILType.Void) + + Attrib(attrTcref, ILAttrib ilMethodRef, [], [], false, None, Range.range0) + + let mutable attribs = tycon.Attribs + + if canBeAugmentedWithCompare + && not (EntityHasWellKnownAttribute g WellKnownEntityAttributes.NoComparisonAttribute tycon) + && not (EntityHasWellKnownAttribute g WellKnownEntityAttributes.CustomComparisonAttribute tycon) + && tycon.GeneratedCompareToValues.IsNone then + attribs <- mkSyntheticCoreAttrib "NoComparisonAttribute" :: attribs + + if canBeAugmentedWithEquals + && not (EntityHasWellKnownAttribute g WellKnownEntityAttributes.NoEqualityAttribute tycon) + && not (EntityHasWellKnownAttribute g WellKnownEntityAttributes.CustomEqualityAttribute tycon) + && not (EntityHasWellKnownAttribute g WellKnownEntityAttributes.ReferenceEqualityAttribute tycon) + && tycon.GeneratedHashAndEqualsValues.IsNone then + attribs <- mkSyntheticCoreAttrib "NoEqualityAttribute" :: attribs + + attribs + let start, tagger = if isStructTy g ty && not tycon.TypeAbbrev.IsSome then // Always show [] whether verbose or not @@ -2011,7 +2052,7 @@ module TastDefinitionPrinting = if isFirstType then WordL.keywordType else - wordL (tagKeyword "and") ^^ layoutAttribs denv start false tycon.TypeOrMeasureKind tycon.Attribs emptyL + wordL (tagKeyword "and") ^^ layoutAttribs denv start false tycon.TypeOrMeasureKind augmentedAttribs emptyL let nameL = ConvertLogicalNameToDisplayLayout (tagger >> mkNav tycon.DefinitionRange >> wordL) tycon.DisplayNameCore @@ -2386,7 +2427,7 @@ module TastDefinitionPrinting = |> addLhs typeDeclL - |> fun tdl -> if isFirstType then layoutAttribs denv start false tycon.TypeOrMeasureKind tycon.Attribs tdl else tdl + |> fun tdl -> if isFirstType then layoutAttribs denv start false tycon.TypeOrMeasureKind augmentedAttribs tdl else tdl |> layoutXmlDocOfEntity denv infoReader tcref // Layout: exception definition diff --git a/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs b/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs index d01eb764bc8..526d1ceaaf2 100644 --- a/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs @@ -440,4 +440,59 @@ type ExplicitClass = |> ignoreWarnings |> compile |> shouldSucceed - |> ignore \ No newline at end of file + |> ignore + +// https://github.com/dotnet/fsharp/issues/15339 +[] +let ``Struct with non-comparable field includes NoComparison in signature`` () = + let implSource = + """ +namespace FSInteractive + +open System + +[] +type LocalReadWriteLockCookie(locker: obj) = + interface IDisposable with + member _.Dispose () = () +""" + + let generatedSignature = + FSharp implSource |> printSignatures + + Assert.Contains("NoComparison", generatedSignature) + + Fsi generatedSignature + |> withAdditionalSourceFile (FsSource implSource) + |> withOptions [ "--warnaserror:64" ] + |> ignoreWarnings + |> compile + |> shouldSucceed + |> ignore + +// https://github.com/dotnet/fsharp/issues/15339 +[] +let ``Struct with non-equatable field includes NoEquality in signature`` () = + let implSource = + """ +namespace TestNs + +[] +type StructWithFunc = + val Fn: int -> int + new(f) = { Fn = f } +""" + + let generatedSignature = + FSharp implSource |> printSignatures + + Assert.Contains("NoComparison", generatedSignature) + Assert.Contains("NoEquality", generatedSignature) + + Fsi generatedSignature + |> withAdditionalSourceFile (FsSource implSource) + |> withOptions [ "--warnaserror:64" ] + |> ignoreWarnings + |> compile + |> shouldSucceed + |> ignore From 4025af18ac2e9e25f3c006ef1ec9ee373a95c7f8 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 10 Apr 2026 14:40:57 +0200 Subject: [PATCH 04/14] Fix backtick escaping in AddBackticksToIdentifierIfNeeded (#15389) Change single-backtick checks to double-backtick checks so identifiers containing backticks in their names get properly escaped with `` `` `` delimiters during signature generation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/SyntaxTree/PrettyNaming.fs | 4 ++-- .../Signatures/ModuleOrNamespaceTests.fs | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Compiler/SyntaxTree/PrettyNaming.fs b/src/Compiler/SyntaxTree/PrettyNaming.fs index 7f3e4edfe58..922d9d9009f 100755 --- a/src/Compiler/SyntaxTree/PrettyNaming.fs +++ b/src/Compiler/SyntaxTree/PrettyNaming.fs @@ -515,8 +515,8 @@ let DoesIdentifierNeedBackticks (name: string) : bool = let AddBackticksToIdentifierIfNeeded (name: string) : string = if DoesIdentifierNeedBackticks name - && not (name.StartsWithOrdinal("`")) - && not (name.EndsWithOrdinal("`")) + && not (name.StartsWithOrdinal("``")) + && not (name.EndsWithOrdinal("``")) then "``" + name + "``" else diff --git a/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs b/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs index 5cf5389d23b..db35c217d75 100644 --- a/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs @@ -298,3 +298,21 @@ module SubModule = [] member Meh: unit -> unit""" + +// https://github.com/dotnet/fsharp/issues/15389 +[] +let ``Backtick in identifier is properly escaped in signature`` () = + FSharp + """ +module Foo + +let ```a` b`` (a:int) (b:int) = () +""" + |> printSignatures + |> prependNewline + |> assertEqualIgnoreLineEnding + """ + +module Foo + +val ```a` b`` : a: int -> b: int -> unit""" From 3a327fa0455929cc6a223f31c215f174c7e1ed4c Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 10 Apr 2026 15:00:53 +0200 Subject: [PATCH 05/14] Fix private keyword placement in prefix-style type abbreviation signatures (#15560) Move layoutAccessibility call after layoutTyparDecls so that 'private' is placed before the entire ''a P' construct rather than between the type parameter and name (producing 'type 'a private P' instead of the correct 'type private 'a P'). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/NicePrint.fs | 6 +++--- .../Signatures/TypeTests.fs | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Compiler/Checking/NicePrint.fs b/src/Compiler/Checking/NicePrint.fs index 1a9e137d3be..3f6d76860fc 100644 --- a/src/Compiler/Checking/NicePrint.fs +++ b/src/Compiler/Checking/NicePrint.fs @@ -2056,14 +2056,14 @@ module TastDefinitionPrinting = let nameL = ConvertLogicalNameToDisplayLayout (tagger >> mkNav tycon.DefinitionRange >> wordL) tycon.DisplayNameCore - let nameL = layoutAccessibility denv tycon.Accessibility nameL - let denv = denv.AddAccessibility tycon.Accessibility - let lhsL = let tps = tycon.TyparsNoRange let tpsL = layoutTyparDecls denv nameL tycon.IsPrefixDisplay tps + let tpsL = layoutAccessibility denv tycon.Accessibility tpsL typewordL ^^ tpsL + let denv = denv.AddAccessibility tycon.Accessibility + let sortKey (minfo: MethInfo) = (not minfo.IsConstructor, diff --git a/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs b/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs index 526d1ceaaf2..b1e22705d3a 100644 --- a/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs @@ -496,3 +496,23 @@ type StructWithFunc = |> compile |> shouldSucceed |> ignore + +// https://github.com/dotnet/fsharp/issues/15560 +[] +let ``Private type abbreviation with prefix type parameter`` () = + let signatures = + FSharp + """ +module Foo + +type P<'a> = class end + +[] +[] +module R = + type private 'a P = unit -> 'a +""" + |> printSignatures + + Assert.Contains("type private 'a P", signatures) + Assert.DoesNotContain("type 'a private P", signatures) From d08967dc3fe6ceecaf607ec2cd9ddb283e3de891 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 10 Apr 2026 15:40:37 +0200 Subject: [PATCH 06/14] Fix missing [] attribute in signatures for types without visible constructors When generating signatures for class types whose constructors are not visible (e.g., private constructors), the [] attribute was not emitted. This caused the generated signatures to fail compilation with error FS0938 because the compiler cannot infer that the type is a class from the signature alone. The fix: 1. Always set start=Some "class" for class types regardless of printVerboseSignatures setting (was gated behind it before) 2. Suppress [] when allDecls is empty since the repr layout already produces 'class end' for empty classes 3. Remove commented-out conditions that were never active Fixes dotnet/fsharp#16531 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/NicePrint.fs | 11 +-- .../Signatures/ModuleOrNamespaceTests.fs | 2 + .../Signatures/TypeTests.fs | 79 +++++++++++++++++++ 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/src/Compiler/Checking/NicePrint.fs b/src/Compiler/Checking/NicePrint.fs index 3f6d76860fc..db0c5c95360 100644 --- a/src/Compiler/Checking/NicePrint.fs +++ b/src/Compiler/Checking/NicePrint.fs @@ -2041,10 +2041,7 @@ module TastDefinitionPrinting = elif isMeasure then None, tagClass elif isClassTy g ty then - if denv.printVerboseSignatures then - (if simplified then None else Some "class"), tagClass - else - None, tagClass + (if simplified then None else Some "class"), tagClass else None, tagUnknownType @@ -2240,15 +2237,15 @@ module TastDefinitionPrinting = let needsStartEnd = match start with | Some "class" -> + // When allDecls is empty, the repr layout produces 'class end' which is sufficient + not (isNil allDecls) && // 'inherits' is not enough for F# type kind inference to infer a class // inherits.IsEmpty && ilFields.IsEmpty && // 'abstract' is not enough for F# type kind inference to infer a class by default in signatures // 'static member' is surprisingly not enough for F# type kind inference to infer a class by default in signatures // 'overrides' is surprisingly not enough for F# type kind inference to infer a class by default in signatures - //(meths |> List.forall (fun m -> m.IsAbstract || m.IsDefiniteFSharpOverride || not m.IsInstance)) && - //(props |> List.forall (fun m -> (not m.HasGetter || m.GetterMethod.IsAbstract))) && - //(props |> List.forall (fun m -> (not m.HasSetter || m.SetterMethod.IsAbstract))) && + // Concrete instance methods and properties are also not enough (Error 938) ctors.IsEmpty && instanceVals.IsEmpty && staticVals.IsEmpty diff --git a/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs b/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs index db35c217d75..5b33bc70d69 100644 --- a/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs @@ -26,6 +26,7 @@ namespace Foo.Types type Area = | Area of string * int namespace Foo.Other + [] type Map<'t,'v> = member Calculate: Foo.Types.Area""" @@ -45,6 +46,7 @@ type Foo = """ namespace Hey.There + [] type Foo = static member Zero: Foo""" diff --git a/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs b/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs index b1e22705d3a..e01b2da1257 100644 --- a/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs @@ -240,6 +240,7 @@ type Foo = """ module Lib +[] type Foo = member Bar: int with get, set""" @@ -257,6 +258,7 @@ type Foo = """ module Lib +[] type Foo = member Bar: a: int -> int with get @@ -516,3 +518,80 @@ module R = Assert.Contains("type private 'a P", signatures) Assert.DoesNotContain("type 'a private P", signatures) + +// https://github.com/dotnet/fsharp/issues/16531 +[] +let ``Private constructor class with static members gets Class attribute in signature`` () = + let implSource = + """ +module Telplin + +type A private () = + static member Foo () = () +""" + + let generatedSignature = + FSharp implSource + |> printSignatures + + // Static members alone are not enough for the compiler to infer class + Assert.Contains("Class", generatedSignature) + + // Roundtrip: the generated signature must compile with the implementation + Fsi generatedSignature + |> withAdditionalSourceFile (FsSource implSource) + |> withOptions [ "--warnaserror:64" ] + |> ignoreWarnings + |> compile + |> shouldSucceed + |> ignore + +// https://github.com/dotnet/fsharp/issues/16531 +[] +let ``Private constructor class with instance members gets Class attribute in signature`` () = + let implSource = + """ +module Telplin + +type A private () = + member a.Foo () = () +""" + + let generatedSignature = + FSharp implSource + |> printSignatures + + // Private ctor is not visible in signature, so [] is needed + Assert.Contains("Class", generatedSignature) + + Fsi generatedSignature + |> withAdditionalSourceFile (FsSource implSource) + |> withOptions [ "--warnaserror:64" ] + |> ignoreWarnings + |> compile + |> shouldSucceed + |> ignore + +// https://github.com/dotnet/fsharp/issues/16531 +[] +let ``Public constructor class does not need Class attribute in signature`` () = + let implSource = + """ +module Telplin + +type B() = + member b.Bar () = () +""" + + let generatedSignature = + FSharp implSource + |> printSignatures + + // Public constructor is visible, so [] should not be needed + Fsi generatedSignature + |> withAdditionalSourceFile (FsSource implSource) + |> withOptions [ "--warnaserror:64" ] + |> ignoreWarnings + |> compile + |> shouldSucceed + |> ignore From 55e125cabea2d2f632b7ec73c2a8bc37a06f235f Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Mon, 13 Apr 2026 12:57:23 +0200 Subject: [PATCH 07/14] Add test coverage for literal attribute args and CompilerCompat - Test multiple literal args in tuple attribute position (names recovered) - Test qualified literal reference limitation (shows constant value) - Add CompilerCompat test: type with literal-based attribute arg exercises Expr.Val in AttribExpr.source pickling across compiler versions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Signatures/ModuleOrNamespaceTests.fs | 58 +++++++++++++++++++ .../CompilerCompatApp/Program.fs | 14 +++-- .../CompilerCompatLib/Library.fs | 15 +++++ 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs b/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs index 5b33bc70d69..638ef94e8a0 100644 --- a/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Signatures/ModuleOrNamespaceTests.fs @@ -301,6 +301,64 @@ module SubModule = [] member Meh: unit -> unit""" +// https://github.com/dotnet/fsharp/issues/13810 +[] +let ``Multiple literal values in attribute tuple args use literal names`` () = + let actual = + FSharp + """ +module TestMod + +open System + +[] +type TwoArgAttribute(a: string, b: string) = + inherit Attribute() + +[] +let First = "first" + +[] +let Second = "second" + +type Foo() = + [] + member _.Bar() = () +""" + |> printSignatures + + // The attribute argument line should show literal names, not constant values + let attrLine = actual.Split('\n') |> Array.find (fun l -> l.Contains("TwoArg") && l.Contains("[<")) + Assert.Contains("First", attrLine) + Assert.Contains("Second", attrLine) + Assert.DoesNotContain("\"first\"", attrLine) + Assert.DoesNotContain("\"second\"", attrLine) + +// https://github.com/dotnet/fsharp/issues/13810 — known limitation: +// Qualified literal references (e.g. Module.Literal) are not recovered; +// the constant value is shown instead. +[] +let ``Qualified literal in attribute shows constant value`` () = + FSharp + """ +module TestMod + +open System.ComponentModel + +module Constants = + [] + let A = "QualifiedValue" + +type Foo() = + [] + member this.Meh () = () +""" + |> printSignatures + |> prependNewline + |> fun actual -> + // Qualified literal refs are not recovered — constant value is shown + Assert.Contains("QualifiedValue", actual) + // https://github.com/dotnet/fsharp/issues/15389 [] let ``Backtick in identifier is properly escaped in signature`` () = diff --git a/tests/projects/CompilerCompat/CompilerCompatApp/Program.fs b/tests/projects/CompilerCompat/CompilerCompatApp/Program.fs index b643a77c711..e7bd46a8ed8 100644 --- a/tests/projects/CompilerCompat/CompilerCompatApp/Program.fs +++ b/tests/projects/CompilerCompat/CompilerCompatApp/Program.fs @@ -58,12 +58,18 @@ let main _argv = printfn "Literal: %d" CompilerCompatLib.Library.LiteralValue printfn "Reflected: %d" (CompilerCompatLib.Library.reflectedFunction 1) - if processed = "Processed: X=123, Y=test" then - printfn "SUCCESS: All compiler compatibility tests passed" - 0 - else + // Test literal attribute arg cross-compiler compatibility + let attrObj = CompilerCompatLib.Library.TypeWithLiteralAttrArg() + printfn "LiteralAttr: %s" (attrObj.GetValue()) + if attrObj.GetValue() <> CompilerCompatLib.Library.LiteralAttrArg then + printfn "ERROR: Literal attr arg value mismatch" + 1 + elif processed <> "Processed: X=123, Y=test" then printfn "ERROR: Processed result doesn't match expected" 1 + else + printfn "SUCCESS: All compiler compatibility tests passed" + 0 with ex -> printfn "ERROR: Exception occurred: %s" ex.Message diff --git a/tests/projects/CompilerCompat/CompilerCompatLib/Library.fs b/tests/projects/CompilerCompat/CompilerCompatLib/Library.fs index 085a50e99a8..625cb787b58 100644 --- a/tests/projects/CompilerCompat/CompilerCompatLib/Library.fs +++ b/tests/projects/CompilerCompat/CompilerCompatLib/Library.fs @@ -40,3 +40,18 @@ module Library = /// Function with ReflectedDefinition [] let reflectedFunction x = x + 1 + + /// Literal string used as an attribute argument. + /// Tests that Expr.Val in AttribExpr.source pickles/unpickles across compiler versions. + [] + let LiteralAttrArg = "compat-test-value" + + /// Custom attribute for cross-version literal attribute arg testing + type TestAttrAttribute(value: string) = + inherit System.Attribute() + member _.Value = value + + /// Type decorated with an attribute whose argument is a literal val reference + [] + type TypeWithLiteralAttrArg() = + member _.GetValue() = LiteralAttrArg From d8d963f3c5c4ee8ce9dea8871f080e48845c4bf2 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Mon, 13 Apr 2026 14:11:38 +0200 Subject: [PATCH 08/14] Add guard condition and edge case tests for signature generation - Struct with explicit [] does not get duplicate attribute - Struct with [] does not get [] injected - Empty class with private constructor uses 'class end' repr - AbstractClass with private constructor roundtrips correctly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Signatures/TypeTests.fs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs b/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs index e01b2da1257..cb0f5da188b 100644 --- a/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs @@ -472,6 +472,46 @@ type LocalReadWriteLockCookie(locker: obj) = |> shouldSucceed |> ignore +// https://github.com/dotnet/fsharp/issues/15339 +[] +let ``Struct with explicit NoComparison does not get duplicate attribute`` () = + let generatedSignature = + FSharp + """ +namespace TestNs + +[] +type AlreadyMarked = + val X: obj + new(x) = { X = x } +""" + |> printSignatures + + // Should NOT duplicate [] — the explicit one is sufficient + let count = System.Text.RegularExpressions.Regex.Matches(generatedSignature, "NoComparison").Count + Assert.Equal(1, count) + +// https://github.com/dotnet/fsharp/issues/15339 +[] +let ``Struct with CustomComparison does not get NoComparison`` () = + let generatedSignature = + FSharp + """ +namespace TestNs + +open System + +[] +type WithCustomCompare = + val X: obj + new(x) = { X = x } + interface IComparable with + member _.CompareTo(_) = 0 +""" + |> printSignatures + + Assert.DoesNotContain("NoComparison", generatedSignature) + // https://github.com/dotnet/fsharp/issues/15339 [] let ``Struct with non-equatable field includes NoEquality in signature`` () = @@ -595,3 +635,51 @@ type B() = |> compile |> shouldSucceed |> ignore + +// https://github.com/dotnet/fsharp/issues/16531 +[] +let ``Empty class with private constructor uses class end`` () = + let implSource = + """ +module Telplin + +type Empty private () = class end +""" + + let generatedSignature = + FSharp implSource + |> printSignatures + + // Empty class should use 'class end' repr, not [] attribute + Assert.Contains("class end", generatedSignature) + + Fsi generatedSignature + |> withAdditionalSourceFile (FsSource implSource) + |> ignoreWarnings + |> compile + |> shouldSucceed + |> ignore + +// https://github.com/dotnet/fsharp/issues/16531 +[] +let ``AbstractClass with private constructor roundtrips`` () = + let implSource = + """ +module Telplin + +[] +type A private () = + abstract member M: unit -> unit +""" + + let generatedSignature = + FSharp implSource + |> printSignatures + + // The generated signature should compile with the implementation + Fsi generatedSignature + |> withAdditionalSourceFile (FsSource implSource) + |> ignoreWarnings + |> compile + |> shouldSucceed + |> ignore From cc75a8fcc8e84c6bb16060012f8b55626fc65f1a Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Mon, 13 Apr 2026 14:16:08 +0200 Subject: [PATCH 09/14] Guard literal recovery against VRefNonLocal to prevent transitive deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only replace Expr.Const with Expr.Val in AttribExpr.source when the literal val reference is local (VRefLocal). Cross-assembly literals (VRefNonLocal from 'open ExternalLib') keep Expr.Const to avoid creating unresolvable transitive assembly dependencies in pickled metadata — consumers of the DLL may not reference the external assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/Expressions/CheckExpressions.fs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index 2271cbe0b6b..48c9c7e9af3 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -11650,8 +11650,11 @@ and TcAttributeEx canFail (cenv: cenv) (env: TcEnv) attrTgt attrEx (synAttr: Syn match e with | Expr.Const(_, m, _) -> match literalIdents |> List.tryFind (fun (r, _) -> Range.equals r m) with - | Some(_, vref) -> Expr.Val(vref, NormalValUse, m) - | None -> e + // Only use Expr.Val for local refs to avoid creating transitive assembly + // dependencies in pickled metadata (VRefNonLocal would require the + // external assembly to be available when importing this DLL). + | Some(_, vref) when vref.IsLocalRef -> Expr.Val(vref, NormalValUse, m) + | _ -> e | _ -> e AttribExpr(sourceExpr, EvalLiteralExprOrAttribArg g e) From 938d7dae1178dd2696b6dff36219db87fa0da208 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Mon, 13 Apr 2026 14:58:39 +0200 Subject: [PATCH 10/14] Normalize Expr.Val back to Expr.Const in p_attrib_expr before pickling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The literal name recovery stores Expr.Val in AttribExpr.source as an in-memory optimization for signature generation display. However, this must NOT change the pickle format: - Old compilers reading Expr.Val in AttribExpr.source would display '(* unsupported attribute argument *)' instead of the constant value in tooltips and signatures — a cross-version display regression. - Old compilers' CheckDeclarations.fs pattern-matches on the source field and would miss the AssemblyVersion format warning. By normalizing Expr.Val(literal_vref) back to Expr.Const(literal_value) before pickling, the on-disk format is identical to before this PR. The literal name display only works during the current compilation (the signature generation use case), which is the correct scope. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/TypedTree/TypedTreePickle.fs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Compiler/TypedTree/TypedTreePickle.fs b/src/Compiler/TypedTree/TypedTreePickle.fs index ab09c556b68..93e277c05b4 100644 --- a/src/Compiler/TypedTree/TypedTreePickle.fs +++ b/src/Compiler/TypedTree/TypedTreePickle.fs @@ -2875,7 +2875,18 @@ and p_attribkind x st = and p_attrib (Attrib(a, b, c, d, e, _targets, f)) st = // AttributeTargets are not preserved p_tup6 (p_tcref "attrib") p_attribkind (p_list p_attrib_expr) (p_list p_attrib_arg) p_bool p_dummy_range (a, b, c, d, e, f) st -and p_attrib_expr (AttribExpr(e1, e2)) st = p_tup2 p_expr p_expr (e1, e2) st +and p_attrib_expr (AttribExpr(e1, e2)) st = + // Normalize Expr.Val back to Expr.Const before pickling. + // The literal name recovery (Expr.Val in source field) is an in-memory optimization + // for signature generation display. We must not change the pickle format, because + // old compilers reading Expr.Val in this position would show degraded attribute display. + let e1 = + match e1 with + | Expr.Val(vref, _, m) when vref.LiteralValue.IsSome -> + Expr.Const(vref.LiteralValue.Value, m, vref.Type) + | _ -> e1 + + p_tup2 p_expr p_expr (e1, e2) st and p_attrib_arg (AttribNamedArg(a, b, c, d)) st = p_tup4 p_string p_ty p_bool p_attrib_expr (a, b, c, d) st From 788a31968c14b0b63e8404d4c3f416883aa8c611 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Tue, 14 Apr 2026 19:42:26 +0200 Subject: [PATCH 11/14] Fix literal recovery for named attrs and tighten doval filter Two fixes from adversarial cross-model review: 1. Add SynExpr.App traversal to literal ident collector so named attribute arguments like [] also get literal name recovery. Previously only positional args worked. 2. Replace IsCompilerGeneratedName (any '@' in name) with targeted StartsWithOrdinal("doval@") in filterVal. The broad '@' check would incorrectly drop backtick-escaped user identifiers containing '@' from module rec signatures. The doval@ prefix is the only compiler-generated name pattern that leaks through TMDefRec bindings, matching the same pattern used in SignatureHash.fs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/Expressions/CheckExpressions.fs | 1 + src/Compiler/Checking/NicePrint.fs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index 48c9c7e9af3..e93c217da66 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -11641,6 +11641,7 @@ and TcAttributeEx canFail (cenv: cenv) (env: TcEnv) attrTgt attrEx (synAttr: Syn | _ -> acc | SynExpr.Paren(expr = inner) -> collect inner acc | SynExpr.Tuple(exprs = exprs) -> List.fold (fun a e -> collect e a) acc exprs + | SynExpr.App(_, _, funcExpr, argExpr, _) -> collect funcExpr (collect argExpr acc) | _ -> acc collect arg [] diff --git a/src/Compiler/Checking/NicePrint.fs b/src/Compiler/Checking/NicePrint.fs index db0c5c95360..2ac6ac5691f 100644 --- a/src/Compiler/Checking/NicePrint.fs +++ b/src/Compiler/Checking/NicePrint.fs @@ -2591,7 +2591,7 @@ module InferredSigPrinting = let rec imdefsL denv x = aboveListL (x |> List.map (imdefL denv)) and imdefL denv x = - let filterVal (v: Val) = not v.IsCompilerGenerated && Option.isNone v.MemberInfo && not (IsCompilerGeneratedName v.LogicalName) + let filterVal (v: Val) = not v.IsCompilerGenerated && Option.isNone v.MemberInfo && not (v.LogicalName.StartsWithOrdinal("doval@")) let filterExtMem (v: Val) = v.IsExtensionMember match x with From 99f10ab619985d0714279aab8f718e54aa3a8aa3 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Tue, 14 Apr 2026 19:49:56 +0200 Subject: [PATCH 12/14] Add release notes for signature generation fixes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 8669e7142f0..2503f666e37 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -27,6 +27,12 @@ * Fix box instruction for literal upcasts. (Issue [#18319](https://github.com/dotnet/fsharp/issues/18319), [PR #19338](https://github.com/dotnet/fsharp/pull/19338)) * Fix Decimal Literal causes InvalidProgramException in Debug builds. (Issue [#18956](https://github.com/dotnet/fsharp/issues/18956), [PR #19338](https://github.com/dotnet/fsharp/pull/19338)) * Fix `AttributeUsage.AllowMultiple` not being inherited for attributes subclassed in C#. ([Issue #17107](https://github.com/dotnet/fsharp/issues/17107), [PR #19315](https://github.com/dotnet/fsharp/pull/19315)) +* Fix signature generation: recursive module `do` binding leaking compiler-generated val. ([Issue #13832](https://github.com/dotnet/fsharp/issues/13832), [PR #19586](https://github.com/dotnet/fsharp/pull/19586)) +* Fix signature generation: literal values in attribute arguments now preserve literal identifier name. ([Issue #13810](https://github.com/dotnet/fsharp/issues/13810), [PR #19586](https://github.com/dotnet/fsharp/pull/19586)) +* Fix signature generation: struct types with non-comparable/non-equatable fields now include `[]`/`[]`. ([Issue #15339](https://github.com/dotnet/fsharp/issues/15339), [PR #19586](https://github.com/dotnet/fsharp/pull/19586)) +* Fix signature generation: backtick escaping for identifiers containing backticks. ([Issue #15389](https://github.com/dotnet/fsharp/issues/15389), [PR #19586](https://github.com/dotnet/fsharp/pull/19586)) +* Fix signature generation: `private` keyword placement for prefix-style type abbreviations. ([Issue #15560](https://github.com/dotnet/fsharp/issues/15560), [PR #19586](https://github.com/dotnet/fsharp/pull/19586)) +* Fix signature generation: missing `[]` attribute for types without visible constructors. ([Issue #16531](https://github.com/dotnet/fsharp/issues/16531), [PR #19586](https://github.com/dotnet/fsharp/pull/19586)) ### Added From abe969164859074377a63c57468b43c529636c03 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Wed, 15 Apr 2026 10:02:02 +0200 Subject: [PATCH 13/14] Gate [] emission on showAttributes to fix FSI printing tests The PR unconditionally set start=Some "class" for class types, causing [] to appear in FSI output. Gate on denv.showAttributes instead of the removed printVerboseSignatures check: - FSI (showAttributes=false): no [] emitted (matches old behavior) - GenerateSignature (showAttributes=true): [] emitted when needed - --sig flag (showAttributes=true): [] emitted when needed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/NicePrint.fs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Compiler/Checking/NicePrint.fs b/src/Compiler/Checking/NicePrint.fs index 2ac6ac5691f..048375c24fb 100644 --- a/src/Compiler/Checking/NicePrint.fs +++ b/src/Compiler/Checking/NicePrint.fs @@ -2041,7 +2041,10 @@ module TastDefinitionPrinting = elif isMeasure then None, tagClass elif isClassTy g ty then - (if simplified then None else Some "class"), tagClass + if denv.showAttributes then + (if simplified then None else Some "class"), tagClass + else + None, tagClass else None, tagUnknownType From 4490e6572a3d3b9bdbd4767c91b9ae58e7153b99 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 16 Apr 2026 08:53:38 +0200 Subject: [PATCH 14/14] Fix flaky help test: reset consolecolors before help output Add --consolecolors+ before help flags (-?, /?, --help) in help tests to ensure enableConsoleColoring is always true when help text is generated. This prevents race conditions from concurrent tests that temporarily set enableConsoleColoring to false (e.g., consolecolors switch test), which caused Linux CI failures where the baseline expected '(on by default)' but got '(off by default)'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CompilerOptions/fsc/misc/misc.fs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/misc/misc.fs b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/misc/misc.fs index b1339c507f6..6d9631dc609 100644 --- a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/misc/misc.fs +++ b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/misc/misc.fs @@ -11,12 +11,14 @@ open System.IO module help_options = // ReqENU SOURCE=dummy.fsx COMPILE_ONLY=1 PRECMD="\$FSC_PIPE >help40.txt -? 2>&1" POSTCMD="\$FSI_PIPE --nologo --quiet --exec ..\\..\\..\\comparer.fsx help40.txt help40.437.1033.bsl" # -? + // Reset enableConsoleColoring before help tests to avoid global mutable state + // pollution from concurrent tests (e.g., ``fsc --consolecolors switch``). [] let ``Help - variant 1``() = FSharp "" |> asExe |> withBufferWidth 120 - |> withOptions ["-?"] + |> withOptions ["--consolecolors+"; "-?"] |> compile |> verifyOutputWithBaseline (Path.Combine(__SOURCE_DIRECTORY__, "compiler_help_output.bsl")) |> shouldSucceed @@ -27,7 +29,7 @@ module help_options = FSharp "" |> asExe |> withBufferWidth 120 - |> withOptions ["/?"] + |> withOptions ["--consolecolors+"; "/?"] |> compile |> verifyOutputWithBaseline (Path.Combine(__SOURCE_DIRECTORY__, "compiler_help_output.bsl")) |> shouldSucceed @@ -38,7 +40,7 @@ module help_options = FSharp "" |> asExe |> withBufferWidth 120 - |> withOptions ["--help"] + |> withOptions ["--consolecolors+"; "--help"] |> compile |> verifyOutputWithBaseline (Path.Combine(__SOURCE_DIRECTORY__, "compiler_help_output.bsl")) |> shouldSucceed