From e02b4f9a32cf5d31586f06ea75b2fe7eee1b4e2f Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Tue, 17 Feb 2026 10:23:34 -0500 Subject: [PATCH 1/6] Fix AttributeUsage.AllowMultiple not inherited for C#-defined attributes (#17107) The F# compiler was not walking the inheritance chain for IL-imported (C#) attribute types when checking AllowMultiple. The supersOfTyconRef function only used tcaug_super, which is not populated for IL types. This fix parameterizes TryFindAttributeUsageAttribute with a getSuper resolver function. The caller in PostInferenceChecks now passes a resolver using GetSuperTypeOfType, which correctly handles both F# and IL-imported types via ILTypeDef.Extends. --- src/Compiler/Checking/PostInferenceChecks.fs | 11 +- src/Compiler/TypedTree/TypedTreeOps.fs | 17 +-- src/Compiler/TypedTree/TypedTreeOps.fsi | 2 +- .../Language/AttributeCheckingTests.fs | 111 ++++++++++++++++++ 4 files changed, 131 insertions(+), 10 deletions(-) diff --git a/src/Compiler/Checking/PostInferenceChecks.fs b/src/Compiler/Checking/PostInferenceChecks.fs index 09e95d5bbf1..aea56ca3d0d 100644 --- a/src/Compiler/Checking/PostInferenceChecks.fs +++ b/src/Compiler/Checking/PostInferenceChecks.fs @@ -2037,7 +2037,16 @@ and CheckAttribs cenv env (attribs: Attribs) = |> Seq.map fst |> Seq.toList // Filter for allowMultiple = false - |> List.filter (fun (tcref, _, m) -> TryFindAttributeUsageAttribute cenv.g m tcref <> Some true) + |> List.filter (fun (tcref, _, m) -> + let getSuper (tcref: TyconRef) = + let ty = generalizedTyconRef cenv.g tcref + match GetSuperTypeOfType cenv.g cenv.amap m ty with + | Some superTy -> + match tryTcrefOfAppTy cenv.g superTy with + | ValueSome sup -> Some sup + | ValueNone -> None + | None -> None + TryFindAttributeUsageAttribute cenv.g m getSuper tcref <> Some true) if cenv.reportErrors then for tcref, _, m in duplicates do diff --git a/src/Compiler/TypedTree/TypedTreeOps.fs b/src/Compiler/TypedTree/TypedTreeOps.fs index 7da5a63eef2..493623af067 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.fs @@ -3514,12 +3514,12 @@ let superOfTycon (g: TcGlobals) (tycon: Tycon) = | None -> g.obj_ty_noNulls | Some ty -> ty -/// walk a TyconRef's inheritance tree, yielding any parent types as an array -let supersOfTyconRef (tcref: TyconRef) = +/// Walk a TyconRef's inheritance tree using the provided super-type resolver, yielding parent types as an array. +let supersOfTyconRefWith (getSuper: TyconRef -> TyconRef option) (tcref: TyconRef) = tcref |> Array.unfold (fun tcref -> - match tcref.TypeContents.tcaug_super with - | Some (TType_app(sup, _, _)) -> Some(sup, sup) - | _ -> None) + match getSuper tcref with + | Some sup -> Some(sup, sup) + | None -> None) //---------------------------------------------------------------------------- // Detect attributes @@ -4174,10 +4174,11 @@ let TryFindTyconRefBoolAttribute g m attribSpec tcref = | [ Some (:? bool as v : obj) ], _ -> Some v | _ -> None) -/// Try to find the resolved attributeusage for an type by walking its inheritance tree and picking the correct attribute usage value -let TryFindAttributeUsageAttribute g m tcref = +/// Try to find the resolved AttributeUsage for a type by walking its inheritance tree and picking the correct attribute usage value. +/// The getSuper function is used to resolve the super-type of each type in the chain, allowing correct handling of both F# and IL-imported types. +let TryFindAttributeUsageAttribute g m (getSuper: TyconRef -> TyconRef option) tcref = [| yield tcref - yield! supersOfTyconRef tcref |] + yield! supersOfTyconRefWith getSuper tcref |] |> Array.tryPick (fun tcref -> TryBindTyconRefAttribute g m g.attrib_AttributeUsageAttribute tcref (fun (_, named) -> named |> List.tryPick (function "AllowMultiple", _, _, ILAttribElem.Bool res -> Some res | _ -> None)) diff --git a/src/Compiler/TypedTree/TypedTreeOps.fsi b/src/Compiler/TypedTree/TypedTreeOps.fsi index 42c0d0b1be4..a4a7b62890f 100755 --- a/src/Compiler/TypedTree/TypedTreeOps.fsi +++ b/src/Compiler/TypedTree/TypedTreeOps.fsi @@ -2515,7 +2515,7 @@ val TyconRefHasWellKnownAttribute: g: TcGlobals -> flag: WellKnownILAttributes - /// Check if a TyconRef has AllowNullLiteralAttribute, returning Some true/Some false/None. val TyconRefAllowsNull: g: TcGlobals -> tcref: TyconRef -> bool option -/// Try to find the AttributeUsage attribute, looking for the value of the AllowMultiple named parameter +/// Try to find the AllowMultiple value of the AttributeUsage attribute on a type definition. val TryFindAttributeUsageAttribute: TcGlobals -> range -> TyconRef -> bool option #if !NO_TYPEPROVIDERS diff --git a/tests/FSharp.Compiler.ComponentTests/Language/AttributeCheckingTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/AttributeCheckingTests.fs index d91997b68f0..898706bca5c 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/AttributeCheckingTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/AttributeCheckingTests.fs @@ -77,3 +77,114 @@ type C() = |> withReferences [csharpBaseClass] |> compile |> shouldSucceed + + [] + let ``C# attribute subclass inherits AllowMultiple true from base`` () = + let csharpLib = + CSharp """ + using System; + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + public class BaseAttribute : Attribute { } + public class ChildAttribute : BaseAttribute { } + """ |> withName "csAttrLib" + + FSharp """ +module Test + +[] +type C() = class end + """ + |> withReferences [csharpLib] + |> compile + |> shouldSucceed + + [] + let ``C# attribute subclass inherits AllowMultiple false from base`` () = + let csharpLib = + CSharp """ + using System; + + [AttributeUsage(AttributeTargets.All, AllowMultiple = false)] + public class BaseAttribute : Attribute { } + public class ChildAttribute : BaseAttribute { } + """ |> withName "csAttrLib" + + FSharp """ +module Test + +[] +type C() = class end + """ + |> withReferences [csharpLib] + |> compile + |> shouldFail + |> withSingleDiagnostic (Error 429, Line 4, Col 10, Line 4, Col 15, "The attribute type 'ChildAttribute' has 'AllowMultiple=false'. Multiple instances of this attribute cannot be attached to a single language element.") + + [] + let ``C# attribute multi-level inheritance inherits AllowMultiple true`` () = + let csharpLib = + CSharp """ + using System; + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + public class BaseAttribute : Attribute { } + public class MiddleAttribute : BaseAttribute { } + public class LeafAttribute : MiddleAttribute { } + """ |> withName "csAttrLib" + + FSharp """ +module Test + +[] +type C() = class end + """ + |> withReferences [csharpLib] + |> compile + |> shouldSucceed + + [] + let ``C# attribute subclass with own AttributeUsage overrides base AllowMultiple`` () = + let csharpLib = + CSharp """ + using System; + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + public class BaseAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.All, AllowMultiple = false)] + public class ChildAttribute : BaseAttribute { } + """ |> withName "csAttrLib" + + FSharp """ +module Test + +[] +type C() = class end + """ + |> withReferences [csharpLib] + |> compile + |> shouldFail + |> withSingleDiagnostic (Error 429, Line 4, Col 10, Line 4, Col 15, "The attribute type 'ChildAttribute' has 'AllowMultiple=false'. Multiple instances of this attribute cannot be attached to a single language element.") + + [] + let ``F# attribute subclass of C# base inherits AllowMultiple true`` () = + let csharpLib = + CSharp """ + using System; + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + public class BaseAttribute : Attribute { } + """ |> withName "csAttrLib" + + FSharp """ +module Test + +type ChildAttribute() = inherit BaseAttribute() + +[] +type C() = class end + """ + |> withReferences [csharpLib] + |> compile + |> shouldSucceed From 086525ffd431da7ebab8a25827ce25bad94a7f82 Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Tue, 17 Feb 2026 11:28:23 -0500 Subject: [PATCH 2/6] Add release note for #17107 AllowMultiple inheritance fix --- docs/release-notes/.FSharp.Compiler.Service/10.0.300.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md index 7a8279aac29..a3633954686 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md @@ -25,6 +25,7 @@ * F# Scripts: Fix default reference paths resolving when an SDK directory is specified. ([PR #19270](https://github.com/dotnet/fsharp/pull/19270)) * Improve static compilation of state machines. ([PR #19297](https://github.com/dotnet/fsharp/pull/19297)) * Fix a bug where `let!` and `use!` were incorrectly allowed outside computation expressions. [PR #19347](https://github.com/dotnet/fsharp/pull/19347) +* Fix `AttributeUsage.AllowMultiple` not being inherited for attributes subclassed in C#. ([Issue #17107](https://github.com/dotnet/fsharp/issues/17107)) ### Added * FSharpType: add ImportILType ([PR #19300](https://github.com/dotnet/fsharp/pull/19300)) From ee4e6a31cdc4c1c49748171ff1f3361f7f0840db Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Tue, 17 Feb 2026 11:28:23 -0500 Subject: [PATCH 3/6] Add release note for #17107 AllowMultiple inheritance fix # Conflicts: # docs/release-notes/.FSharp.Compiler.Service/10.0.300.md --- docs/release-notes/.FSharp.Compiler.Service/10.0.300.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md index a3633954686..7533286a897 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md @@ -26,6 +26,7 @@ * Improve static compilation of state machines. ([PR #19297](https://github.com/dotnet/fsharp/pull/19297)) * Fix a bug where `let!` and `use!` were incorrectly allowed outside computation expressions. [PR #19347](https://github.com/dotnet/fsharp/pull/19347) * Fix `AttributeUsage.AllowMultiple` not being inherited for attributes subclassed in C#. ([Issue #17107](https://github.com/dotnet/fsharp/issues/17107)) +* 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)) ### Added * FSharpType: add ImportILType ([PR #19300](https://github.com/dotnet/fsharp/pull/19300)) From 7834a63d6eb632f094868a194ea98c3771ea4003 Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Fri, 13 Mar 2026 17:06:19 -0500 Subject: [PATCH 4/6] Address review: remove getSuper indirection from TryFindAttributeUsageAttribute Move hierarchy walking logic to PostInferenceChecks.fs where ImportMap is directly available via GetSuperTypeOfType. TryFindAttributeUsageAttribute now performs a single-tcref lookup; the recursive walk up the inheritance chain is handled at the call site in CheckAttribs. --- src/Compiler/Checking/PostInferenceChecks.fs | 21 +++++++++-------- src/Compiler/TypedTree/TypedTreeOps.fs | 24 +++++--------------- src/Compiler/TypedTree/TypedTreeOps.fsi | 3 +++ 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/src/Compiler/Checking/PostInferenceChecks.fs b/src/Compiler/Checking/PostInferenceChecks.fs index aea56ca3d0d..3b40ab34aeb 100644 --- a/src/Compiler/Checking/PostInferenceChecks.fs +++ b/src/Compiler/Checking/PostInferenceChecks.fs @@ -2036,17 +2036,18 @@ and CheckAttribs cenv env (attribs: Attribs) = |> Seq.filter (fun (_, count) -> count > 1) |> Seq.map fst |> Seq.toList - // Filter for allowMultiple = false + // Filter for allowMultiple = false, walking the inheritance chain to find AttributeUsage |> List.filter (fun (tcref, _, m) -> - let getSuper (tcref: TyconRef) = - let ty = generalizedTyconRef cenv.g tcref - match GetSuperTypeOfType cenv.g cenv.amap m ty with - | Some superTy -> - match tryTcrefOfAppTy cenv.g superTy with - | ValueSome sup -> Some sup - | ValueNone -> None - | None -> None - TryFindAttributeUsageAttribute cenv.g m getSuper tcref <> Some true) + let rec allowsMultiple (tcref: TyconRef) = + match TryFindAttributeUsageAttribute cenv.g m tcref with + | Some res -> res + | None -> + generalizedTyconRef cenv.g tcref + |> GetSuperTypeOfType cenv.g cenv.amap m + |> Option.bind (tryTcrefOfAppTy cenv.g >> ValueOption.toOption) + |> Option.map allowsMultiple + |> Option.defaultValue false + not (allowsMultiple tcref)) if cenv.reportErrors then for tcref, _, m in duplicates do diff --git a/src/Compiler/TypedTree/TypedTreeOps.fs b/src/Compiler/TypedTree/TypedTreeOps.fs index 493623af067..8f0c36b72e1 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.fs @@ -3514,13 +3514,6 @@ let superOfTycon (g: TcGlobals) (tycon: Tycon) = | None -> g.obj_ty_noNulls | Some ty -> ty -/// Walk a TyconRef's inheritance tree using the provided super-type resolver, yielding parent types as an array. -let supersOfTyconRefWith (getSuper: TyconRef -> TyconRef option) (tcref: TyconRef) = - tcref |> Array.unfold (fun tcref -> - match getSuper tcref with - | Some sup -> Some(sup, sup) - | None -> None) - //---------------------------------------------------------------------------- // Detect attributes //---------------------------------------------------------------------------- @@ -4174,17 +4167,12 @@ let TryFindTyconRefBoolAttribute g m attribSpec tcref = | [ Some (:? bool as v : obj) ], _ -> Some v | _ -> None) -/// Try to find the resolved AttributeUsage for a type by walking its inheritance tree and picking the correct attribute usage value. -/// The getSuper function is used to resolve the super-type of each type in the chain, allowing correct handling of both F# and IL-imported types. -let TryFindAttributeUsageAttribute g m (getSuper: TyconRef -> TyconRef option) tcref = - [| yield tcref - yield! supersOfTyconRefWith getSuper tcref |] - |> Array.tryPick (fun tcref -> - TryBindTyconRefAttribute g m g.attrib_AttributeUsageAttribute tcref - (fun (_, named) -> named |> List.tryPick (function "AllowMultiple", _, _, ILAttribElem.Bool res -> Some res | _ -> None)) - (fun (Attrib(_, _, _, named, _, _, _)) -> named |> List.tryPick (function AttribNamedArg("AllowMultiple", _, _, AttribBoolArg res ) -> Some res | _ -> None)) - (fun (_, named) -> named |> List.tryPick (function "AllowMultiple", Some (:? bool as res : obj) -> Some res | _ -> None)) - ) +/// Try to find the AllowMultiple value of the AttributeUsage attribute on a type definition. +let TryFindAttributeUsageAttribute g m tcref = + TryBindTyconRefAttribute g m g.attrib_AttributeUsageAttribute tcref + (fun (_, named) -> named |> List.tryPick (function "AllowMultiple", _, _, ILAttribElem.Bool res -> Some res | _ -> None)) + (fun (Attrib(_, _, _, named, _, _, _)) -> named |> List.tryPick (function AttribNamedArg("AllowMultiple", _, _, AttribBoolArg res ) -> Some res | _ -> None)) + (fun (_, named) -> named |> List.tryPick (function "AllowMultiple", Some (:? bool as res : obj) -> Some res | _ -> None)) /// Try to find a specific attribute on a type definition, where the attribute accepts a string argument. /// diff --git a/src/Compiler/TypedTree/TypedTreeOps.fsi b/src/Compiler/TypedTree/TypedTreeOps.fsi index a4a7b62890f..344267b487d 100755 --- a/src/Compiler/TypedTree/TypedTreeOps.fsi +++ b/src/Compiler/TypedTree/TypedTreeOps.fsi @@ -2503,6 +2503,9 @@ val TryFindTyconRefStringAttributeFast: /// Try to find a specific attribute on a type definition, where the attribute accepts a bool argument. val TryFindTyconRefBoolAttribute: TcGlobals -> range -> BuiltinAttribInfo -> TyconRef -> bool option +/// Try to find the AllowMultiple value of the AttributeUsage attribute on a type definition. +val TryFindAttributeUsageAttribute: TcGlobals -> range -> TyconRef -> bool option + /// Try to find a specific attribute on a type definition val TyconRefHasAttribute: TcGlobals -> range -> BuiltinAttribInfo -> TyconRef -> bool From d86734bfa117bb072f09a45874d5aee16e9cbd3b Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Wed, 1 Apr 2026 10:06:53 -0500 Subject: [PATCH 5/6] Move release note for #17107 from 10.0.300.md to 11.0.100.md --- docs/release-notes/.FSharp.Compiler.Service/10.0.300.md | 3 --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md index 7533286a897..b71362d0fec 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md @@ -25,9 +25,6 @@ * F# Scripts: Fix default reference paths resolving when an SDK directory is specified. ([PR #19270](https://github.com/dotnet/fsharp/pull/19270)) * Improve static compilation of state machines. ([PR #19297](https://github.com/dotnet/fsharp/pull/19297)) * Fix a bug where `let!` and `use!` were incorrectly allowed outside computation expressions. [PR #19347](https://github.com/dotnet/fsharp/pull/19347) -* Fix `AttributeUsage.AllowMultiple` not being inherited for attributes subclassed in C#. ([Issue #17107](https://github.com/dotnet/fsharp/issues/17107)) -* 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)) - ### Added * FSharpType: add ImportILType ([PR #19300](https://github.com/dotnet/fsharp/pull/19300)) * Type checker: recover on argument/overload checking ([PR #19314](https://github.com/dotnet/fsharp/pull/19314)) 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 d5c2087765e..b996a1b137c 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -17,6 +17,7 @@ * Fix `YieldFromFinal`/`ReturnFromFinal` being incorrectly called in non-tail positions (`for`, `use`, `use!`, `try/with` handler). ([Issue #19402](https://github.com/dotnet/fsharp/issues/19402), [PR #19403](https://github.com/dotnet/fsharp/pull/19403)) * Fixed how the source ranges of warn directives are reported (as trivia) in the parser output (by not reporting leading spaces). ([Issue #19405](https://github.com/dotnet/fsharp/issues/19405), [PR #19408]((https://github.com/dotnet/fsharp/pull/19408))) * Fix UoM value type `ToString()` returning garbage values when `--checknulls+` is enabled, caused by double address-taking in codegen. ([Issue #19435](https://github.com/dotnet/fsharp/issues/19435), [PR #19440](https://github.com/dotnet/fsharp/pull/19440)) +* 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)) ### Added From 1bdca1841425ac6e0ac8bc77935cdde45d788f66 Mon Sep 17 00:00:00 2001 From: edgargonzalez Date: Wed, 1 Apr 2026 10:24:31 -0500 Subject: [PATCH 6/6] Remove duplicate TryFindAttributeUsageAttribute signature in TypedTreeOps.fsi --- src/Compiler/TypedTree/TypedTreeOps.fsi | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Compiler/TypedTree/TypedTreeOps.fsi b/src/Compiler/TypedTree/TypedTreeOps.fsi index 344267b487d..e0e522fae48 100755 --- a/src/Compiler/TypedTree/TypedTreeOps.fsi +++ b/src/Compiler/TypedTree/TypedTreeOps.fsi @@ -2518,9 +2518,6 @@ val TyconRefHasWellKnownAttribute: g: TcGlobals -> flag: WellKnownILAttributes - /// Check if a TyconRef has AllowNullLiteralAttribute, returning Some true/Some false/None. val TyconRefAllowsNull: g: TcGlobals -> tcref: TyconRef -> bool option -/// Try to find the AllowMultiple value of the AttributeUsage attribute on a type definition. -val TryFindAttributeUsageAttribute: TcGlobals -> range -> TyconRef -> bool option - #if !NO_TYPEPROVIDERS /// returns Some(assemblyName) for success val TryDecodeTypeProviderAssemblyAttr: ILAttribute -> (string | null) option