diff --git a/.chronus/changes/fix-blockless-namespace-using-resolution-2026-7-30.md b/.chronus/changes/fix-blockless-namespace-using-resolution-2026-7-30.md new file mode 100644 index 00000000000..1c67a55fdb7 --- /dev/null +++ b/.chronus/changes/fix-blockless-namespace-using-resolution-2026-7-30.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Fix false-positive name resolution failure when a `using` statement references a namespace whose first segment coincides with an intermediate segment in a multi-segment blockless namespace declaration (e.g. `using TypeSpec.Http` in a file with `namespace _Specs_.TypeSpec.Bar;`). The intermediate namespace segment `_Specs_.TypeSpec` was incorrectly shadowing the global `TypeSpec` namespace through the `inScopeNamespaces` lookup. diff --git a/packages/compiler/src/core/name-resolver.ts b/packages/compiler/src/core/name-resolver.ts index 314419008c9..84582dfd69e 100644 --- a/packages/compiler/src/core/name-resolver.ts +++ b/packages/compiler/src/core/name-resolver.ts @@ -159,6 +159,13 @@ export interface NameResolver { interface ResolveTypReferenceOptions { resolveDecorators?: boolean; + /** + * When true, global scope is checked before inScopeNamespaces at the TypeSpecScript scope. + * Used when resolving `using` statement targets so that e.g. `using TypeSpec.Http` always + * refers to the global TypeSpec namespace regardless of any blockless namespace declaration + * that creates a same-named intermediate segment (e.g. `namespace _Specs_.TypeSpec.Bar`). + */ + resolveGlobalFirst?: boolean; } // This needs to be global to be sure to not reallocate per program. @@ -1093,14 +1100,6 @@ export function createResolver(program: Program): NameResolver { } if (!binding && scope && scope.kind === SyntaxKind.TypeSpecScript) { - // check any blockless namespace decls - for (const ns of scope.inScopeNamespaces) { - const mergedSymbol = getMergedSymbol(ns.symbol); - binding = tableLookup(mergedSymbol.exports!, node, options.resolveDecorators); - - if (binding) return resolvedResult(binding); - } - // check "global scope" declarations const globalBinding = tableLookup( globalNamespaceNode.symbol.exports!, @@ -1111,22 +1110,61 @@ export function createResolver(program: Program): NameResolver { // check using types const usingBinding = tableLookup(scope.locals, node, options.resolveDecorators); - if (globalBinding && usingBinding) { - return ambiguousResult([globalBinding, usingBinding]); - } else if (globalBinding) { - return resolvedResult(globalBinding); - } else if (usingBinding) { - if (usingBinding.flags & SymbolFlags.DuplicateUsing) { - return ambiguousResult([ - ...((augmentedSymbolTables.get(scope.locals)?.duplicates.get(usingBinding) as any) ?? - []), - ]); + if (options.resolveGlobalFirst) { + // When resolving `using` statement targets, check global scope first so that + // e.g. `using TypeSpec.Http` always finds the global TypeSpec namespace rather + // than an intermediate segment such as `_Specs_.TypeSpec` introduced by a + // blockless namespace declaration like `namespace _Specs_.TypeSpec.Bar;`. + if (globalBinding) { + return resolvedResult(globalBinding); + } + + // check any blockless namespace decls + for (const ns of scope.inScopeNamespaces) { + const mergedSymbol = getMergedSymbol(ns.symbol); + binding = tableLookup(mergedSymbol.exports!, node, options.resolveDecorators); + if (binding) return resolvedResult(binding); } - if (usingBinding.flags & SymbolFlags.Using && usingBinding.symbolSource) { - usedUsingSym.get(scope)?.add(usingBinding.symbolSource) ?? - usedUsingSym.set(scope, new Set([usingBinding.symbolSource])); + + if (usingBinding) { + if (usingBinding.flags & SymbolFlags.DuplicateUsing) { + return ambiguousResult([ + ...((augmentedSymbolTables.get(scope.locals)?.duplicates.get(usingBinding) as any) ?? + []), + ]); + } + if (usingBinding.flags & SymbolFlags.Using && usingBinding.symbolSource) { + usedUsingSym.get(scope)?.add(usingBinding.symbolSource) ?? + usedUsingSym.set(scope, new Set([usingBinding.symbolSource])); + } + return resolvedResult(usingBinding.symbolSource!); + } + } else { + // Normal resolution: check any blockless namespace decls first (inner scope before + // outer), then global scope and usings with ambiguity detection. + for (const ns of scope.inScopeNamespaces) { + const mergedSymbol = getMergedSymbol(ns.symbol); + binding = tableLookup(mergedSymbol.exports!, node, options.resolveDecorators); + if (binding) return resolvedResult(binding); + } + + if (globalBinding && usingBinding) { + return ambiguousResult([globalBinding, usingBinding]); + } else if (globalBinding) { + return resolvedResult(globalBinding); + } else if (usingBinding) { + if (usingBinding.flags & SymbolFlags.DuplicateUsing) { + return ambiguousResult([ + ...((augmentedSymbolTables.get(scope.locals)?.duplicates.get(usingBinding) as any) ?? + []), + ]); + } + if (usingBinding.flags & SymbolFlags.Using && usingBinding.symbolSource) { + usedUsingSym.get(scope)?.add(usingBinding.symbolSource) ?? + usedUsingSym.set(scope, new Set([usingBinding.symbolSource])); + } + return resolvedResult(usingBinding.symbolSource!); } - return resolvedResult(usingBinding.symbolSource!); } } @@ -1242,6 +1280,7 @@ export function createResolver(program: Program): NameResolver { const parentNs = using.parent!; const { finalSymbol: usedSym, resolutionResult: usedSymResult } = resolveTypeReference( using.name, + { resolveGlobalFirst: true }, ); if (~usedSymResult & ResolutionResultFlags.Resolved) { continue; // Keep going and count on checker to report those errors. diff --git a/packages/compiler/test/checker/namespaces.test.ts b/packages/compiler/test/checker/namespaces.test.ts index 7ddc446dd80..b849d7510f0 100644 --- a/packages/compiler/test/checker/namespaces.test.ts +++ b/packages/compiler/test/checker/namespaces.test.ts @@ -3,7 +3,13 @@ import { describe, it } from "vitest"; import type { Program } from "../../src/core/program.js"; import type { Type } from "../../src/core/types.js"; import { getTypeName } from "../../src/index.js"; -import { expectDiagnostics, expectTypeEquals, mockFile, t } from "../../src/testing/index.js"; +import { + expectDiagnosticEmpty, + expectDiagnostics, + expectTypeEquals, + mockFile, + t, +} from "../../src/testing/index.js"; import { Tester } from "../tester.js"; describe("compiler: namespaces with blocks", () => { @@ -341,6 +347,25 @@ describe("compiler: blockless namespaces", () => { strictEqual(Foo.models.size, 1); strictEqual(Foo.namespaces.size, 1); }); + + it("does not let intermediate namespace segments shadow global namespace names in using statements", async () => { + // Regression test: `namespace X.A.C;` creates `X.A` as an intermediate segment. + // Previously `using A.B` would incorrectly resolve `A` to `X.A` (found via + // inScopeNamespaces ancestor) instead of the global `A` namespace. + const diagnostics = await Tester.files({ + "lib.tsp": ` + namespace A.B { + model BModel {} + } + `, + }).diagnose(` + import "./lib.tsp"; + using A.B; + namespace X.A.C; + model M extends BModel {} + `); + expectDiagnosticEmpty(diagnostics); + }); }); describe("compiler: namespace type name", () => {