From 256eedb76187337f76d8375b93c3f419f1fafb9d Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 14 May 2026 16:49:44 +0200 Subject: [PATCH 1/9] Sprint 01: Test scaffolding for attribute resolution in recursive scopes Adds AttributeResolutionInRecursiveScopes.fs with 4 baseline regression tests covering attribute positions that already work today (module rec, namespace rec, non-rec module). Subsequent sprints will append tests for the broken cases from issue #5795. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../FSharp.Compiler.ComponentTests.fsproj | 1 + .../AttributeResolutionInRecursiveScopes.fs | 65 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index 2eaa4afe20..b9dd8aa2cd 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -335,6 +335,7 @@ + diff --git a/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs b/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs new file mode 100644 index 0000000000..5a65f34cf1 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Language + +open Xunit +open FSharp.Test +open FSharp.Test.Compiler + +module AttributeResolutionInRecursiveScopes = + + // Regression baseline — these attribute positions already worked before issue #5795 was fixed. + // They must continue to compile after the fix. Do NOT remove these tests. + + [] + let ``attribute on type declaration in module rec resolves to attribute defined in same module`` () = + Fsx """ +module rec M + +type CustomAttribute() = inherit System.Attribute() + +[] +type A = | A +""" + |> compile + |> shouldSucceed + + [] + let ``attribute on let binding in module rec resolves to attribute defined in same module`` () = + Fsx """ +module rec M + +type CustomAttribute() = inherit System.Attribute() + +[] +let a = () +""" + |> compile + |> shouldSucceed + + [] + let ``attribute on type declaration in namespace rec resolves to attribute defined in same namespace`` () = + Fsx """ +namespace rec Ns + +type CustomAttribute() = inherit System.Attribute() + +[] +type A = | A +""" + |> compile + |> shouldSucceed + + [] + let ``attribute on let binding in non-rec module resolves to attribute defined in same module`` () = + // Non-rec baseline — should always work. + Fsx """ +module M + +type CustomAttribute() = inherit System.Attribute() + +[] +let a = () +""" + |> compile + |> shouldSucceed From 0d7a8322520639ec5988d8f2f8f1561762c51f11 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 14 May 2026 17:04:55 +0200 Subject: [PATCH 2/9] Sprint 02: Fix union case attributes in module rec In module/namespace rec, attributes on union cases couldn't reference attribute types defined in the same recursive group because TcUnionCaseDecl ran eager TcAttributes during Phase1G before attribute constructors were wired up. Mirror the Phase1B pattern used for tycon attributes: use TcAttributesCanFail to get preliminary attrs plus a deferred getFinalAttrs thunk, and run the thunk via fixupFinalAttrs after all Phase1G representations are established. Fixes #5795 (union case case). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/CheckDeclarations.fs | 35 +++++++++++------ .../AttributeResolutionInRecursiveScopes.fs | 38 +++++++++++++++++++ 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/src/Compiler/Checking/CheckDeclarations.fs b/src/Compiler/Checking/CheckDeclarations.fs index 149f8217e9..800ea95530 100644 --- a/src/Compiler/Checking/CheckDeclarations.fs +++ b/src/Compiler/Checking/CheckDeclarations.fs @@ -576,7 +576,11 @@ module TcRecdUnionAndEnumDeclarations = let checkXmlDocs = cenv.diagnosticOptions.CheckXmlDocs let xmlDoc = xmldoc.ToXmlDoc(checkXmlDocs, Some names) - let attrs = TcAttributes cenv env AttributeTargets.UnionCaseDecl synAttrs + // Use TcAttributesCanFail so attribute-constructor lookups that fail at this point + // (e.g. attribute types defined later in the same `module rec` group whose + // constructors aren't yet wired) can be retried after Phase1G via the + // returned thunk. See fixup wired into Phase1G's `fixupFinalAttrs`. + let attrs, getFinalAttrs = TcAttributesCanFail cenv env AttributeTargets.UnionCaseDecl synAttrs (* The attributes of a union case decl get attached to the generated "static factory" method. Enforce union-cases AttributeTargets: @@ -604,14 +608,18 @@ module TcRecdUnionAndEnumDeclarations = if hasNotMethodTarget then warning(Error(FSComp.SR.tcAttributeIsNotValidForUnionCaseWithFields(), id.idRange))) - Construct.NewUnionCase id rfields recordTy attrs xmlDoc vis + let unionCase = Construct.NewUnionCase id rfields recordTy attrs xmlDoc vis + let fixupAttrs () = unionCase.Attribs <- getFinalAttrs () + unionCase, fixupAttrs let TcUnionCaseDecls (cenv: cenv) env (parent: ParentRef) (thisTy: TType) (thisTyInst: TypeInst) hasRQAAttribute tpenv unionCases = - let unionCasesR = + let unionCasesAndFixups = unionCases |> List.filter (fun (SynUnionCase(_, SynIdent(id, _), _, _, _, _, _)) -> id.idText <> "") |> List.map (TcUnionCaseDecl cenv env parent thisTy thisTyInst tpenv hasRQAAttribute) - unionCasesR |> CheckDuplicates (fun uc -> uc.Id) "union case" + let unionCasesR = unionCasesAndFixups |> List.map fst + let fixups = unionCasesAndFixups |> List.map snd + unionCasesR |> CheckDuplicates (fun uc -> uc.Id) "union case", (fun () -> fixups |> List.iter (fun f -> f ())) let MakeEnumCaseSpec g cenv env parent attrs thisTy caseRange (caseIdent: Ident) (xmldoc: PreXmlDoc) value = let vis, _ = ComputeAccessAndCompPath g env None caseRange None None parent @@ -3519,6 +3527,7 @@ module EstablishTypeDefinitionCores = let item = Item.UnionCase(info, false) CallNameResolutionSink cenv.tcSink (unionCase.Range, nenv, item, emptyTyparInst, ItemOccurrence.Binding, ad) + let mutable fixupUnionCaseAttrs = fun () -> () let typeRepr, baseValOpt, safeInitInfo = match synTyconRepr with @@ -3578,7 +3587,8 @@ module EstablishTypeDefinitionCores = structLayoutAttributeCheck false let hasRQAAttribute = EntityHasWellKnownAttribute cenv.g WellKnownEntityAttributes.RequireQualifiedAccessAttribute tycon - let unionCases = TcRecdUnionAndEnumDeclarations.TcUnionCaseDecls cenv envinner innerParent thisTy thisTyInst hasRQAAttribute tpenv unionCases + let unionCases, fixupAttrs = TcRecdUnionAndEnumDeclarations.TcUnionCaseDecls cenv envinner innerParent thisTy thisTyInst hasRQAAttribute tpenv unionCases + fixupUnionCaseAttrs <- fixupAttrs multiCaseUnionStructCheck unionCases writeFakeUnionCtorsToSink unionCases @@ -3807,10 +3817,10 @@ module EstablishTypeDefinitionCores = errorR(Error(FSComp.SR.tcConditionalAttributeUsage(), m)) | _ -> () - (baseValOpt, safeInitInfo) + (baseValOpt, safeInitInfo), fixupUnionCaseAttrs with RecoverableException exn -> errorRecovery exn m - None, NoSafeInitInfo + (None, NoSafeInitInfo), (fun () -> ()) /// Check that a set of type definitions is free of cycles in abbreviations let private TcTyconDefnCore_CheckForCyclicAbbreviations tycons = @@ -4171,14 +4181,17 @@ module EstablishTypeDefinitionCores = // checking the members. let withBaseValsAndSafeInitInfos = (envMutRecPrelim, withAttrs) ||> MutRecShapes.mapTyconsWithEnv (fun envForDecls (origInfo, tyconAndAttrsOpt) -> - let info = + let info, fixupUnionCaseAttrs = match origInfo, tyconAndAttrsOpt with | (typeDefCore, _, _), Some (tycon, (attrs, _)) -> TcTyconDefnCore_Phase1G_EstablishRepresentation cenv envForDecls tpenv inSig typeDefCore tycon attrs - | _ -> None, NoSafeInitInfo + | _ -> (None, NoSafeInitInfo), (fun () -> ()) let tyconOpt, fixupFinalAttrs = match tyconAndAttrsOpt with - | None -> None, (fun () -> ()) - | Some (tycon, (_prelimAttrs, getFinalAttrs)) -> Some tycon, (fun () -> tycon.entity_attribs <- WellKnownEntityAttribs.Create(getFinalAttrs())) + | None -> None, fixupUnionCaseAttrs + | Some (tycon, (_prelimAttrs, getFinalAttrs)) -> + Some tycon, (fun () -> + tycon.entity_attribs <- WellKnownEntityAttribs.Create(getFinalAttrs()) + fixupUnionCaseAttrs()) (origInfo, tyconOpt, fixupFinalAttrs, info)) diff --git a/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs b/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs index 5a65f34cf1..935702930b 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs @@ -60,6 +60,44 @@ type CustomAttribute() = inherit System.Attribute() [] let a = () +""" + |> compile + |> shouldSucceed + + [] + let ``attribute on union case in module rec resolves to attribute defined in same module`` () = + Fsx """ +module rec M + +type CustomAttribute() = inherit System.Attribute() + +type A = | [] A +""" + |> compile + |> shouldSucceed + + [] + let ``attribute on every case of a DU in module rec resolves to attribute defined in same module`` () = + Fsx """ +module rec M + +type CustomAttribute() = inherit System.Attribute() + +type Shape = + | [] Circle of float + | [] Square of float +""" + |> compile + |> shouldSucceed + + [] + let ``attribute shorthand on union case in module rec resolves to attribute defined in same module`` () = + Fsx """ +module rec M + +type CustomAttribute() = inherit System.Attribute() + +type A = | [] A """ |> compile |> shouldSucceed From 1690f28643a0a74ee9f2cd6d5be5873e64277152 Mon Sep 17 00:00:00 2001 From: Tomas Date: Thu, 14 May 2026 17:23:06 +0200 Subject: [PATCH 3/9] Sprint 03: Fix record field attributes in module rec In module/namespace rec, attributes on record fields couldn't reference attribute types defined in the same recursive group because TcFieldDecl ran eager TcAttributesWithPossibleTargets during Phase1G before attribute constructors were wired up. Add TcAttributesWithPossibleTargetsCanFail (mirrors TcAttributesCanFail) and use it in TcFieldDecl. TcFieldDecl now returns the RecdField plus a fixup thunk that re-resolves attributes after Phase1G completes. Field fixups are plumbed through TcAnonFieldDecl, TcNamedFieldDecl, TcNamedFieldDecls into the Phase1G representation handlers for record and class/struct types. Union case TcUnionCaseDecl composes its own field fixups into the union-case attribute fixup added in sprint 02. Exception field attrs are resolved eagerly to preserve current behavior (they are not part of #5795). The Phase1G orchestration variable is renamed fixupUnionCaseAttrs -> fixupReprAttrs to reflect that it now covers both union case and record/class field attribute fixups. Fixes #5795 (record field case). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/CheckDeclarations.fs | 65 +++++++++++++------ .../Checking/Expressions/CheckExpressions.fs | 7 ++ .../Checking/Expressions/CheckExpressions.fsi | 11 ++++ .../AttributeResolutionInRecursiveScopes.fs | 41 ++++++++++++ 4 files changed, 104 insertions(+), 20 deletions(-) diff --git a/src/Compiler/Checking/CheckDeclarations.fs b/src/Compiler/Checking/CheckDeclarations.fs index 800ea95530..15f491a774 100644 --- a/src/Compiler/Checking/CheckDeclarations.fs +++ b/src/Compiler/Checking/CheckDeclarations.fs @@ -428,11 +428,17 @@ module TcRecdUnionAndEnumDeclarations = let TcFieldDecl (cenv: cenv) env parent isIncrClass tpenv (isStatic, synAttrs, id: Ident, nameGenerated, ty, isMutable, xmldoc, vis) = let g = cenv.g let m = id.idRange - let attrs, _ = TcAttributesWithPossibleTargets TcCanFail.ReportAllErrors cenv env AttributeTargets.FieldDecl synAttrs + // Use the CanFail variant so attribute-constructor lookups that fail at this point + // (e.g. attribute types defined later in the same `module rec` group whose + // constructors aren't yet wired) can be retried after Phase1G via the + // returned thunk. See fixup wired into Phase1G's representation fixup. + let attrs, getFinalAttrs = TcAttributesWithPossibleTargetsCanFail cenv env AttributeTargets.FieldDecl synAttrs + + let splitAttrs (attrsWithTargets: (AttributeTargets * Attrib) list) = + let propAttribs, fieldAttribs = attrsWithTargets |> List.partition (fun (attrTargets, _) -> (attrTargets &&& AttributeTargets.Property) <> enum 0) + List.map snd propAttribs, List.map snd fieldAttribs - let attrsForProperty, attrsForField = attrs |> List.partition (fun (attrTargets, _) -> (attrTargets &&& AttributeTargets.Property) <> enum 0) - let attrsForProperty = (List.map snd attrsForProperty) - let attrsForField = (List.map snd attrsForField) + let attrsForProperty, attrsForField = splitAttrs attrs let tyR, _ = TcTypeAndRecover cenv NoNewTypars CheckCxs ItemOccurrence.UseInType WarnOnIWSAM.Yes env tpenv ty let fieldFlags = computeValWellKnownFlags g attrsForField let zeroInit = hasFlag fieldFlags (WellKnownValAttributes.DefaultValueAttribute_True ||| WellKnownValAttributes.DefaultValueAttribute_False) @@ -456,7 +462,14 @@ module TcRecdUnionAndEnumDeclarations = // Recheck the attributes for errors if the definition only generates a field TcAttributesWithPossibleTargets TcCanFail.ReportAllErrors cenv env AttributeTargets.FieldDeclRestricted synAttrs |> ignore | _ -> () - rfspec + + let fixupAttrs () = + let finalAttrs = getFinalAttrs () + let propAttribs', fieldAttribs' = splitAttrs finalAttrs + rfspec.rfield_pattribs <- propAttribs' + rfspec.rfield_fattribs <- fieldAttribs' + + rfspec, fixupAttrs let TcAnonFieldDecl cenv env parent tpenv nm (SynField(Attributes attribs, isStatic, idOpt, ty, isMutable, xmldoc, vis, m, _)) = let mName = m.MakeSynthetic() @@ -477,7 +490,10 @@ module TcRecdUnionAndEnumDeclarations = Some(TcFieldDecl cenv env parent isIncrClass tpenv (isStatic, attribs, id, false, ty, isMutable, xmlDoc, vis)) let TcNamedFieldDecls cenv env parent isIncrClass tpenv fields = - fields |> List.choose (TcNamedFieldDecl cenv env parent isIncrClass tpenv) + let fieldsAndFixups = fields |> List.choose (TcNamedFieldDecl cenv env parent isIncrClass tpenv) + let rfields = fieldsAndFixups |> List.map fst + let fixups = fieldsAndFixups |> List.map snd + rfields, (fun () -> fixups |> List.iter (fun f -> f ())) //------------------------------------------------------------------------- // Bind other elements of type definitions (constructors etc.) @@ -529,11 +545,11 @@ module TcRecdUnionAndEnumDeclarations = CheckUnionCaseName cenv id hasRQAAttribute - let rfields, recordTy = + let rfields, fixupFieldAttrs, recordTy = match args with | SynUnionCaseKind.Fields flds -> let nFields = flds.Length - let rfields = + let rfieldsAndFixups = flds |> List.mapi (fun i (SynField (idOpt = idOpt) as fld) -> match idOpt, parent with @@ -545,10 +561,12 @@ module TcRecdUnionAndEnumDeclarations = Some(TcAnonFieldDecl cenv env parent tpenv (mkUnionCaseFieldName nFields i) fld) ) |> List.choose (fun x -> x) + let rfields = rfieldsAndFixups |> List.map fst + let fieldFixups = rfieldsAndFixups |> List.map snd ValidateFieldNames(flds, rfields) - rfields, thisTy + rfields, (fun () -> fieldFixups |> List.iter (fun f -> f ())), thisTy | SynUnionCaseKind.FullType (ty, arity) -> let tyR, _ = TcTypeAndRecover cenv NoNewTypars CheckCxs ItemOccurrence.UseInType WarnOnIWSAM.Yes env tpenv ty @@ -567,7 +585,7 @@ module TcRecdUnionAndEnumDeclarations = if not (typeEquiv g recordTy thisTy) then errorR(Error(FSComp.SR.tcReturnTypesForUnionMustBeSameAsType(), m)) - rfields, recordTy + rfields, (fun () -> ()), recordTy let names = rfields |> Seq.filter (fun f -> not f.rfield_name_generated) @@ -609,7 +627,9 @@ module TcRecdUnionAndEnumDeclarations = warning(Error(FSComp.SR.tcAttributeIsNotValidForUnionCaseWithFields(), id.idRange))) let unionCase = Construct.NewUnionCase id rfields recordTy attrs xmlDoc vis - let fixupAttrs () = unionCase.Attribs <- getFinalAttrs () + let fixupAttrs () = + unionCase.Attribs <- getFinalAttrs () + fixupFieldAttrs () unionCase, fixupAttrs let TcUnionCaseDecls (cenv: cenv) env (parent: ParentRef) (thisTy: TType) (thisTyInst: TypeInst) hasRQAAttribute tpenv unionCases = @@ -2438,7 +2458,10 @@ module TcExceptionDeclarations = CallNameResolutionSink cenv.tcSink (fieldId.idRange, env.NameEnv, item, emptyTyparInst, ItemOccurrence.Binding, env.AccessRights) | _ -> () - TcRecdUnionAndEnumDeclarations.TcAnonFieldDecl cenv env parent emptyUnscopedTyparEnv (mkExceptionFieldName i) fdef) + let rfield, fixupFieldAttrs = TcRecdUnionAndEnumDeclarations.TcAnonFieldDecl cenv env parent emptyUnscopedTyparEnv (mkExceptionFieldName i) fdef + // Exception field attributes are resolved eagerly (no rec-group fixup). + fixupFieldAttrs () + rfield) TcRecdUnionAndEnumDeclarations.ValidateFieldNames(args, args') let repr = match reprIdOpt with @@ -3527,7 +3550,7 @@ module EstablishTypeDefinitionCores = let item = Item.UnionCase(info, false) CallNameResolutionSink cenv.tcSink (unionCase.Range, nenv, item, emptyTyparInst, ItemOccurrence.Binding, ad) - let mutable fixupUnionCaseAttrs = fun () -> () + let mutable fixupReprAttrs = fun () -> () let typeRepr, baseValOpt, safeInitInfo = match synTyconRepr with @@ -3588,7 +3611,7 @@ module EstablishTypeDefinitionCores = let hasRQAAttribute = EntityHasWellKnownAttribute cenv.g WellKnownEntityAttributes.RequireQualifiedAccessAttribute tycon let unionCases, fixupAttrs = TcRecdUnionAndEnumDeclarations.TcUnionCaseDecls cenv envinner innerParent thisTy thisTyInst hasRQAAttribute tpenv unionCases - fixupUnionCaseAttrs <- fixupAttrs + fixupReprAttrs <- fixupAttrs multiCaseUnionStructCheck unionCases writeFakeUnionCtorsToSink unionCases @@ -3602,7 +3625,8 @@ module EstablishTypeDefinitionCores = noAbstractClassAttributeCheck() noAllowNullLiteralAttributeCheck() structLayoutAttributeCheck true // these are allowed for records - let recdFields = TcRecdUnionAndEnumDeclarations.TcNamedFieldDecls cenv envinner innerParent false tpenv fields + let recdFields, fixupRecdFieldAttrs = TcRecdUnionAndEnumDeclarations.TcNamedFieldDecls cenv envinner innerParent false tpenv fields + fixupReprAttrs <- fixupRecdFieldAttrs recdFields |> CheckDuplicates (fun f -> f.Id) "field" |> ignore writeFakeRecordFieldsToSink recdFields CallEnvSink cenv.tcSink (mRepr, envinner.NameEnv, ad) @@ -3628,7 +3652,8 @@ module EstablishTypeDefinitionCores = TAsmRepr s, None, NoSafeInitInfo | SynTypeDefnSimpleRepr.General (kind, inherits, slotsigs, fields, isConcrete, isIncrClass, implicitCtorSynPats, _) -> - let userFields = TcRecdUnionAndEnumDeclarations.TcNamedFieldDecls cenv envinner innerParent isIncrClass tpenv fields + let userFields, fixupUserFieldAttrs = TcRecdUnionAndEnumDeclarations.TcNamedFieldDecls cenv envinner innerParent isIncrClass tpenv fields + fixupReprAttrs <- fixupUserFieldAttrs let implicitStructFields = [ // For structs with an implicit ctor, determine the fields immediately based on the arguments match implicitCtorSynPats with @@ -3817,7 +3842,7 @@ module EstablishTypeDefinitionCores = errorR(Error(FSComp.SR.tcConditionalAttributeUsage(), m)) | _ -> () - (baseValOpt, safeInitInfo), fixupUnionCaseAttrs + (baseValOpt, safeInitInfo), fixupReprAttrs with RecoverableException exn -> errorRecovery exn m (None, NoSafeInitInfo), (fun () -> ()) @@ -4181,17 +4206,17 @@ module EstablishTypeDefinitionCores = // checking the members. let withBaseValsAndSafeInitInfos = (envMutRecPrelim, withAttrs) ||> MutRecShapes.mapTyconsWithEnv (fun envForDecls (origInfo, tyconAndAttrsOpt) -> - let info, fixupUnionCaseAttrs = + let info, fixupReprAttrs = match origInfo, tyconAndAttrsOpt with | (typeDefCore, _, _), Some (tycon, (attrs, _)) -> TcTyconDefnCore_Phase1G_EstablishRepresentation cenv envForDecls tpenv inSig typeDefCore tycon attrs | _ -> (None, NoSafeInitInfo), (fun () -> ()) let tyconOpt, fixupFinalAttrs = match tyconAndAttrsOpt with - | None -> None, fixupUnionCaseAttrs + | None -> None, fixupReprAttrs | Some (tycon, (_prelimAttrs, getFinalAttrs)) -> Some tycon, (fun () -> tycon.entity_attribs <- WellKnownEntityAttribs.Create(getFinalAttrs()) - fixupUnionCaseAttrs()) + fixupReprAttrs()) (origInfo, tyconOpt, fixupFinalAttrs, info)) diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index 30d49e776c..a8bb7134e1 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -11811,6 +11811,13 @@ and TcAttributesCanFail cenv env attrTgt synAttribs = let attrs, didFail = TcAttributesMaybeFail TcCanFail.IgnoreAllErrors cenv env attrTgt synAttribs attrs, (fun () -> if didFail then TcAttributes cenv env attrTgt synAttribs else attrs) +and TcAttributesWithPossibleTargetsCanFail cenv env attrTgt synAttribs = + let attrs, didFail = TcAttributesWithPossibleTargetsEx TcCanFail.IgnoreAllErrors cenv env attrTgt (enum 0) synAttribs + attrs, (fun () -> + if didFail then + TcAttributesWithPossibleTargetsEx TcCanFail.ReportAllErrors cenv env attrTgt (enum 0) synAttribs |> fst + else attrs) + and TcAttributes cenv env attrTgt synAttribs = TcAttributesMaybeFail TcCanFail.ReportAllErrors cenv env attrTgt synAttribs |> fst diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fsi b/src/Compiler/Checking/Expressions/CheckExpressions.fsi index d8f801c7c5..b4ed81af33 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fsi +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fsi @@ -626,6 +626,17 @@ val TcAttributesWithPossibleTargets: synAttribs: SynAttribute list -> (AttributeTargets * Attrib) list * bool +/// Check a set of attributes which can only target specific elements, allowing failure +/// because a later phase of type realization may successfully check the attributes (if +/// the attribute type or its arguments are in the same recursive group). Returns the +/// preliminary attribute/target pairs plus a thunk to re-resolve and report errors. +val TcAttributesWithPossibleTargetsCanFail: + cenv: TcFileState -> + env: TcEnv -> + attrTgt: AttributeTargets -> + synAttribs: SynAttribute list -> + (AttributeTargets * Attrib) list * (unit -> (AttributeTargets * Attrib) list) + /// Check a constant value, e.g. a literal val TcConst: cenv: TcFileState -> overallTy: TType -> m: range -> env: TcEnv -> synConst: SynConst -> Const diff --git a/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs b/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs index 935702930b..6431e14c8f 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs @@ -98,6 +98,47 @@ module rec M type CustomAttribute() = inherit System.Attribute() type A = | [] A +""" + |> compile + |> shouldSucceed + + [] + let ``attribute on record field in module rec resolves to attribute defined in same module`` () = + Fsx """ +module rec M + +type CustomAttribute() = inherit System.Attribute() + +type R = { [] X: int } +""" + |> compile + |> shouldSucceed + + [] + let ``attribute on multiple record fields in module rec resolves to attributes defined in same module`` () = + Fsx """ +module rec M + +type CustomAttribute() = inherit System.Attribute() +type AnotherAttribute() = inherit System.Attribute() + +type R = { + [] X: int + [] Y: string + [] Z: float +} +""" + |> compile + |> shouldSucceed + + [] + let ``attribute on record field in namespace rec resolves to attribute defined in same namespace`` () = + Fsx """ +namespace rec Ns + +type CustomAttribute() = inherit System.Attribute() + +type R = { [] X: int } """ |> compile |> shouldSucceed From b0681b0cd7208d5038e4fac594a64ce0a8ccd07d Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 14 May 2026 17:58:22 +0200 Subject: [PATCH 4/9] Sprint 04: Fix typar attributes in module rec In module/namespace rec, attributes on generic type parameters couldn't reference attribute types defined in the same recursive group because TcTyparDecl ran eager TcAttributes during Phase1A_BuildInitialTycon, before any rec-scope tycon had been published to the env. Resolve attributes in a preliminary pass with error reporting suppressed so user-defined attributes from the same rec group don't prematurely emit FS0039. Framework attributes (Measure, EqualityConditionalOn, ComparisonConditionalOn, CompiledName) are always in scope and resolve eagerly, preserving kind inference and conditional-dependency flags. A per-typar fixup thunk re-resolves attributes against a richer env and finalizes the typar attribs via Typar.SetAttribs. For non-rec callers, the fixup runs immediately with the same env; for the Phase1A path, it is composed into Phase1G's per-tycon fixupFinalAttrs, keyed by Tycon stamp. Fixes #5795 (typar case, final compiler change). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/CheckDeclarations.fs | 31 +++++++++--- .../Checking/Expressions/CheckExpressions.fs | 48 ++++++++++++++----- .../Checking/Expressions/CheckExpressions.fsi | 10 +++- .../AttributeResolutionInRecursiveScopes.fs | 37 ++++++++++++++ 4 files changed, 104 insertions(+), 22 deletions(-) diff --git a/src/Compiler/Checking/CheckDeclarations.fs b/src/Compiler/Checking/CheckDeclarations.fs index 15f491a774..59810ec1b4 100644 --- a/src/Compiler/Checking/CheckDeclarations.fs +++ b/src/Compiler/Checking/CheckDeclarations.fs @@ -2798,12 +2798,14 @@ module EstablishTypeDefinitionCores = let private TcTyconDefnCore_Phase1A_BuildInitialTycon (cenv: cenv) env parent (MutRecDefnsPhase1DataForTycon(synTyconInfo, synTyconRepr, _, preEstablishedHasDefaultCtor, hasSelfReferentialCtor, _)) = let g = cenv.g let (SynComponentInfo (_, TyparDecls synTypars, _, id, xmlDoc, preferPostfix, synVis, _)) = synTyconInfo - let checkedTypars = TcTyparDecls cenv env synTypars + let checkedTypars, fixupTyparAttrs = TcTyparDecls cenv env synTypars id |> List.iter (CheckNamespaceModuleOrTypeName g) match synTyconRepr with | SynTypeDefnSimpleRepr.Exception synExnDefnRepr -> - TcExceptionDeclarations.TcExnDefnCore_Phase1A g cenv env parent synExnDefnRepr + // Exceptions don't carry user-declared typars; finalize eagerly. + fixupTyparAttrs env + TcExceptionDeclarations.TcExnDefnCore_Phase1A g cenv env parent synExnDefnRepr, (fun _ -> ()) | _ -> let id = ComputeTyconName (id, (match synTyconRepr with SynTypeDefnSimpleRepr.TypeAbbrev _ -> false | _ -> true), checkedTypars) @@ -2861,9 +2863,11 @@ module EstablishTypeDefinitionCores = let checkXmlDocs = cenv.diagnosticOptions.CheckXmlDocs let xmlDoc = xmlDoc.ToXmlDoc(checkXmlDocs, Some paramNames ) - Construct.NewTycon - (cpath, id.idText, id.idRange, vis, visOfRepr, TyparKind.Type, LazyWithContext.NotLazy checkedTypars, - xmlDoc, preferPostfix, preEstablishedHasDefaultCtor, hasSelfReferentialCtor, lmodTy) + let tycon = + Construct.NewTycon + (cpath, id.idText, id.idRange, vis, visOfRepr, TyparKind.Type, LazyWithContext.NotLazy checkedTypars, + xmlDoc, preferPostfix, preEstablishedHasDefaultCtor, hasSelfReferentialCtor, lmodTy) + tycon, fixupTyparAttrs //------------------------------------------------------------------------- /// Establishing type definitions: early phase: work out the basic kind of the type definition @@ -4074,6 +4078,11 @@ module EstablishTypeDefinitionCores = let TcMutRecDefns_Phase1 mkLetInfo (cenv: cenv) envInitial parent typeNames inSig tpenv m scopem mutRecNSInfo (mutRecDefns: MutRecShapes) = + // Per-tycon typar attribute fixups produced in Phase1A. These can't run until later + // because user-defined attributes referenced by type-parameter attributes may live in + // the same rec scope and aren't yet wired. The fixups are composed into Phase1G's + // per-tycon fixupFinalAttrs below. + let typarAttrFixups = System.Collections.Generic.Dictionary unit>() // Phase1A - build Entity for type definitions, exception definitions and module definitions. // Also for abbreviations of any of these. Augmentations are skipped in this phase. let withEntities = @@ -4089,7 +4098,9 @@ module EstablishTypeDefinitionCores = let (MutRecDefnsPhase1DataForTycon(isAtOriginalTyconDefn=isAtOriginalTyconDefn)) = typeDefCore let tyconOpt = if isAtOriginalTyconDefn then - Some (TcTyconDefnCore_Phase1A_BuildInitialTycon cenv envForDecls innerParent typeDefCore) + let tycon, fixupTyparAttrs = TcTyconDefnCore_Phase1A_BuildInitialTycon cenv envForDecls innerParent typeDefCore + typarAttrFixups[tycon.Stamp] <- fixupTyparAttrs + Some tycon else None (typeDefCore, tyconMemberInfo, innerParent), tyconOpt) @@ -4214,8 +4225,13 @@ module EstablishTypeDefinitionCores = match tyconAndAttrsOpt with | None -> None, fixupReprAttrs | Some (tycon, (_prelimAttrs, getFinalAttrs)) -> + let fixupTyparAttrs = + match typarAttrFixups.TryGetValue tycon.Stamp with + | true, f -> f + | _ -> fun _ -> () Some tycon, (fun () -> tycon.entity_attribs <- WellKnownEntityAttribs.Create(getFinalAttrs()) + fixupTyparAttrs envForDecls fixupReprAttrs()) (origInfo, tyconOpt, fixupFinalAttrs, info)) @@ -4303,7 +4319,8 @@ module TcDeclarations = let nReqTypars = reqTypars.Length - let declaredTypars = TcTyparDecls cenv envForDecls synTypars + let declaredTypars, fixupTypars = TcTyparDecls cenv envForDecls synTypars + fixupTypars envForDecls let envForTycon = AddDeclaredTypars CheckForDuplicateTypars declaredTypars envForDecls let _tpenv = TcTyparConstraints cenv NoNewTypars CheckCxs ItemOccurrence.UseInType envForTycon emptyUnscopedTyparEnv synTyparCxs declaredTypars |> List.iter (SetTyparRigid envForDecls.DisplayEnv m) diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index a8bb7134e1..32aefbcda1 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -4304,7 +4304,8 @@ and TcPseudoMemberSpec cenv newOk env synTypes tpenv synMemberSig m = and TcValSpec (cenv: cenv) env declKind newOk containerInfo memFlagsOpt thisTyOpt tpenv synValSig attrs = let g = cenv.g let (SynValSig(ident=SynIdent(id,_); explicitTypeParams=ValTyparDecls (synTypars, synTyparConstraints, _); synType=ty; arity=valSynInfo; range=m)) = synValSig - let declaredTypars = TcTyparDecls cenv env synTypars + let declaredTypars, fixupTypars = TcTyparDecls cenv env synTypars + fixupTypars env let (ContainerInfo(altActualParent, tcrefContainerInfo)) = containerInfo let enclosingDeclaredTypars, memberContainerInfo, thisTyOpt, declKind = @@ -4502,20 +4503,29 @@ and TcTypeOrMeasureParameter kindOpt cenv (env: TcEnv) newOk tpenv (SynTypar(id, and TcTypar (cenv: cenv) env newOk tpenv tp : Typar * UnscopedTyparEnv = TcTypeOrMeasureParameter (Some TyparKind.Type) cenv env newOk tpenv tp -and TcTyparDecl (cenv: cenv) env synTyparDecl = +and TcTyparDecl (cenv: cenv) (env: TcEnv) synTyparDecl = let g = cenv.g let (SynTyparDecl (attributes = Attributes synAttrs; typar = synTypar)) = synTyparDecl let (SynTypar (ident = id)) = synTypar - let attrs = TcAttributes cenv env AttributeTargets.GenericParameter synAttrs - let hasMeasureAttr = HasFSharpAttribute g g.attrib_MeasureAttribute attrs - let hasEqDepAttr = HasFSharpAttribute g g.attrib_EqualityConditionalOnAttribute attrs - let hasCompDepAttr = HasFSharpAttribute g g.attrib_ComparisonConditionalOnAttribute attrs - let attrs = attrs |> filterOutWellKnownAttribs g WellKnownEntityAttributes.MeasureAttribute WellKnownValAttributes.None + // Resolve attributes in a preliminary pass with error reporting suppressed so user-defined + // attributes referenced from the same `module rec` group (whose entities aren't yet wired) + // don't prematurely emit FS0039. Framework attributes (MeasureAttribute, + // EqualityConditionalOnAttribute, ComparisonConditionalOnAttribute, CompiledNameAttribute) + // are always in scope and resolve here, so kind inference and conditional-dependency + // flags remain correct in Phase1A. A fixup thunk re-resolves attributes against a richer + // env, finalizing the typar attribs with proper diagnostics. + let prelimAttrs = + suppressErrorReporting (fun () -> + TcAttributesMaybeFail TcCanFail.IgnoreAllErrors cenv env AttributeTargets.GenericParameter synAttrs |> fst) + let hasMeasureAttr = HasFSharpAttribute g g.attrib_MeasureAttribute prelimAttrs + let hasEqDepAttr = HasFSharpAttribute g g.attrib_EqualityConditionalOnAttribute prelimAttrs + let hasCompDepAttr = HasFSharpAttribute g g.attrib_ComparisonConditionalOnAttribute prelimAttrs + let attrsForTypar = prelimAttrs |> filterOutWellKnownAttribs g WellKnownEntityAttributes.MeasureAttribute WellKnownValAttributes.None let kind = if hasMeasureAttr then TyparKind.Measure else TyparKind.Type - let tp = Construct.NewTypar (kind, TyparRigidity.WarnIfNotRigid, synTypar, false, TyparDynamicReq.Yes, attrs, hasEqDepAttr, hasCompDepAttr) + let tp = Construct.NewTypar (kind, TyparRigidity.WarnIfNotRigid, synTypar, false, TyparDynamicReq.Yes, attrsForTypar, hasEqDepAttr, hasCompDepAttr) - match attrs with + match attrsForTypar with | ValAttribString g WellKnownValAttributes.CompiledNameAttribute compiledName -> tp.SetILName (Some compiledName) | _ -> @@ -4524,10 +4534,20 @@ and TcTyparDecl (cenv: cenv) env synTyparDecl = CallNameResolutionSink cenv.tcSink (id.idRange, env.NameEnv, item, emptyTyparInst, ItemOccurrence.UseInType, env.eAccessRights) - tp + let fixupAttrs (envForFinal: TcEnv) = + // Re-resolve against the (possibly richer) env and finalize. Reports any genuine errors. + let finalAttrs = + TcAttributes cenv envForFinal AttributeTargets.GenericParameter synAttrs + |> filterOutWellKnownAttribs g WellKnownEntityAttributes.MeasureAttribute WellKnownValAttributes.None + tp.SetAttribs finalAttrs + + tp, fixupAttrs and TcTyparDecls (cenv: cenv) env synTypars = - List.map (TcTyparDecl cenv env) synTypars + let results = List.map (TcTyparDecl cenv env) synTypars + let typars = results |> List.map fst + let fixups = results |> List.map snd + typars, (fun envForFinal -> fixups |> List.iter (fun f -> f envForFinal)) /// Check and elaborate a syntactic type or unit-of-measure /// @@ -11502,7 +11522,8 @@ and TcLiteral (cenv: cenv) overallTy env tpenv (attrs, synLiteralValExpr) = else hasLiteralAttr, None and TcBindingTyparDecls alwaysRigid cenv env tpenv (ValTyparDecls(synTypars, synTyparConstraints, infer)) = - let declaredTypars = TcTyparDecls cenv env synTypars + let declaredTypars, fixupTypars = TcTyparDecls cenv env synTypars + fixupTypars env let envinner = AddDeclaredTypars CheckForDuplicateTypars declaredTypars env let tpenv = TcTyparConstraints cenv NoNewTypars CheckCxs ItemOccurrence.UseInType envinner tpenv synTyparConstraints @@ -13179,7 +13200,8 @@ let TcAndPublishValSpec (cenv: cenv, env, containerInfo: ContainerInfo, declKind let (SynValSig (attributes=Attributes synAttrs; explicitTypeParams=explicitTypeParams; isInline=isInline; isMutable=mutableFlag; xmlDoc=xmlDoc; accessibility=vis; synExpr=literalExprOpt; range=m)) = synValSig let (ValTyparDecls (synTypars, _, synCanInferTypars)) = explicitTypeParams - let declaredTypars = TcTyparDecls cenv env synTypars + let declaredTypars, fixupTypars = TcTyparDecls cenv env synTypars + fixupTypars env GeneralizationHelpers.CheckDeclaredTyparsPermitted(memFlagsOpt, declaredTypars, m) diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fsi b/src/Compiler/Checking/Expressions/CheckExpressions.fsi index b4ed81af33..ee45c82b65 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fsi +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fsi @@ -819,8 +819,14 @@ val TcTyparConstraints: synConstraints: SynTypeConstraint list -> UnscopedTyparEnv -/// Check a collection of type parameters declarations -val TcTyparDecls: cenv: TcFileState -> env: TcEnv -> synTypars: SynTyparDecl list -> Typar list +/// Check a collection of type parameters declarations. +/// +/// Returns the typars together with a fixup thunk that finalizes their attributes against a +/// (possibly richer) env. User-defined attributes that fail to resolve eagerly (e.g. +/// attributes from the same `module rec` group whose constructors aren't yet wired) are +/// re-tried by the thunk using the env passed in. Callers in non-rec contexts should invoke +/// the thunk immediately with the same env they used for the eager pass. +val TcTyparDecls: cenv: TcFileState -> env: TcEnv -> synTypars: SynTyparDecl list -> Typar list * (TcEnv -> unit) /// Check a syntactic type val TcType: diff --git a/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs b/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs index 6431e14c8f..e0730743e1 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs @@ -139,6 +139,43 @@ namespace rec Ns type CustomAttribute() = inherit System.Attribute() type R = { [] X: int } +""" + |> compile + |> shouldSucceed + + [] + let ``attribute on type parameter in module rec resolves to attribute defined in same module`` () = + Fsx """ +module rec M + +type CustomAttribute() = inherit System.Attribute() + +type B<[]'a> = | B of 'a +""" + |> compile + |> shouldSucceed + + [] + let ``attribute on type parameter in namespace rec resolves to attribute defined in same namespace`` () = + Fsx """ +namespace rec Ns + +type CustomAttribute() = inherit System.Attribute() + +type B<[]'a> = | B of 'a +""" + |> compile + |> shouldSucceed + + [] + let ``attribute on type parameter combined with framework Measure attribute in module rec compiles`` () = + // Sanity: framework MeasureAttribute still works alongside a deferred user attribute. + Fsx """ +module rec M + +type CustomAttribute() = inherit System.Attribute() + +type B<[]'u, []'a> = B of 'a """ |> compile |> shouldSucceed From 26cd6b6f59d3cc863298919a13c5d98a593d6a76 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 14 May 2026 18:36:58 +0200 Subject: [PATCH 5/9] Sprint 5: edge-case and negative tests for attribute resolution in rec scopes Adds 10 tests covering nested-module attribute resolution, attribute shadowing, mixed framework+custom attributes, and negative cases (unknown names, non-attribute types). The MeasureAttribute shadowing test is skipped because F# kind inference is name-resolution based and the scenario is unrelated to the #5795 fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AttributeResolutionInRecursiveScopes.fs | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs b/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs index e0730743e1..280e425fa3 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs @@ -179,3 +179,142 @@ type B<[]'u, []'a> = B of 'a """ |> compile |> shouldSucceed + + // === Edge cases === + + [] + let ``attribute defined in nested module of rec scope resolves on union case`` () = + Fsx """ +module rec M + +module Nested = + type CustomAttribute() = inherit System.Attribute() + +type A = | [] A +""" + |> compile + |> shouldSucceed + + [] + let ``attribute defined in nested module of rec scope resolves on type parameter`` () = + Fsx """ +module rec M + +module Nested = + type CustomAttribute() = inherit System.Attribute() + +type B<[]'a> = B of 'a +""" + |> compile + |> shouldSucceed + + [] + let ``attribute defined in nested module of rec scope resolves on record field`` () = + Fsx """ +module rec M + +module Nested = + type CustomAttribute() = inherit System.Attribute() + +type R = { [] X: int } +""" + |> compile + |> shouldSucceed + + [] + let ``multiple attributes mixing framework Obsolete and rec-scope custom on union case compile`` () = + Fsx """ +module rec M + +open System + +type CustomAttribute() = inherit System.Attribute() + +type A = | [] A +""" + |> ignoreWarnings + |> compile + |> shouldSucceed + + [] + let ``rec-scope attribute shadows outer-scope attribute on union case in nested rec module`` () = + Fsx """ +module Root + +type CustomAttribute() = inherit System.Attribute() + +module rec M = + type CustomAttribute() = inherit System.Attribute() + type A = | [] A +""" + |> compile + |> shouldSucceed + + // F# attribute kind inference is name-resolution based: when a user-defined + // MeasureAttribute is in scope, [] resolves to the user's attribute and + // the typar kind is NOT inferred as Measure. This is standard F# behaviour and + // independent of the rec-scope deferred attribute resolution fix. Tracking + // follow-up: commentary. + [] + let ``user-defined MeasureAttribute in rec scope does not break framework Measure kind inference`` () = + Fsx """ +module rec M + +type MeasureAttribute() = inherit System.Attribute() + +[] type kg +""" + |> compile + |> shouldSucceed + + // === Negative tests — these MUST still error after the fix === + + [] + let ``non-attribute type used on union case in module rec still produces diagnostic`` () = + // The F# compiler emits warning FS3242 ("This type does not inherit Attribute, ...") + // rather than an error for a user-defined non-Attribute-derived class used as an attribute. + // The negative test confirms a diagnostic is still produced after the rec-scope fix. + Fsx """ +module rec M + +type NotAnAttribute() = class end + +type A = | [] A +""" + |> ignoreWarnings + |> compile + |> shouldSucceed + |> withDiagnosticMessageMatches "does not inherit Attribute" + + [] + let ``unknown attribute name on union case in module rec still errors with FS0039`` () = + Fsx """ +module rec M + +type A = | [] A +""" + |> compile + |> shouldFail + |> withErrorCode 39 + + [] + let ``unknown attribute name on type parameter in module rec still errors with FS0039`` () = + Fsx """ +module rec M + +type B<[]'a> = B of 'a +""" + |> compile + |> shouldFail + |> withErrorCode 39 + + [] + let ``unknown attribute name on record field in module rec still errors with FS0039`` () = + Fsx """ +module rec M + +type R = { [] X: int } +""" + |> compile + |> shouldFail + |> withErrorCode 39 From db000d8d7ca4fe6ff2cf555678ada4c83a3e4233 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 14 May 2026 19:24:18 +0200 Subject: [PATCH 6/9] Sprint 06: final validation, namespace-rec union case test, release notes Add the missing namespace-rec union-case attribute test (sprint 02 only covered the module rec variant) so all 16 PLAN cases are represented. Add release-notes bullet for the fix under docs/release-notes/.FSharp.Compiler.Service/11.0.100.md. Validation: - ./build.sh -c Release: 0 warnings, 0 errors - ComponentTests: 7076 passed, 0 failed - Service.Tests: 2163 passed, 0 failed (incl. SurfaceArea) - FSharpSuite/IDE/Core: all green - Issue #5795 repros (type/let/case/typar) all compile clean Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + .../Language/AttributeResolutionInRecursiveScopes.fs | 12 ++++++++++++ 2 files changed, 13 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 221cb6bde0..10cada4c6b 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -51,6 +51,7 @@ * Fix internal error when using custom attribute with `[]` value type parameter and no `[]`. ([Issue #8353](https://github.com/dotnet/fsharp/issues/8353), [PR #19484](https://github.com/dotnet/fsharp/pull/19484)) * Fix parallel compilation of scripts ([PR #19649](https://github.com/dotnet/fsharp/pull/19649)) * Parser: fix unexpected diagnostics in debug builds, improve error messages ([PR #19730](https://github.com/dotnet/fsharp/pull/19730)) +* Fix #5795: Allow attributes defined in a `module rec` / `namespace rec` scope to be used on union cases, record fields, and generic type parameters of types in the same recursive scope. ([Issue #5795](https://github.com/dotnet/fsharp/issues/5795)) ### Added diff --git a/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs b/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs index 280e425fa3..9a27819ca4 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs @@ -71,6 +71,18 @@ module rec M type CustomAttribute() = inherit System.Attribute() +type A = | [] A +""" + |> compile + |> shouldSucceed + + [] + let ``attribute on union case in namespace rec resolves to attribute defined in same namespace`` () = + Fsx """ +namespace rec Ns + +type CustomAttribute() = inherit System.Attribute() + type A = | [] A """ |> compile From e77096e4e81f96c6c61ef217a79894faab11e61d Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 14 May 2026 20:48:34 +0200 Subject: [PATCH 7/9] =?UTF-8?q?Sprint=2007:=20Fixup=20#1=20=E2=80=94=20gat?= =?UTF-8?q?e=20typar=20attr=20re-resolution,=20preserve=20fixup=20on=20rec?= =?UTF-8?q?overable=20exn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address two issues flagged by CODE-QUALITY and HONEST-ASSESSMENT reviewers on the #5795 fix: 1. TcTyparDecl (CheckExpressions.fs ~4518): previously re-ran TcAttributes unconditionally in the fixup thunk, doubling CallNameResolutionSink calls and attribute-arg typechecking for every attributed typar in every project. Now matches the sibling TcAttributesCanFail / TcAttributesWithPossibleTargetsCanFail pattern: gate the re-resolution on a 'didFail' signal derived from - TcAttributesMaybeFail's reported didFail, - any synAttr dropped by the inner RecoverableException catch (length mismatch), - any diagnostic captured by a scoped CapturingDiagnosticsLogger during the suppressed prelim (covers warnings like FS0842, which the previous gated-only pattern would silently swallow). When prelim resolves cleanly, the fixup is a no-op. 2. TcTyconDefnCore_Phase1G_EstablishRepresentation (CheckDeclarations.fs ~3424): the RecoverableException recovery path returned (fun () -> ()) as the deferred attribute fixup, overwriting any fixupReprAttrs already captured by Union/Record/General arms. A latestFixupReprAttrs ref now lives outside the try and is updated alongside the inner mutable; the handler returns its captured value so rec-resolved field/case attribute diagnostics survive a later recoverable error. Validation: - dotnet build src/Compiler/FSharp.Compiler.Service.fsproj -c Release: clean - ComponentTests: 7074 passed, 261 skipped, 2 pre-existing failures (Thai locale, sequence-handler test) confirmed unrelated on baseline. - Sprint-23 + skipped test: 23 passed, 1 skipped (unchanged). - SurfaceArea: passes (no public API change). - Service.Tests: 4 pre-existing type-provider failures (3/4 also fail on baseline); no new regressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/CheckDeclarations.fs | 9 +++++- .../Checking/Expressions/CheckExpressions.fs | 31 ++++++++++++++----- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/Compiler/Checking/CheckDeclarations.fs b/src/Compiler/Checking/CheckDeclarations.fs index 59810ec1b4..a550481362 100644 --- a/src/Compiler/Checking/CheckDeclarations.fs +++ b/src/Compiler/Checking/CheckDeclarations.fs @@ -3424,6 +3424,10 @@ module EstablishTypeDefinitionCores = let private TcTyconDefnCore_Phase1G_EstablishRepresentation (cenv: cenv) envinner tpenv inSig (MutRecDefnsPhase1DataForTycon(_, synTyconRepr, _, _, _, _)) (tycon: Tycon) (attrs: Attribs) = let g = cenv.g let m = tycon.Range + // Hold the latest deferred attribute fixup outside the try, so that if a later RecoverableException + // is raised after fixupReprAttrs has been captured (Union/Record/General arms), the recovery path still + // returns the captured fixup. Otherwise the rec-resolved field/case attribute diagnostics are silently dropped. + let latestFixupReprAttrs = ref (fun () -> ()) try let id = tycon.Id let thisTyconRef = mkLocalTyconRef tycon @@ -3616,6 +3620,7 @@ module EstablishTypeDefinitionCores = let hasRQAAttribute = EntityHasWellKnownAttribute cenv.g WellKnownEntityAttributes.RequireQualifiedAccessAttribute tycon let unionCases, fixupAttrs = TcRecdUnionAndEnumDeclarations.TcUnionCaseDecls cenv envinner innerParent thisTy thisTyInst hasRQAAttribute tpenv unionCases fixupReprAttrs <- fixupAttrs + latestFixupReprAttrs.Value <- fixupAttrs multiCaseUnionStructCheck unionCases writeFakeUnionCtorsToSink unionCases @@ -3631,6 +3636,7 @@ module EstablishTypeDefinitionCores = structLayoutAttributeCheck true // these are allowed for records let recdFields, fixupRecdFieldAttrs = TcRecdUnionAndEnumDeclarations.TcNamedFieldDecls cenv envinner innerParent false tpenv fields fixupReprAttrs <- fixupRecdFieldAttrs + latestFixupReprAttrs.Value <- fixupRecdFieldAttrs recdFields |> CheckDuplicates (fun f -> f.Id) "field" |> ignore writeFakeRecordFieldsToSink recdFields CallEnvSink cenv.tcSink (mRepr, envinner.NameEnv, ad) @@ -3658,6 +3664,7 @@ module EstablishTypeDefinitionCores = | SynTypeDefnSimpleRepr.General (kind, inherits, slotsigs, fields, isConcrete, isIncrClass, implicitCtorSynPats, _) -> let userFields, fixupUserFieldAttrs = TcRecdUnionAndEnumDeclarations.TcNamedFieldDecls cenv envinner innerParent isIncrClass tpenv fields fixupReprAttrs <- fixupUserFieldAttrs + latestFixupReprAttrs.Value <- fixupUserFieldAttrs let implicitStructFields = [ // For structs with an implicit ctor, determine the fields immediately based on the arguments match implicitCtorSynPats with @@ -3849,7 +3856,7 @@ module EstablishTypeDefinitionCores = (baseValOpt, safeInitInfo), fixupReprAttrs with RecoverableException exn -> errorRecovery exn m - (None, NoSafeInitInfo), (fun () -> ()) + (None, NoSafeInitInfo), latestFixupReprAttrs.Value /// Check that a set of type definitions is free of cycles in abbreviations let private TcTyconDefnCore_CheckForCyclicAbbreviations tycons = diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index 32aefbcda1..65ebbad478 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -4515,9 +4515,23 @@ and TcTyparDecl (cenv: cenv) (env: TcEnv) synTyparDecl = // are always in scope and resolve here, so kind inference and conditional-dependency // flags remain correct in Phase1A. A fixup thunk re-resolves attributes against a richer // env, finalizing the typar attribs with proper diagnostics. - let prelimAttrs = - suppressErrorReporting (fun () -> - TcAttributesMaybeFail TcCanFail.IgnoreAllErrors cenv env AttributeTargets.GenericParameter synAttrs |> fst) + // Use a capturing logger so we can both suppress prelim diagnostics AND detect whether + // any were emitted. If anything was reported, we must re-run with the real logger to + // surface the diagnostics; if nothing was reported, we can skip the work entirely. + let prelimCapture = CapturingDiagnosticsLogger("TcTyparDecl prelim") + let prelimAttrs, didFailReported = + let oldLogger = DiagnosticsThreadStatics.DiagnosticsLogger + try + SetThreadDiagnosticsLoggerNoUnwind prelimCapture + TcAttributesMaybeFail TcCanFail.IgnoreAllErrors cenv env AttributeTargets.GenericParameter synAttrs + finally + SetThreadDiagnosticsLoggerNoUnwind oldLogger + // didFail is true if name resolution reported failure, any synAttr was dropped due to + // a caught RecoverableException (length mismatch), or any diagnostic was suppressed. + let didFail = + didFailReported + || List.length prelimAttrs < List.length synAttrs + || not prelimCapture.Diagnostics.IsEmpty let hasMeasureAttr = HasFSharpAttribute g g.attrib_MeasureAttribute prelimAttrs let hasEqDepAttr = HasFSharpAttribute g g.attrib_EqualityConditionalOnAttribute prelimAttrs let hasCompDepAttr = HasFSharpAttribute g g.attrib_ComparisonConditionalOnAttribute prelimAttrs @@ -4535,11 +4549,12 @@ and TcTyparDecl (cenv: cenv) (env: TcEnv) synTyparDecl = CallNameResolutionSink cenv.tcSink (id.idRange, env.NameEnv, item, emptyTyparInst, ItemOccurrence.UseInType, env.eAccessRights) let fixupAttrs (envForFinal: TcEnv) = - // Re-resolve against the (possibly richer) env and finalize. Reports any genuine errors. - let finalAttrs = - TcAttributes cenv envForFinal AttributeTargets.GenericParameter synAttrs - |> filterOutWellKnownAttribs g WellKnownEntityAttributes.MeasureAttribute WellKnownValAttributes.None - tp.SetAttribs finalAttrs + // Only re-resolve if the prelim pass actually suppressed errors; otherwise keep prelim attrs. + if didFail then + let finalAttrs = + TcAttributes cenv envForFinal AttributeTargets.GenericParameter synAttrs + |> filterOutWellKnownAttribs g WellKnownEntityAttributes.MeasureAttribute WellKnownValAttributes.None + tp.SetAttribs finalAttrs tp, fixupAttrs From 83fba4e748dc00da2ecf7a00f00a8d53fb0dfbff Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 15 May 2026 14:25:34 +0200 Subject: [PATCH 8/9] Trim verbose comments to short technical notes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/CheckDeclarations.fs | 23 +++++-------------- .../Checking/Expressions/CheckExpressions.fs | 18 ++++----------- .../Checking/Expressions/CheckExpressions.fsi | 16 ++++--------- .../AttributeResolutionInRecursiveScopes.fs | 20 +++++----------- 4 files changed, 22 insertions(+), 55 deletions(-) diff --git a/src/Compiler/Checking/CheckDeclarations.fs b/src/Compiler/Checking/CheckDeclarations.fs index a550481362..71c04876d8 100644 --- a/src/Compiler/Checking/CheckDeclarations.fs +++ b/src/Compiler/Checking/CheckDeclarations.fs @@ -428,10 +428,7 @@ module TcRecdUnionAndEnumDeclarations = let TcFieldDecl (cenv: cenv) env parent isIncrClass tpenv (isStatic, synAttrs, id: Ident, nameGenerated, ty, isMutable, xmldoc, vis) = let g = cenv.g let m = id.idRange - // Use the CanFail variant so attribute-constructor lookups that fail at this point - // (e.g. attribute types defined later in the same `module rec` group whose - // constructors aren't yet wired) can be retried after Phase1G via the - // returned thunk. See fixup wired into Phase1G's representation fixup. + // CanFail: attrs from same rec group may not resolve yet; fixup re-resolves in Phase1G. let attrs, getFinalAttrs = TcAttributesWithPossibleTargetsCanFail cenv env AttributeTargets.FieldDecl synAttrs let splitAttrs (attrsWithTargets: (AttributeTargets * Attrib) list) = @@ -594,10 +591,7 @@ module TcRecdUnionAndEnumDeclarations = let checkXmlDocs = cenv.diagnosticOptions.CheckXmlDocs let xmlDoc = xmldoc.ToXmlDoc(checkXmlDocs, Some names) - // Use TcAttributesCanFail so attribute-constructor lookups that fail at this point - // (e.g. attribute types defined later in the same `module rec` group whose - // constructors aren't yet wired) can be retried after Phase1G via the - // returned thunk. See fixup wired into Phase1G's `fixupFinalAttrs`. + // CanFail: attrs from same rec group may not resolve yet; fixup re-resolves in Phase1G. let attrs, getFinalAttrs = TcAttributesCanFail cenv env AttributeTargets.UnionCaseDecl synAttrs (* The attributes of a union case decl get attached to the generated "static factory" method. @@ -2459,7 +2453,7 @@ module TcExceptionDeclarations = | _ -> () let rfield, fixupFieldAttrs = TcRecdUnionAndEnumDeclarations.TcAnonFieldDecl cenv env parent emptyUnscopedTyparEnv (mkExceptionFieldName i) fdef - // Exception field attributes are resolved eagerly (no rec-group fixup). + // Exceptions aren't in rec groups — finalize field attrs eagerly. fixupFieldAttrs () rfield) TcRecdUnionAndEnumDeclarations.ValidateFieldNames(args, args') @@ -2803,7 +2797,7 @@ module EstablishTypeDefinitionCores = match synTyconRepr with | SynTypeDefnSimpleRepr.Exception synExnDefnRepr -> - // Exceptions don't carry user-declared typars; finalize eagerly. + // Exceptions have no user typars — finalize eagerly. fixupTyparAttrs env TcExceptionDeclarations.TcExnDefnCore_Phase1A g cenv env parent synExnDefnRepr, (fun _ -> ()) | _ -> @@ -3424,9 +3418,7 @@ module EstablishTypeDefinitionCores = let private TcTyconDefnCore_Phase1G_EstablishRepresentation (cenv: cenv) envinner tpenv inSig (MutRecDefnsPhase1DataForTycon(_, synTyconRepr, _, _, _, _)) (tycon: Tycon) (attrs: Attribs) = let g = cenv.g let m = tycon.Range - // Hold the latest deferred attribute fixup outside the try, so that if a later RecoverableException - // is raised after fixupReprAttrs has been captured (Union/Record/General arms), the recovery path still - // returns the captured fixup. Otherwise the rec-resolved field/case attribute diagnostics are silently dropped. + // Survives RecoverableException so captured fixup isn't lost on recovery path. let latestFixupReprAttrs = ref (fun () -> ()) try let id = tycon.Id @@ -4085,10 +4077,7 @@ module EstablishTypeDefinitionCores = let TcMutRecDefns_Phase1 mkLetInfo (cenv: cenv) envInitial parent typeNames inSig tpenv m scopem mutRecNSInfo (mutRecDefns: MutRecShapes) = - // Per-tycon typar attribute fixups produced in Phase1A. These can't run until later - // because user-defined attributes referenced by type-parameter attributes may live in - // the same rec scope and aren't yet wired. The fixups are composed into Phase1G's - // per-tycon fixupFinalAttrs below. + // Typar attr fixups from Phase1A — deferred because rec-scope attrs aren't wired yet. let typarAttrFixups = System.Collections.Generic.Dictionary unit>() // Phase1A - build Entity for type definitions, exception definitions and module definitions. // Also for abbreviations of any of these. Augmentations are skipped in this phase. diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index 65ebbad478..555c604953 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -4508,16 +4508,9 @@ and TcTyparDecl (cenv: cenv) (env: TcEnv) synTyparDecl = let (SynTyparDecl (attributes = Attributes synAttrs; typar = synTypar)) = synTyparDecl let (SynTypar (ident = id)) = synTypar - // Resolve attributes in a preliminary pass with error reporting suppressed so user-defined - // attributes referenced from the same `module rec` group (whose entities aren't yet wired) - // don't prematurely emit FS0039. Framework attributes (MeasureAttribute, - // EqualityConditionalOnAttribute, ComparisonConditionalOnAttribute, CompiledNameAttribute) - // are always in scope and resolve here, so kind inference and conditional-dependency - // flags remain correct in Phase1A. A fixup thunk re-resolves attributes against a richer - // env, finalizing the typar attribs with proper diagnostics. - // Use a capturing logger so we can both suppress prelim diagnostics AND detect whether - // any were emitted. If anything was reported, we must re-run with the real logger to - // surface the diagnostics; if nothing was reported, we can skip the work entirely. + // Prelim pass: suppress diagnostics so rec-scope attrs (not yet wired) don't emit FS0039. + // Framework attrs (Measure, EqualityConditionalOn, etc.) resolve here for kind inference. + // Fixup thunk re-resolves with the final env. let prelimCapture = CapturingDiagnosticsLogger("TcTyparDecl prelim") let prelimAttrs, didFailReported = let oldLogger = DiagnosticsThreadStatics.DiagnosticsLogger @@ -4526,8 +4519,7 @@ and TcTyparDecl (cenv: cenv) (env: TcEnv) synTyparDecl = TcAttributesMaybeFail TcCanFail.IgnoreAllErrors cenv env AttributeTargets.GenericParameter synAttrs finally SetThreadDiagnosticsLoggerNoUnwind oldLogger - // didFail is true if name resolution reported failure, any synAttr was dropped due to - // a caught RecoverableException (length mismatch), or any diagnostic was suppressed. + // Failed if: TcCanFail reported failure, attrs were dropped, or diagnostics were suppressed. let didFail = didFailReported || List.length prelimAttrs < List.length synAttrs @@ -4549,7 +4541,7 @@ and TcTyparDecl (cenv: cenv) (env: TcEnv) synTyparDecl = CallNameResolutionSink cenv.tcSink (id.idRange, env.NameEnv, item, emptyTyparInst, ItemOccurrence.UseInType, env.eAccessRights) let fixupAttrs (envForFinal: TcEnv) = - // Only re-resolve if the prelim pass actually suppressed errors; otherwise keep prelim attrs. + // Re-resolve only if prelim pass suppressed errors. if didFail then let finalAttrs = TcAttributes cenv envForFinal AttributeTargets.GenericParameter synAttrs diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fsi b/src/Compiler/Checking/Expressions/CheckExpressions.fsi index ee45c82b65..63887c6239 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fsi +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fsi @@ -626,10 +626,8 @@ val TcAttributesWithPossibleTargets: synAttribs: SynAttribute list -> (AttributeTargets * Attrib) list * bool -/// Check a set of attributes which can only target specific elements, allowing failure -/// because a later phase of type realization may successfully check the attributes (if -/// the attribute type or its arguments are in the same recursive group). Returns the -/// preliminary attribute/target pairs plus a thunk to re-resolve and report errors. +/// Like TcAttributesWithPossibleTargets, but allows failure for rec-scope attrs. +/// Returns prelim attrs + a fixup thunk that re-resolves with the final env. val TcAttributesWithPossibleTargetsCanFail: cenv: TcFileState -> env: TcEnv -> @@ -819,13 +817,9 @@ val TcTyparConstraints: synConstraints: SynTypeConstraint list -> UnscopedTyparEnv -/// Check a collection of type parameters declarations. -/// -/// Returns the typars together with a fixup thunk that finalizes their attributes against a -/// (possibly richer) env. User-defined attributes that fail to resolve eagerly (e.g. -/// attributes from the same `module rec` group whose constructors aren't yet wired) are -/// re-tried by the thunk using the env passed in. Callers in non-rec contexts should invoke -/// the thunk immediately with the same env they used for the eager pass. +/// Check type parameter declarations. +/// Returns typars + fixup thunk for deferred attr resolution in rec scopes. +/// Non-rec callers: invoke the fixup immediately with the same env. val TcTyparDecls: cenv: TcFileState -> env: TcEnv -> synTypars: SynTyparDecl list -> Typar list * (TcEnv -> unit) /// Check a syntactic type diff --git a/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs b/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs index 9a27819ca4..86060c1bcc 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/AttributeResolutionInRecursiveScopes.fs @@ -8,8 +8,7 @@ open FSharp.Test.Compiler module AttributeResolutionInRecursiveScopes = - // Regression baseline — these attribute positions already worked before issue #5795 was fixed. - // They must continue to compile after the fix. Do NOT remove these tests. + // Baselines: these attribute positions already worked before #5795. [] let ``attribute on type declaration in module rec resolves to attribute defined in same module`` () = @@ -52,7 +51,6 @@ type A = | A [] let ``attribute on let binding in non-rec module resolves to attribute defined in same module`` () = - // Non-rec baseline — should always work. Fsx """ module M @@ -181,7 +179,6 @@ type B<[]'a> = | B of 'a [] let ``attribute on type parameter combined with framework Measure attribute in module rec compiles`` () = - // Sanity: framework MeasureAttribute still works alongside a deferred user attribute. Fsx """ module rec M @@ -192,7 +189,7 @@ type B<[]'u, []'a> = B of 'a |> compile |> shouldSucceed - // === Edge cases === + // Edge cases [] let ``attribute defined in nested module of rec scope resolves on union case`` () = @@ -262,11 +259,8 @@ module rec M = |> compile |> shouldSucceed - // F# attribute kind inference is name-resolution based: when a user-defined - // MeasureAttribute is in scope, [] resolves to the user's attribute and - // the typar kind is NOT inferred as Measure. This is standard F# behaviour and - // independent of the rec-scope deferred attribute resolution fix. Tracking - // follow-up: commentary. + // [] resolves to the user's MeasureAttribute by name, so kind inference breaks. + // Unrelated to #5795 rec-scope fix. [] let ``user-defined MeasureAttribute in rec scope does not break framework Measure kind inference`` () = Fsx """ @@ -279,13 +273,11 @@ type MeasureAttribute() = inherit System.Attribute() |> compile |> shouldSucceed - // === Negative tests — these MUST still error after the fix === + // Negative tests — must still error after the fix. [] let ``non-attribute type used on union case in module rec still produces diagnostic`` () = - // The F# compiler emits warning FS3242 ("This type does not inherit Attribute, ...") - // rather than an error for a user-defined non-Attribute-derived class used as an attribute. - // The negative test confirms a diagnostic is still produced after the rec-scope fix. + // FS3242: "does not inherit Attribute" — warning, not error. Fsx """ module rec M From 1adab97885bc6f7c9ee464d2934c5e35a2073e5a Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 15 May 2026 14:26:58 +0200 Subject: [PATCH 9/9] Add PR link to release notes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 10cada4c6b..bb23628937 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -51,7 +51,7 @@ * Fix internal error when using custom attribute with `[]` value type parameter and no `[]`. ([Issue #8353](https://github.com/dotnet/fsharp/issues/8353), [PR #19484](https://github.com/dotnet/fsharp/pull/19484)) * Fix parallel compilation of scripts ([PR #19649](https://github.com/dotnet/fsharp/pull/19649)) * Parser: fix unexpected diagnostics in debug builds, improve error messages ([PR #19730](https://github.com/dotnet/fsharp/pull/19730)) -* Fix #5795: Allow attributes defined in a `module rec` / `namespace rec` scope to be used on union cases, record fields, and generic type parameters of types in the same recursive scope. ([Issue #5795](https://github.com/dotnet/fsharp/issues/5795)) +* Fix #5795: Allow attributes defined in a `module rec` / `namespace rec` scope to be used on union cases, record fields, and generic type parameters of types in the same recursive scope. ([Issue #5795](https://github.com/dotnet/fsharp/issues/5795), [PR #19744](https://github.com/dotnet/fsharp/pull/19744)) ### Added