From eda2f5c8ce873c20c0d64fafb458c4d55e0381d1 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Tue, 14 Jul 2026 18:57:54 -0400 Subject: [PATCH 1/3] Fix recursive wildcard import resolution (import X::*::**) Recursive wildcard imports parsed without error but never actually resolved names in namespaces nested more than one level under the imported target, because: - AstBuilder.VisitImportRule discarded the parsed IsRecursive flag - SysmlImportNode had no field to carry recursion state - ReferenceResolver.TryResolve's wildcard-import step only ever checked one namespace level deep This is a legitimate, spec-sanctioned construct present in the OMG conformance corpus (SimpleTests/ImportTest.sysml), but the corpus sweep only checks for parse errors, never resolution correctness, so the gap went uncaught. Fixes: - SysmlImportNode: added IsRecursive property - AstBuilder.VisitImportRule: now passes through the parsed recursion flag instead of discarding it - ReferenceResolver: added FindRecursiveWildcardMatch, which searches the symbol table for a same-named member in any namespace nested at any depth under the imported target when IsRecursive is true, preferring the shallowest match on ambiguity Added 3 regression tests to WorkspaceLoaderTests covering resolution via recursive wildcard import, resolution via nested-level names, and the non-recursive control case (must NOT reach nested members). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Semantic/Model/AstBuilder.cs | 3 +- .../Semantic/Model/ReferenceResolver.cs | 43 +++++++ .../Semantic/Model/SysmlNode.cs | 9 ++ .../Semantic/WorkspaceLoaderTests.cs | 107 ++++++++++++++++++ 4 files changed, 161 insertions(+), 1 deletion(-) diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs index 86d5fa69..ad306e40 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs @@ -2081,7 +2081,7 @@ private static IReadOnlyList ExtractExposedNames( return null; } - var (qn, isWildcard, bracketFilterText, _) = ExtractImportTarget(decl.namespaceImport(), decl.membershipImport()); + var (qn, isWildcard, bracketFilterText, isRecursive) = ExtractImportTarget(decl.namespaceImport(), decl.membershipImport()); if (qn is null) { return null; @@ -2092,6 +2092,7 @@ private static IReadOnlyList ExtractExposedNames( ImportedNamespace = qn, ImportedNames = [qn], IsWildcard = isWildcard, + IsRecursive = isRecursive, BracketFilterExpressionText = bracketFilterText, }; } diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs index bce7a878..133e0f86 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs @@ -440,6 +440,19 @@ private bool TryResolve( resolvedName = candidate; return true; } + + // Recursive wildcard import (`import X::*::**;`) additionally reaches members of any + // namespace nested within X at any depth, not just X's own direct members — e.g. + // `import VersionMark::*::**;` should let an unqualified `CliSubsystem` resolve to + // `VersionMark::Cli::CliSubsystem` even though `Cli` is a nested package, not X itself. + // Prefer the least-nested match for determinism when more than one namespace at + // different depths happens to declare a same-named member. + if (wildcard.IsRecursive && + FindRecursiveWildcardMatch(resolvedNamespace, name) is { Length: > 0 } recursiveMatch) + { + resolvedName = recursiveMatch; + return true; + } } // Step 4: Explicit named imports — for each `import X::Y` where Y == name, @@ -468,6 +481,36 @@ private bool TryResolve( return false; } + /// + /// Searches the symbol table for a member named declared in any + /// namespace nested (at any depth) within , for + /// resolving a recursive wildcard import's (import X::*::**;) unqualified + /// references. Returns the shallowest (least-nested) match when more than one namespace at + /// different depths declares a same-named member, and when no + /// match exists. + /// + private string FindRecursiveWildcardMatch(string resolvedNamespace, string name) + { + var prefix = resolvedNamespace + "::"; + var suffix = "::" + name; + string? best = null; + foreach (var qualifiedName in _symbolTable.Symbols.Keys) + { + if (!qualifiedName.StartsWith(prefix, StringComparison.Ordinal) || + !qualifiedName.EndsWith(suffix, StringComparison.Ordinal)) + { + continue; + } + + if (best is null || qualifiedName.Length < best.Length) + { + best = qualifiedName; + } + } + + return best ?? string.Empty; + } + /// /// Resolves a raw (possibly nested-relative) namespace name referenced by an import /// statement to its fully-qualified form, trying a direct symbol-table lookup first and diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs index 2c574581..54e08b36 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs @@ -195,6 +195,15 @@ public sealed class SysmlImportNode : SysmlNode /// public bool IsWildcard { get; init; } + /// + /// Gets a value indicating whether this import is recursive — derived from a trailing + /// ::** on a namespace-import wildcard (import X::*::**;), which per the + /// KerML/SysML v2 grammar imports members of X and every namespace nested within it + /// at any depth, not just X's direct members. for a plain + /// import X::*; (direct members only) or any non-wildcard import. + /// + public bool IsRecursive { get; init; } + /// /// Gets the raw source text of this import's bracketed filter expression (from /// expose <path>::**[<expr>]'s filterPackageMember().ownedExpression().GetText()), diff --git a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs index cd87186f..3cbde0a1 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs @@ -397,6 +397,113 @@ part def Foo specializes Bar {} } } + // Level 12b: Unqualified name resolves via recursive wildcard import + /// + /// A recursive wildcard import (import X::*::**;) must reach members declared in + /// namespaces nested at any depth under X, not just X's own direct members. + /// This mirrors the official OMG conformance construct in + /// test/SysMLModels/OMG/examples/SimpleTests/ImportTest.sysml (private import + /// Pkg211::*::**;), which previously parsed without error but never actually resolved + /// nested-namespace references — the OMG corpus sweep only checks for parse errors, never + /// resolution correctness, so this gap went uncaught. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_UnqualifiedNameViaRecursiveWildcardImport_ResolvesWithoutWarning() + { + // Arrange — Foo and Bar each live two namespace levels below package A, in the + // nested Sub1 and Sub2 sub-packages respectively. Consumer uses a recursive + // wildcard import targeting A and references both by short name. + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); + try + { + await File.WriteAllTextAsync(tempFile, """ + package TestNs { + package A { + package Sub1 { + part def Foo; + } + package Sub2 { + part def Bar; + } + } + } + package Consumer { + import TestNs::A::*::**; + + part def UsesBoth { + part f : Foo; + part b : Bar; + } + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert — "Foo" and "Bar" both resolve via the recursive wildcard import, even + // though they're nested two levels below the imported namespace. + Assert.NotNull(result.Workspace); + Assert.DoesNotContain(result.Diagnostics, + d => d.Severity == DemaConsulting.SysML2Tools.Parser.DiagnosticSeverity.Warning && + (d.Message.Contains("Foo") || d.Message.Contains("Bar"))); + } + finally + { + File.Delete(tempFile); + } + } + + // Level 12c: Non-recursive wildcard import must NOT reach nested-namespace members + /// + /// A plain (non-recursive) wildcard import (import X::*;) must only bring X's own + /// direct members into scope — it must NOT reach members declared in namespaces nested + /// under X. This is the control case proving the recursive-descent fallback added for + /// import X::*::**; is correctly gated on IsRecursive and doesn't leak into + /// the plain wildcard-import path. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_UnqualifiedNameViaPlainWildcardImport_DoesNotReachNestedNamespace() + { + // Arrange — Foo lives in the nested Sub1 sub-package, but Consumer only uses a + // plain (non-recursive) wildcard import targeting A. + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); + try + { + await File.WriteAllTextAsync(tempFile, """ + package TestNs { + package A { + package Sub1 { + part def Foo; + } + } + } + package Consumer { + import TestNs::A::*; + + part def Uses { + part f : Foo; + } + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert — "Foo" must NOT resolve; a non-recursive wildcard import doesn't reach + // into nested namespaces, so this must still produce an Unresolved reference warning. + Assert.NotNull(result.Workspace); + Assert.Contains(result.Diagnostics, + d => d.Severity == DemaConsulting.SysML2Tools.Parser.DiagnosticSeverity.Warning && + d.Message.Contains("Foo")); + } + finally + { + File.Delete(tempFile); + } + } + // Level 13: Explicit named import resolves short name /// /// A part def that specializes a type using only its short name, where that type is From a19d8636ab806a013d52cb1e9408c746842a1830 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Tue, 14 Jul 2026 19:29:54 -0400 Subject: [PATCH 2/3] Fix recursive membership import resolution (import X::Y::**) A recursive membership import (import X::Y::**;) is a distinct grammar shape from the recursive namespace-import form (import X::*::**;) fixed in the previous commit: here Y is the explicit membership-import target itself, not a containing namespace being wildcard-searched. It must bring Y into scope by its own short name, in addition to reaching Y's own nested descendants. Auditing the codebase for other places sharing this bug class found that expose's equivalent path (ExposeScopeResolver) already handles both recursive forms correctly with existing tests (PR #36); this gap was isolated to plain import statements' ReferenceResolver. Fixes: - SysmlImportNode: added IsMembershipImport property, set from AstBuilder.VisitImportRule based on which grammar alternative (namespaceImport vs membershipImport) was actually parsed - ReferenceResolver: added a check so a recursive membership import also resolves an unqualified reference to the import target itself (not just its descendants), guarded so it does not fire for the namespace-import recursive form Added a regression test to WorkspaceLoaderTests covering resolution of both the membership-import target's own name and its nested descendants via a single recursive membership import. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Semantic/Model/AstBuilder.cs | 1 + .../Semantic/Model/ReferenceResolver.cs | 24 ++++++++ .../Semantic/Model/SysmlNode.cs | 13 +++++ .../Semantic/WorkspaceLoaderTests.cs | 55 +++++++++++++++++++ 4 files changed, 93 insertions(+) diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs index ad306e40..35d1f14a 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs @@ -2093,6 +2093,7 @@ private static IReadOnlyList ExtractExposedNames( ImportedNames = [qn], IsWildcard = isWildcard, IsRecursive = isRecursive, + IsMembershipImport = decl.membershipImport() is not null, BracketFilterExpressionText = bracketFilterText, }; } diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs index 133e0f86..eb5cec6a 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs @@ -441,6 +441,19 @@ private bool TryResolve( return true; } + // A recursive *membership* import (`import X::Y::**;`) brings Y itself into scope by + // its own short name — not just Y's nested descendants — since Y is the explicit + // membership-import target, not merely a containing namespace being wildcard-searched. + // e.g. `import VersionMark::Cli::**;` should let an unqualified `Cli` reference resolve + // to `VersionMark::Cli` itself, in addition to reaching Cli's own nested members below. + if (wildcard.IsMembershipImport && + LastSegment(resolvedNamespace) == name && + _symbolTable.Contains(resolvedNamespace)) + { + resolvedName = resolvedNamespace; + return true; + } + // Recursive wildcard import (`import X::*::**;`) additionally reaches members of any // namespace nested within X at any depth, not just X's own direct members — e.g. // `import VersionMark::*::**;` should let an unqualified `CliSubsystem` resolve to @@ -511,6 +524,17 @@ private string FindRecursiveWildcardMatch(string resolvedNamespace, string name) return best ?? string.Empty; } + /// + /// Returns the final ::-separated segment of a qualified name (e.g. + /// "VersionMark::Cli""Cli"), or the whole string when it contains no + /// :: separator. + /// + private static string LastSegment(string qualifiedName) + { + var lastSep = qualifiedName.LastIndexOf("::", StringComparison.Ordinal); + return lastSep >= 0 ? qualifiedName[(lastSep + 2)..] : qualifiedName; + } + /// /// Resolves a raw (possibly nested-relative) namespace name referenced by an import /// statement to its fully-qualified form, trying a direct symbol-table lookup first and diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs index 54e08b36..fa4cb857 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs @@ -204,6 +204,19 @@ public sealed class SysmlImportNode : SysmlNode /// public bool IsRecursive { get; init; } + /// + /// Gets a value indicating whether this import was parsed from the membership-import + /// grammar form (import X::Y; or its recursive import X::Y::**; variant, where + /// ImportedNamespace is the fully-qualified path to the specific member Y) + /// rather than the namespace-import wildcard form (import X::*; or + /// import X::*::**;, where ImportedNamespace is the containing namespace whose + /// members are being imported). This distinction matters for recursive resolution: a + /// recursive membership import must additionally bring Y itself into scope by its + /// own short name (in addition to Y's nested descendants), whereas a recursive + /// namespace import must not — X itself was never a name being imported. + /// + public bool IsMembershipImport { get; init; } + /// /// Gets the raw source text of this import's bracketed filter expression (from /// expose <path>::**[<expr>]'s filterPackageMember().ownedExpression().GetText()), diff --git a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs index 3cbde0a1..fcde0476 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs @@ -454,6 +454,61 @@ part def UsesBoth { } } + // Level 12d: Unqualified name resolves via recursive membership import, including the + // imported member's own name + /// + /// A recursive membership import (import X::Y::**;, as distinct from the + /// recursive namespace-import form covered by the previous test) must bring both the + /// explicitly named member Y itself into scope by its own short name, and every + /// name declared in a namespace nested under Y at any depth. This is a distinct + /// grammar shape from import X::*::**;: there, X is a containing namespace + /// being wildcard-searched and is never itself a name being imported, whereas here Y + /// is the explicit membership-import target and must resolve by its own name too. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_UnqualifiedNameViaRecursiveMembershipImport_ResolvesWithoutWarning() + { + // Arrange — A is a nested package directly under TestNs; Foo lives one level further + // down, inside A's own Sub1 sub-package. Consumer uses a recursive membership import + // targeting A itself and references both A and Foo by short name. + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); + try + { + await File.WriteAllTextAsync(tempFile, """ + package TestNs { + package A { + package Sub1 { + part def Foo; + } + } + } + package Consumer { + import TestNs::A::**; + + part def UsesA { + part a : A; + part f : Foo; + } + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert — both "A" (the membership-import target itself) and "Foo" (A's nested + // descendant) resolve without warning. + Assert.NotNull(result.Workspace); + Assert.DoesNotContain(result.Diagnostics, + d => d.Severity == DemaConsulting.SysML2Tools.Parser.DiagnosticSeverity.Warning && + (d.Message.Contains("'A'") || d.Message.Contains("Foo"))); + } + finally + { + File.Delete(tempFile); + } + } + // Level 12c: Non-recursive wildcard import must NOT reach nested-namespace members /// /// A plain (non-recursive) wildcard import (import X::*;) must only bring X's own From f78609f435f42f0f8d9220401085c0547f3ba624 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Tue, 14 Jul 2026 19:45:48 -0400 Subject: [PATCH 3/3] Fix depth tie-break in recursive wildcard import resolution Code review of the prior two commits found FindRecursiveWildcardMatch picked the 'least-nested' match among ambiguous recursive-import candidates by comparing raw qualified-name string length, not actual namespace depth. Since segment names vary in length, a deeper match can have a shorter qualified-name string than a shallower one, silently violating the documented 'prefer the shallowest match' invariant. Fixed to count actual '::'-separated segment depth between the resolved namespace and the matched name, with a length guard to avoid an invalid slice range on the exact one-level match (where the prefix and suffix overlap). Added a regression test with two same-named members at different depths, using deliberately long package names at the shallow depth and short ones at the deep depth, proving the shallower match wins even though its qualified-name string is longer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Semantic/Model/ReferenceResolver.cs | 17 ++++- .../Semantic/WorkspaceLoaderTests.cs | 63 ++++++++++++++++++- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs index eb5cec6a..dbe94999 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs @@ -507,17 +507,30 @@ private string FindRecursiveWildcardMatch(string resolvedNamespace, string name) var prefix = resolvedNamespace + "::"; var suffix = "::" + name; string? best = null; + var bestDepth = int.MaxValue; foreach (var qualifiedName in _symbolTable.Symbols.Keys) { if (!qualifiedName.StartsWith(prefix, StringComparison.Ordinal) || - !qualifiedName.EndsWith(suffix, StringComparison.Ordinal)) + !qualifiedName.EndsWith(suffix, StringComparison.Ordinal) || + qualifiedName.Length < prefix.Length + suffix.Length) { continue; } - if (best is null || qualifiedName.Length < best.Length) + // Depth is the number of intermediate "::"-separated segments between + // resolvedNamespace and name — count separators in the portion of qualifiedName + // strictly between the matched prefix and suffix, not raw string length (segment + // names vary in length, so a deeper match can have a shorter qualified-name string + // than a shallower one). The length guard above excludes the exact one-level match + // "prefix + name" (already handled by the direct-candidate check before this method + // is called), where prefix and suffix overlap and slicing out a "middle" would + // otherwise be an invalid (negative-length) range. + var middle = qualifiedName[prefix.Length..^suffix.Length]; + var depth = middle.Length == 0 ? 0 : middle.Split("::").Length; + if (depth < bestDepth) { best = qualifiedName; + bestDepth = depth; } } diff --git a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs index fcde0476..7c7eb6d7 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs @@ -454,7 +454,68 @@ part def UsesBoth { } } - // Level 12d: Unqualified name resolves via recursive membership import, including the + // Level 12d: Ambiguous recursive wildcard match resolves to the shallowest namespace depth, + // not the shortest qualified-name string + /// + /// When two namespaces at different nesting depths under a recursive wildcard import's + /// target both declare a same-named member, resolution must prefer the genuinely + /// shallowest (least-nested) match — measured by the number of intermediate namespace + /// segments, not by comparing raw qualified-name string length. This uses deliberately + /// long package names at the shallow depth and short ones at the deep depth so a + /// length-based tie-break (an easy mistake) would pick the wrong one. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_RecursiveWildcardImport_AmbiguousMatch_PrefersShallowestDepth() + { + // Arrange — Foo is declared twice under A: once one level down in a namespace with a + // deliberately long name (shallow, but long string), and once three levels down through + // short-named namespaces (deep, but short string). The shallow one must win. + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); + try + { + await File.WriteAllTextAsync(tempFile, """ + package TestNs { + package A { + package AVeryLongIntermediateNamespaceName { + part def Foo; + } + package B { + package C { + package D { + part def Foo {} + } + } + } + } + } + package Consumer { + import TestNs::A::*::**; + + part def Uses { + part f : Foo; + } + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + Assert.NotNull(result.Workspace); + var index = result.Workspace!.Index; + + // Assert — "Foo" resolves as a Typing edge to the shallower (one-level-deep) match, + // not the deeper-but-shorter-string one. + Assert.Contains(index.GetOutgoingEdges("Consumer::Uses::f"), + e => e.Kind == DemaConsulting.SysML2Tools.Semantic.Model.SysmlEdgeKind.Typing && + e.TargetQualifiedName == "TestNs::A::AVeryLongIntermediateNamespaceName::Foo"); + } + finally + { + File.Delete(tempFile); + } + } + + // Level 12e: Unqualified name resolves via recursive membership import, including the // imported member's own name /// /// A recursive membership import (import X::Y::**;, as distinct from the