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..055577a1859 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 attributes not resolved from opened namespaces in `namespace rec` / `module rec` scopes. ([Issue #7931](https://github.com/dotnet/fsharp/issues/7931), [PR #19502](https://github.com/dotnet/fsharp/pull/19502)) * Fix DU case names matching IWSAM member names no longer cause duplicate property entries. (Issue [#14321](https://github.com/dotnet/fsharp/issues/14321), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) * Fix DefaultAugmentation(false) duplicate entry in method table. (Issue [#16565](https://github.com/dotnet/fsharp/issues/16565), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) * Fix abstract event accessors now have SpecialName flag. (Issue [#5834](https://github.com/dotnet/fsharp/issues/5834), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) diff --git a/src/Compiler/Checking/CheckDeclarations.fs b/src/Compiler/Checking/CheckDeclarations.fs index f425f49dafc..19c2b9c93ef 100644 --- a/src/Compiler/Checking/CheckDeclarations.fs +++ b/src/Compiler/Checking/CheckDeclarations.fs @@ -2735,6 +2735,23 @@ module EstablishTypeDefinitionCores = | _ -> () ] |> set + /// Pre-process open declarations from a list of mutually recursive shapes so that + /// opened namespaces are available during Phase1A attribute checking. + /// In recursive scopes, opens are normally processed in Phase1AB (after Phase1A builds + /// module/type entities), but attributes on modules need the opened namespaces. + /// Errors are suppressed because some opens may refer to modules being defined in the + /// current recursive scope, which don't exist yet during Phase1A. Those opens will be + /// properly processed (with full error reporting) during Phase1AB. + let private preProcessOpensForPhase1A (cenv: cenv) (env: TcEnv) (shapes: MutRecShapes<_, _, _>) = + suppressErrorReporting (fun () -> + use _holder = TemporarilySuspendReportingTypecheckResultsToSink cenv.tcSink + (env, shapes) ||> List.fold (fun env shape -> + match shape with + | MutRecShape.Open(MutRecDataForOpen(target, openm, moduleRange, _)) -> + let env, _ = TcOpenDecl cenv openm moduleRange env target + env + | _ -> env)) + let TcTyconDefnCore_Phase1A_BuildInitialModule (cenv: cenv) envInitial parent typeNames compInfo decls = let g = cenv.g let (SynComponentInfo(Attributes attribs, _, _, longPath, xml, _, vis, im)) = compInfo @@ -2750,7 +2767,11 @@ module EstablishTypeDefinitionCores = CheckForDuplicateConcreteType envInitial id.idText im CheckNamespaceModuleOrTypeName g id - let envForDecls, moduleTyAcc = MakeInnerEnv true envInitial id moduleKind + let envForDecls, moduleTyAcc = MakeInnerEnv true envInitial id moduleKind + + // Pre-process opens from children so nested modules can see opened namespaces during attribute checking + let envForDecls = preProcessOpensForPhase1A cenv envForDecls decls + let moduleTy = Construct.NewEmptyModuleOrNamespaceType moduleKind let checkXmlDocs = cenv.diagnosticOptions.CheckXmlDocs @@ -4039,12 +4060,18 @@ module EstablishTypeDefinitionCores = let TcMutRecDefns_Phase1 mkLetInfo (cenv: cenv) envInitial parent typeNames inSig tpenv m scopem mutRecNSInfo (mutRecDefns: MutRecShapes) = + // Pre-process top-level opens so they are available during attribute checking in Phase1A. + // In recursive scopes (namespace rec / module rec), opens are normally processed in Phase1AB + // after module entities are built, but module attributes need access to opened namespaces. + // See https://github.com/dotnet/fsharp/issues/7931 + let envWithOpens = preProcessOpensForPhase1A cenv envInitial mutRecDefns + // 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 = mutRecDefns |> MutRecShapes.mapWithParent - (parent, typeNames, envInitial) + (parent, typeNames, envWithOpens) // Build the initial entity for each module definition (fun (innerParent, typeNames, envForDecls) compInfo decls -> TcTyconDefnCore_Phase1A_BuildInitialModule cenv envForDecls innerParent typeNames compInfo decls) diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/AttributeUsage/AttributeResolutionInRecursiveScopes.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/AttributeUsage/AttributeResolutionInRecursiveScopes.fs new file mode 100644 index 00000000000..56737c3ee74 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/AttributeUsage/AttributeResolutionInRecursiveScopes.fs @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Conformance.BasicGrammarElements + +open Xunit +open FSharp.Test.Compiler + +module AttributeResolutionInRecursiveScopes = + + // https://github.com/dotnet/fsharp/issues/7931 + [] + let ``Extension attribute on module in namespace rec`` () = + FSharp """ +namespace rec Ns + +open System.Runtime.CompilerServices + +[] +module Module = + [] + let ext1 (x: int) = x.ToString() + """ + |> asLibrary + |> typecheck + |> shouldSucceed + + // https://github.com/dotnet/fsharp/issues/7931 + [] + let ``Extension attribute on type in namespace rec`` () = + FSharp """ +namespace rec Ns + +open System.Runtime.CompilerServices + +[] +type T() = + class end + """ + |> asLibrary + |> typecheck + |> shouldSucceed + + // https://github.com/dotnet/fsharp/issues/5795 - Custom attribute used on type and let in rec module + [] + let ``Custom attribute used on type and let in rec module`` () = + FSharp """ +module rec M + +type CustomAttribute() = + inherit System.Attribute() + +[] type A = | A +[] let a = () + """ + |> typecheck + |> shouldSucceed + + // Nested module case: open inside outer module, attribute on inner module + [] + let ``Open inside nested module resolves for attribute on inner module in namespace rec`` () = + FSharp """ +namespace rec Ns + +module Outer = + open System.Runtime.CompilerServices + + [] + module Inner = + [] + let ext1 (x: int) = x.ToString() + """ + |> asLibrary + |> typecheck + |> shouldSucceed + + // Non-recursive baseline: should always work + [] + let ``Extension attribute works without rec - baseline`` () = + FSharp """ +namespace Ns + +open System.Runtime.CompilerServices + +[] +module Module = + [] + let ext1 (x: int) = x.ToString() + """ + |> asLibrary + |> typecheck + |> shouldSucceed + + // Multiple opens in namespace rec + [] + let ``Multiple opens resolve for attributes in namespace rec`` () = + FSharp """ +namespace rec Ns + +open System +open System.Runtime.CompilerServices + +[] +module Module = + [] + [] + let ext1 (x: int) = x.ToString() + """ + |> asLibrary + |> typecheck + |> shouldSucceed + + // Open in module rec resolves for module attributes + [] + let ``Open in module rec resolves for nested module attribute`` () = + FSharp """ +module rec M + +open System.Runtime.CompilerServices + +[] +module Inner = + [] + let ext1 (x: int) = x.ToString() + """ + |> typecheck + |> shouldSucceed + + // Open with Obsolete attribute in namespace rec + [] + let ``Obsolete attribute resolves via open in namespace rec`` () = + FSharp """ +namespace rec Ns + +open System + +[] +module DeprecatedModule = + let x = 42 + """ + |> asLibrary + |> typecheck + |> shouldSucceed diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index f5d1048408e..40ee7fbb86e 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -39,6 +39,7 @@ +