From d0bf14fb399fd3bc266dfe8f2aa7e88c6d1775ff Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Thu, 14 May 2026 19:53:05 -0500 Subject: [PATCH 1/9] Rotate [] binding attrs into the arity-info return position In mkSynBinding, move any attribute written with the explicit 'return:' target from the binding's prefix attributes into SynValData.SynValInfo.retInfo. This makes the syntactic placement (which the parser puts on the binding) match the semantic intent (the attribute targets the method's return value). Fixes: - #19020: [] silently dropped on class members - #17904: false-positive AllowMultiple=false when [] and [] appear on the same member --- src/Compiler/SyntaxTree/SyntaxTreeOps.fs | 28 +++++ .../Language/AttributeCheckingTests.fs | 102 +++++++++++++++++- 2 files changed, 129 insertions(+), 1 deletion(-) diff --git a/src/Compiler/SyntaxTree/SyntaxTreeOps.fs b/src/Compiler/SyntaxTree/SyntaxTreeOps.fs index 9ce91852896..e99dd97de85 100644 --- a/src/Compiler/SyntaxTree/SyntaxTreeOps.fs +++ b/src/Compiler/SyntaxTree/SyntaxTreeOps.fs @@ -740,6 +740,32 @@ module SynInfo = let argInfos = infosForObjArgs @ infosForArgs SynValData(Some memFlags, SynValInfo(argInfos, retInfo), None) + let private isReturnTargetedAttribute (a: SynAttribute) = + match a.Target with + | Some id -> id.idText = "return" + | None -> false + + /// Rotate any `[]` attributes from a binding's prefix attribute list into the + /// arity-info return position (`SynValInfo.retInfo`). Without this, downstream code that + /// reads `Val.Attribs` would incorrectly see them alongside method-targeted attributes + /// (see issues #17904 and #19020). + let RotateReturnAttributes (attrs: SynAttributes) (valSynData: SynValData) : SynAttributes * SynValData = + // Fast path: avoid all allocation when there's nothing to rotate (the common case). + let hasReturn = + attrs |> List.exists (fun lst -> lst.Attributes |> List.exists isReturnTargetedAttribute) + if not hasReturn then + attrs, valSynData + else + let mutable returnTargeted = [] + let newAttrs = + attrs |> List.choose (fun lst -> + let ret, kept = lst.Attributes |> List.partition isReturnTargetedAttribute + returnTargeted <- returnTargeted @ ret + if List.isEmpty kept then None else Some { lst with Attributes = kept }) + let (SynValData(memFlags, SynValInfo(args, SynArgInfo(retAttrs, opt, retId)), thisIdOpt)) = valSynData + let retList: SynAttributeList = { Attributes = returnTargeted; Range = (List.head returnTargeted).Range } + newAttrs, SynValData(memFlags, SynValInfo(args, SynArgInfo(retList :: retAttrs, opt, retId)), thisIdOpt) + let mkSynBindingRhs staticOptimizations rhsExpr mRhs retInfo = let rhsExpr = List.foldBack (fun (c, e1) e2 -> SynExpr.LibraryOnlyStaticOptimization(c, e1, e2, mRhs)) staticOptimizations rhsExpr @@ -759,6 +785,8 @@ let mkSynBinding let info = SynInfo.InferSynValData(memberFlagsOpt, Some headPat, Option.map snd retInfo, origRhsExpr) + let attrs, info = SynInfo.RotateReturnAttributes attrs info + let rhsExpr, retTyOpt = mkSynBindingRhs staticOptimizations origRhsExpr mRhs retInfo let mBind = unionRangeWithXmlDoc xmlDoc mBind SynBinding(vis, SynBindingKind.Normal, isInline, isMutable, attrs, xmlDoc, info, headPat, retTyOpt, rhsExpr, mBind, spBind, trivia) diff --git a/tests/FSharp.Compiler.ComponentTests/Language/AttributeCheckingTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/AttributeCheckingTests.fs index 8262651bec0..8971b924c80 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/AttributeCheckingTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/AttributeCheckingTests.fs @@ -43,7 +43,107 @@ type C() = |> ignoreWarnings |> compile |> shouldSucceed - + + // Regression test for https://github.com/dotnet/fsharp/issues/19020 + [] + let ``[] prefix attributes are emitted on class members`` () = + FSharp """ +module TestModule +open System + +[] +type DescAttribute() = inherit Attribute() + +module Mod = + [] + let func a = a + 1 + +type T() = + [] + member _.InstMember a = a + 1 + + [] + static member StatMember a = a + 1 + +[] +let main _ = + let asm = System.Reflection.Assembly.GetExecutingAssembly() + let modType = asm.GetType("TestModule+Mod") + let modAttrs = modType.GetMethod("func").ReturnParameter.GetCustomAttributes(typeof, false).Length + let inst = typeof.GetMethod("InstMember").ReturnParameter.GetCustomAttributes(typeof, false).Length + let stat = typeof.GetMethod("StatMember").ReturnParameter.GetCustomAttributes(typeof, false).Length + if modAttrs <> 1 then failwithf "module func: expected 1, got %d" modAttrs + if inst <> 1 then failwithf "InstMember: expected 1 (bug #19020 — attribute dropped on instance members), got %d" inst + if stat <> 1 then failwithf "StatMember: expected 1 (bug #19020 — attribute dropped on static members), got %d" stat + 0 + """ + |> asExe + |> compile + |> shouldSucceed + |> run + |> shouldSucceed + + // Regression test for https://github.com/dotnet/fsharp/issues/17904 + [] + let ``AllowMultiple=false allows the same attribute on method and on its return value`` () = + Fsx """ +open System + +[] +type AttrAttribute(m: string) = + inherit Attribute() + +type Test() = + [] + [] + member _.Foo() = () + """ + |> ignoreWarnings + |> compile + |> shouldSucceed + + // Regression test for https://github.com/dotnet/fsharp/issues/17904 — short-form + explicit-same-target + // mix must still be flagged as a duplicate because both apply to the same metadata target. + [] + let ``AllowMultiple=false still errors when short-form and method-targeted attributes are mixed`` () = + Fsx """ +open System + +[] +type AttrAttribute() = + inherit Attribute() + +type Test() = + [] + [] + member _.Foo() = () + """ + |> ignoreWarnings + |> compile + |> shouldFail + |> withSingleDiagnostic (Error 429, Line 10, Col 7, Line 10, Col 19, "The attribute type 'AttrAttribute' has 'AllowMultiple=false'. Multiple instances of this attribute cannot be attached to a single language element.") + + // Regression test for https://github.com/dotnet/fsharp/issues/17904 + [] + let ``AllowMultiple=false still errors when the same attribute is applied twice to the same target`` () = + Fsx """ +open System + +[] +type AttrAttribute() = + inherit Attribute() + +type Test() = + [] + [] + member _.Foo() = () + """ + |> ignoreWarnings + |> compile + |> shouldFail + |> withSingleDiagnostic (Error 429, Line 10, Col 7, Line 10, Col 19, "The attribute type 'AttrAttribute' has 'AllowMultiple=false'. Multiple instances of this attribute cannot be attached to a single language element.") + + [] let ``Regression: typechecker does not fail when attribute is on type variable (https://github.com/dotnet/fsharp/issues/13525)`` () = let csharpBaseClass = From 01539e221b712b67a9ca1b08b993c7c9215555f2 Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Fri, 15 May 2026 08:15:22 -0500 Subject: [PATCH 2/9] Pick up rotated [] attribs in TcNormalizedBinding and active-pattern detection After SynInfo.RotateReturnAttributes moves [] from the binding's prefix attributes into SynValData.SynValInfo.retInfo, two downstream consumers also need updating: - TcNormalizedBinding's retAttribs computation now type-checks attrs already in valSynData's return SynArgInfo, so isStructRetTy/argAndRetAttribs work for [] on partial active patterns. - ActivePatternElemsOfValRef now classifies the flag bag from ValReprInfo's result ArgReprInfo rather than scanning vref.Attribs. Fixes recursive struct active patterns (e.g. let rec (|HasOne|_|)). --- .../Checking/Expressions/CheckExpressions.fs | 10 ++++++++-- src/Compiler/Checking/NameResolution.fs | 15 ++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index 30d49e776c8..d99d20913db 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -11234,11 +11234,17 @@ and TcNormalizedBinding declKind (cenv: cenv) env tpenv overallTy safeThisValOpt |> List.zip attrs |> List.partition(function | _, Attrib(_, _, _, _, _, Some ts, _) -> ts &&& AttributeTargets.ReturnValue <> enum 0 | _ -> false) |> fun (r, v) -> (List.map fst r, List.map snd r, List.map snd v) + // SynInfo.RotateReturnAttributes (called from mkSynBinding) may have already moved + // [] attributes into valSynData's return SynArgInfo. Pick those up too. + let valSynDataRetSynAttrs = + let (SynValData(_, SynValInfo(_, SynArgInfo(retAttrs, _, _)), _)) = valSynData + retAttrs |> List.collect (fun a -> a.Attributes) let retAttribs = + let fromValSyn = TcAttrs AttributeTargets.ReturnValue true valSynDataRetSynAttrs match rtyOpt with | Some (SynBindingReturnInfo(attributes = Attributes retAttrs)) -> - rotRetAttribs @ TcAttrs AttributeTargets.ReturnValue true retAttrs - | None -> rotRetAttribs + rotRetAttribs @ fromValSyn @ TcAttrs AttributeTargets.ReturnValue true retAttrs + | None -> rotRetAttribs @ fromValSyn let valSynData = match rotRetSynAttrs with | [] -> valSynData diff --git a/src/Compiler/Checking/NameResolution.fs b/src/Compiler/Checking/NameResolution.fs index 54290c21bd0..c2ff7d12589 100644 --- a/src/Compiler/Checking/NameResolution.fs +++ b/src/Compiler/Checking/NameResolution.fs @@ -94,11 +94,16 @@ let ActivePatternElemsOfValRef g (vref: ValRef) = ActivePatternReturnKind.RefTypeWrapper else let _, apReturnTy = stripFunTy g vref.TauType - let hasStructAttribute() = - vref.Attribs - |> List.exists (function - | Attrib(targetsOpt = Some(System.AttributeTargets.ReturnValue)) as a -> hasFlag (classifyValAttrib g a) WellKnownValAttributes.StructAttribute - | _ -> false) + let hasStructAttribute() = + // After SynInfo.RotateReturnAttributes, [] lives in + // ValReprInfo's result ArgReprInfo rather than vref.Attribs. The flag bits + // in ArgReprInfo.Attribs are computed lazily, so classify the underlying + // attribute list directly. + match vref.ValReprInfo with + | Some(ValReprInfo(_, _, retInfo)) -> + let flags = computeValWellKnownFlags g (retInfo.Attribs.AsList()) + hasFlag flags WellKnownValAttributes.StructAttribute + | None -> false if isValueOptionTy g apReturnTy || hasStructAttribute() then ActivePatternReturnKind.StructTypeWrapper elif isBoolTy g apReturnTy then ActivePatternReturnKind.Boolean else ActivePatternReturnKind.RefTypeWrapper From cdce480d416754f3785863b5a406789373a3e167 Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Fri, 15 May 2026 08:23:20 -0500 Subject: [PATCH 3/9] Format SyntaxTreeOps.fs with fantomas --- src/Compiler/SyntaxTree/SyntaxTreeOps.fs | 26 +++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/Compiler/SyntaxTree/SyntaxTreeOps.fs b/src/Compiler/SyntaxTree/SyntaxTreeOps.fs index e99dd97de85..da070557119 100644 --- a/src/Compiler/SyntaxTree/SyntaxTreeOps.fs +++ b/src/Compiler/SyntaxTree/SyntaxTreeOps.fs @@ -752,18 +752,34 @@ module SynInfo = let RotateReturnAttributes (attrs: SynAttributes) (valSynData: SynValData) : SynAttributes * SynValData = // Fast path: avoid all allocation when there's nothing to rotate (the common case). let hasReturn = - attrs |> List.exists (fun lst -> lst.Attributes |> List.exists isReturnTargetedAttribute) + attrs + |> List.exists (fun lst -> lst.Attributes |> List.exists isReturnTargetedAttribute) + if not hasReturn then attrs, valSynData else let mutable returnTargeted = [] + let newAttrs = - attrs |> List.choose (fun lst -> + attrs + |> List.choose (fun lst -> let ret, kept = lst.Attributes |> List.partition isReturnTargetedAttribute returnTargeted <- returnTargeted @ ret - if List.isEmpty kept then None else Some { lst with Attributes = kept }) - let (SynValData(memFlags, SynValInfo(args, SynArgInfo(retAttrs, opt, retId)), thisIdOpt)) = valSynData - let retList: SynAttributeList = { Attributes = returnTargeted; Range = (List.head returnTargeted).Range } + + if List.isEmpty kept then + None + else + Some { lst with Attributes = kept }) + + let (SynValData(memFlags, SynValInfo(args, SynArgInfo(retAttrs, opt, retId)), thisIdOpt)) = + valSynData + + let retList: SynAttributeList = + { + Attributes = returnTargeted + Range = (List.head returnTargeted).Range + } + newAttrs, SynValData(memFlags, SynValInfo(args, SynArgInfo(retList :: retAttrs, opt, retId)), thisIdOpt) let mkSynBindingRhs staticOptimizations rhsExpr mRhs retInfo = From 85b631fe04c6c46f0f8122f73fe6f7457c42ac4f Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Fri, 15 May 2026 08:45:49 -0500 Subject: [PATCH 4/9] Remove dead [] rotation block in TcNormalizedBinding With the parser-level rotation in SynInfo.RotateReturnAttributes, the binding's prefix attrs never contain [] by the time TcNormalizedBinding runs. The partition-and-rotate dance and the valSynData patch are no-ops in every reachable case, so remove them and read return attrs directly from SynValData.SynValInfo.retInfo (where the parser put them) plus any attrs on the return type annotation. --- .../Checking/Expressions/CheckExpressions.fs | 37 ++++++------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index d99d20913db..54aacd392e7 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -11223,35 +11223,20 @@ and TcNormalizedBinding declKind (cenv: cenv) env tpenv overallTy safeThisValOpt errorR(Error(FSComp.SR.tcAttributesAreNotPermittedOnLetBindings(), attr.Range)) attrs - // Rotate [] from binding to return value - // Also patch the syntactic representation - let retAttribs, valAttribs, valSynData = - let attribs = TcAttrs attrTgt false attrs - let rotRetSynAttrs, rotRetAttribs, valAttribs = - // Do not rotate if some attrs fail to typecheck... - if attribs.Length <> attrs.Length then [], [], attribs - else attribs - |> List.zip attrs - |> List.partition(function | _, Attrib(_, _, _, _, _, Some ts, _) -> ts &&& AttributeTargets.ReturnValue <> enum 0 | _ -> false) - |> fun (r, v) -> (List.map fst r, List.map snd r, List.map snd v) - // SynInfo.RotateReturnAttributes (called from mkSynBinding) may have already moved - // [] attributes into valSynData's return SynArgInfo. Pick those up too. + // [] attributes are moved out of the binding's prefix and into + // SynValData.SynValInfo.retInfo by SynInfo.RotateReturnAttributes in mkSynBinding. + // Pick them up from there along with any attributes on the return type annotation. + let valAttribs = TcAttrs attrTgt false attrs + + let retAttribs = let valSynDataRetSynAttrs = let (SynValData(_, SynValInfo(_, SynArgInfo(retAttrs, _, _)), _)) = valSynData retAttrs |> List.collect (fun a -> a.Attributes) - let retAttribs = - let fromValSyn = TcAttrs AttributeTargets.ReturnValue true valSynDataRetSynAttrs - match rtyOpt with - | Some (SynBindingReturnInfo(attributes = Attributes retAttrs)) -> - rotRetAttribs @ fromValSyn @ TcAttrs AttributeTargets.ReturnValue true retAttrs - | None -> rotRetAttribs @ fromValSyn - let valSynData = - match rotRetSynAttrs with - | [] -> valSynData - | {Range=mHead} :: _ -> - let (SynValData(valMf, SynValInfo(args, SynArgInfo(attrs, opt, retId)), valId)) = valSynData - SynValData(valMf, SynValInfo(args, SynArgInfo({Attributes=rotRetSynAttrs; Range=mHead} :: attrs, opt, retId)), valId) - retAttribs, valAttribs, valSynData + let fromValSyn = TcAttrs AttributeTargets.ReturnValue true valSynDataRetSynAttrs + match rtyOpt with + | Some(SynBindingReturnInfo(attributes = Attributes retAttrs)) -> + fromValSyn @ TcAttrs AttributeTargets.ReturnValue true retAttrs + | None -> fromValSyn let valAttribFlags = computeValWellKnownFlags g valAttribs From e84a5473b13e8bea1d9087fdc49534ec495e28b7 Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Fri, 15 May 2026 16:13:56 -0500 Subject: [PATCH 5/9] Add release note for #17904 and #19020 fix --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + 1 file changed, 1 insertion(+) 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 3e2c18a6ef0..1a7ecb1a6ba 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -1,5 +1,6 @@ ### Fixed +* Fix `[]` prefix attributes being silently dropped on class members, and fix false-positive `AllowMultiple=false` errors when `[]` and `[]` are applied to the same binding. ([Issue #17904](https://github.com/dotnet/fsharp/issues/17904), [Issue #19020](https://github.com/dotnet/fsharp/issues/19020), [PR #19738](https://github.com/dotnet/fsharp/pull/19738)) * Fix attributes on return type of unparenthesized tuple methods being silently dropped from IL. ([Issue #462](https://github.com/dotnet/fsharp/issues/462), [PR #19714](https://github.com/dotnet/fsharp/pull/19714)) * Fix internal error FS0073 "Undefined or unsolved type variable" in IlxGen when nested inline SRTP functions with multiple overloads leave unsolved typars in the non-witness codegen path. ([Issue #19709](https://github.com/dotnet/fsharp/issues/19709), [PR #19710](https://github.com/dotnet/fsharp/pull/19710)) * Fix NRE when calling virtual Object methods on value types through inline SRTP functions. ([Issue #8098](https://github.com/dotnet/fsharp/issues/8098), [PR #19511](https://github.com/dotnet/fsharp/pull/19511)) From 77b53ed5ae6eaf34a23b3609ba4e1276e43cf87d Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Sat, 16 May 2026 21:21:19 -0500 Subject: [PATCH 6/9] Simplify regression tests for #17904 and #19020 Drop the module-let case from the #19020 test (module bindings were never affected). Rename tests with 'Issue NNNNN -' prefix and tighten source samples and failure messages. --- .../Language/AttributeCheckingTests.fs | 75 +++++++------------ 1 file changed, 29 insertions(+), 46 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/Language/AttributeCheckingTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/AttributeCheckingTests.fs index 8971b924c80..d96d8561dda 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/AttributeCheckingTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/AttributeCheckingTests.fs @@ -44,37 +44,30 @@ type C() = |> compile |> shouldSucceed - // Regression test for https://github.com/dotnet/fsharp/issues/19020 + // Regression tests for #17904 and #19020. Both have a common root cause: + // [] prefix attributes must route to the return-value metadata slot, + // not stay on the binding alongside method-targeted attributes. + [] - let ``[] prefix attributes are emitted on class members`` () = + let ``Issue 19020 - [] is emitted on the return parameter of class members`` () = FSharp """ -module TestModule +module M open System - [] type DescAttribute() = inherit Attribute() -module Mod = - [] - let func a = a + 1 - type T() = [] - member _.InstMember a = a + 1 - + member _.Inst a = a + 1 [] - static member StatMember a = a + 1 + static member Stat a = a + 1 [] let main _ = - let asm = System.Reflection.Assembly.GetExecutingAssembly() - let modType = asm.GetType("TestModule+Mod") - let modAttrs = modType.GetMethod("func").ReturnParameter.GetCustomAttributes(typeof, false).Length - let inst = typeof.GetMethod("InstMember").ReturnParameter.GetCustomAttributes(typeof, false).Length - let stat = typeof.GetMethod("StatMember").ReturnParameter.GetCustomAttributes(typeof, false).Length - if modAttrs <> 1 then failwithf "module func: expected 1, got %d" modAttrs - if inst <> 1 then failwithf "InstMember: expected 1 (bug #19020 — attribute dropped on instance members), got %d" inst - if stat <> 1 then failwithf "StatMember: expected 1 (bug #19020 — attribute dropped on static members), got %d" stat + let inst = typeof.GetMethod("Inst").ReturnParameter.GetCustomAttributes(typeof, false).Length + let stat = typeof.GetMethod("Stat").ReturnParameter.GetCustomAttributes(typeof, false).Length + if inst <> 1 then failwithf "instance member: expected 1, got %d" inst + if stat <> 1 then failwithf "static member: expected 1, got %d" stat 0 """ |> asExe @@ -83,65 +76,55 @@ let main _ = |> run |> shouldSucceed - // Regression test for https://github.com/dotnet/fsharp/issues/17904 [] - let ``AllowMultiple=false allows the same attribute on method and on its return value`` () = + let ``Issue 17904 - [] on method and [] on return value are not duplicates`` () = Fsx """ open System - [] -type AttrAttribute(m: string) = - inherit Attribute() +type AttrAttribute() = inherit Attribute() -type Test() = - [] - [] +type T() = + [] + [] member _.Foo() = () """ |> ignoreWarnings |> compile |> shouldSucceed - // Regression test for https://github.com/dotnet/fsharp/issues/17904 — short-form + explicit-same-target - // mix must still be flagged as a duplicate because both apply to the same metadata target. [] - let ``AllowMultiple=false still errors when short-form and method-targeted attributes are mixed`` () = + let ``Issue 17904 - two [] on the same return value remain duplicates`` () = Fsx """ open System - [] -type AttrAttribute() = - inherit Attribute() +type AttrAttribute() = inherit Attribute() -type Test() = - [] - [] +type T() = + [] + [] member _.Foo() = () """ |> ignoreWarnings |> compile |> shouldFail - |> withSingleDiagnostic (Error 429, Line 10, Col 7, Line 10, Col 19, "The attribute type 'AttrAttribute' has 'AllowMultiple=false'. Multiple instances of this attribute cannot be attached to a single language element.") + |> withSingleDiagnostic (Error 429, Line 8, Col 7, Line 8, Col 19, "The attribute type 'AttrAttribute' has 'AllowMultiple=false'. Multiple instances of this attribute cannot be attached to a single language element.") - // Regression test for https://github.com/dotnet/fsharp/issues/17904 [] - let ``AllowMultiple=false still errors when the same attribute is applied twice to the same target`` () = + let ``Issue 17904 - [] and [] on a method remain duplicates`` () = Fsx """ open System - [] -type AttrAttribute() = - inherit Attribute() +type AttrAttribute() = inherit Attribute() -type Test() = - [] - [] +type T() = + [] + [] member _.Foo() = () """ |> ignoreWarnings |> compile |> shouldFail - |> withSingleDiagnostic (Error 429, Line 10, Col 7, Line 10, Col 19, "The attribute type 'AttrAttribute' has 'AllowMultiple=false'. Multiple instances of this attribute cannot be attached to a single language element.") + |> withSingleDiagnostic (Error 429, Line 8, Col 7, Line 8, Col 19, "The attribute type 'AttrAttribute' has 'AllowMultiple=false'. Multiple instances of this attribute cannot be attached to a single language element.") [] From 74e8643bba7737b356d17adb1055d86f9a625008 Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Sat, 16 May 2026 21:46:22 -0500 Subject: [PATCH 7/9] Add regression test mirroring the FSharp.Core Option.Value scenario Verifies that [] on a union-type member is not rotated to the return value. --- .../Language/AttributeCheckingTests.fs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/FSharp.Compiler.ComponentTests/Language/AttributeCheckingTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/AttributeCheckingTests.fs index d96d8561dda..7464dec733b 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/AttributeCheckingTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/AttributeCheckingTests.fs @@ -126,6 +126,30 @@ type T() = |> shouldFail |> withSingleDiagnostic (Error 429, Line 8, Col 7, Line 8, Col 19, "The attribute type 'AttrAttribute' has 'AllowMultiple=false'. Multiple instances of this attribute cannot be attached to a single language element.") + [] + let ``CompilationRepresentation(Instance) on a union-type member is not rotated to the return value`` () = + FSharp """ +module M +type MyOption<'T> = + | MySome of 'T + | MyNone + [] + member this.Value = match this with | MySome v -> v | MyNone -> failwith "MyNone" + +[] +let main _ = + let m = typeof>.GetMethod("get_Value") + if isNull m then failwith "get_Value not found — CompilationRepresentation(Instance) was likely rotated away from the member" + if m.IsStatic then failwith "get_Value should be an instance method" + if (MySome 42).Value <> 42 then failwith "Value did not return the carried payload" + 0 + """ + |> asExe + |> compile + |> shouldSucceed + |> run + |> shouldSucceed + [] let ``Regression: typechecker does not fail when attribute is on type variable (https://github.com/dotnet/fsharp/issues/13525)`` () = From 37fe0e727c514dea1dbf609c3eb8e175b6a33bcf Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Sun, 17 May 2026 11:02:48 -0500 Subject: [PATCH 8/9] Address Copilot review: drop double-counted return attribs in TcNormalizedBinding SynValData.SynValInfo.retInfo already contains both the return type annotation attribs (via InferSynReturnData in mkSynBinding) and the rotated [] prefix attribs. The previous code also pulled the annotation attribs from rtyOpt, processing them twice. Use SynValData as the single source of truth. --- .../Checking/Expressions/CheckExpressions.fs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index 54aacd392e7..6636901e460 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -11169,7 +11169,7 @@ and TcNormalizedBinding declKind (cenv: cenv) env tpenv overallTy safeThisValOpt let envinner = AddDeclaredTypars NoCheckForDuplicateTypars (enclosingDeclaredTypars@declaredTypars) env match bind with - | NormalizedBinding(vis, kind, isInline, isMutable, attrs, xmlDoc, _, valSynData, pat, NormalizedBindingRhs(spatsL, rtyOpt, rhsExpr), _, debugPoint) -> + | NormalizedBinding(vis, kind, isInline, isMutable, attrs, xmlDoc, _, valSynData, pat, NormalizedBindingRhs(spatsL, _, rhsExpr), _, debugPoint) -> let (SynValData(memberFlags = memberFlagsOpt)) = valSynData let mBinding = pat.Range @@ -11224,19 +11224,16 @@ and TcNormalizedBinding declKind (cenv: cenv) env tpenv overallTy safeThisValOpt attrs // [] attributes are moved out of the binding's prefix and into - // SynValData.SynValInfo.retInfo by SynInfo.RotateReturnAttributes in mkSynBinding. - // Pick them up from there along with any attributes on the return type annotation. + // SynValData.SynValInfo.retInfo by SynInfo.RotateReturnAttributes in mkSynBinding, + // alongside any attributes on the return type annotation populated by InferSynReturnData. + // Use that as the single source of truth. let valAttribs = TcAttrs attrTgt false attrs let retAttribs = - let valSynDataRetSynAttrs = - let (SynValData(_, SynValInfo(_, SynArgInfo(retAttrs, _, _)), _)) = valSynData - retAttrs |> List.collect (fun a -> a.Attributes) - let fromValSyn = TcAttrs AttributeTargets.ReturnValue true valSynDataRetSynAttrs - match rtyOpt with - | Some(SynBindingReturnInfo(attributes = Attributes retAttrs)) -> - fromValSyn @ TcAttrs AttributeTargets.ReturnValue true retAttrs - | None -> fromValSyn + let (SynValData(_, SynValInfo(_, SynArgInfo(retAttrs, _, _)), _)) = valSynData + retAttrs + |> List.collect (fun a -> a.Attributes) + |> TcAttrs AttributeTargets.ReturnValue true let valAttribFlags = computeValWellKnownFlags g valAttribs From 72f0e5322d140437d8c2284c5dd8ac85c73c68c8 Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Mon, 18 May 2026 10:21:25 -0500 Subject: [PATCH 9/9] Add FCS regression tests for method vs return-parameter attribute separation Three tests verify the FCS Symbols API correctly surfaces: - [] on FSharpMemberOrFunctionOrValue.Attributes only - [] on ReturnParameter.Attributes only - Both, independently, when both targets are used on the same member Uses the marker-based Checker.getSymbolUse API and the existing HasAttribute<'T>() member against System.ComponentModel.DescriptionAttribute (AttributeTargets.All) to avoid new test-local helpers. --- .../FSharp.Compiler.Service.Tests/Symbols.fs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/FSharp.Compiler.Service.Tests/Symbols.fs b/tests/FSharp.Compiler.Service.Tests/Symbols.fs index 69bfb70519a..25cc0cd4b6e 100644 --- a/tests/FSharp.Compiler.Service.Tests/Symbols.fs +++ b/tests/FSharp.Compiler.Service.Tests/Symbols.fs @@ -256,6 +256,43 @@ let x = 123 |> Option.map (fun su -> su.Symbol :?> FSharpMemberOrFunctionOrValue) |> Option.iter (fun symbol -> symbol.Attributes.Count |> shouldEqual 1) + [] + let ``FCS - [] surfaces on the method's Attributes only`` () = + let source = """ +open System.ComponentModel +type Calculator() = + [] + member _.Compu{caret}te () = 1 +""" + let mfv = (Checker.getSymbolUse source).Symbol :?> FSharpMemberOrFunctionOrValue + mfv.HasAttribute() |> shouldEqual true + mfv.ReturnParameter.HasAttribute() |> shouldEqual false + + [] + let ``FCS - [] surfaces on ReturnParameter.Attributes only`` () = + let source = """ +open System.ComponentModel +type Calculator() = + [] + member _.Compu{caret}te () = 1 +""" + let mfv = (Checker.getSymbolUse source).Symbol :?> FSharpMemberOrFunctionOrValue + mfv.HasAttribute() |> shouldEqual false + mfv.ReturnParameter.HasAttribute() |> shouldEqual true + + [] + let ``FCS - [] and [] surface independently on the same member`` () = + let source = """ +open System.ComponentModel +type Calculator() = + [] + [] + member _.Compu{caret}te () = 1 +""" + let mfv = (Checker.getSymbolUse source).Symbol :?> FSharpMemberOrFunctionOrValue + mfv.HasAttribute() |> shouldEqual true + mfv.ReturnParameter.HasAttribute() |> shouldEqual true + module Types = [] let ``FSharpType.Print parent namespace qualifiers`` () =