From edb0d202fcacac139f5f88e83120b217e61b1478 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:11:23 +0000 Subject: [PATCH 1/6] Initial plan From 444f66bc3fbe9ad07557ec0b7b9880a8f7af446f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:42:00 +0000 Subject: [PATCH 2/6] fix(compiler): fix false-positive duplicate-using with blockless namespace Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- ...fix-using-blockless-namespace-2026-7-30.md | 7 ++++ packages/compiler/src/core/checker.ts | 15 +++++++- packages/compiler/test/checker/using.test.ts | 35 +++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 .chronus/changes/fix-using-blockless-namespace-2026-7-30.md diff --git a/.chronus/changes/fix-using-blockless-namespace-2026-7-30.md b/.chronus/changes/fix-using-blockless-namespace-2026-7-30.md new file mode 100644 index 00000000000..2dbae74095e --- /dev/null +++ b/.chronus/changes/fix-using-blockless-namespace-2026-7-30.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Fix false-positive `duplicate-using` diagnostic when a `using` statement appears before a file-level (blockless) namespace declaration and the same namespace is also used inside the file namespace. diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 30f44f4c4c5..d6956d07532 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -4962,9 +4962,22 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return newTacker; } + // The file-level (blockless) namespace node, if any. A using that appears after + // this namespace declaration is scoped to it (like a using inside a block namespace), + // and should be tracked separately from usings that appear before it. + const fileNamespaceNode = file.inScopeNamespaces[0]; + for (const using of file.usings) { const ns = using.parent!; - const sym = getMergedSymbol(ns.symbol); + // If the using appears after the blockless namespace declaration, treat it as + // scoped to that namespace for duplicate detection. This is consistent with how + // usings inside block namespaces are handled and avoids false-positive + // duplicate-using errors when the same namespace is imported both before and + // after a blockless namespace declaration. + const sym = + fileNamespaceNode !== undefined && using.pos >= fileNamespaceNode.pos + ? getMergedSymbol(fileNamespaceNode.symbol) + : getMergedSymbol(ns.symbol); const tracker = getTracker(sym); const targetSym = resolver.getNodeLinks(using.name).resolvedSymbol; if (!targetSym) continue; diff --git a/packages/compiler/test/checker/using.test.ts b/packages/compiler/test/checker/using.test.ts index a6a4c7669da..85cc27ab0c5 100644 --- a/packages/compiler/test/checker/using.test.ts +++ b/packages/compiler/test/checker/using.test.ts @@ -123,6 +123,41 @@ describe("compiler: using statements", () => { expectDiagnosticEmpty(diagnostics); }); + it("doesn't consider using before file namespace and using inside file namespace as duplicate", async () => { + const diagnostics = await Tester.files({ + "a.tsp": `namespace A { model AModel {} }`, + "b.tsp": ` + import "./a.tsp"; + using A; + namespace B; + using A; + `, + }).diagnose(` + import "./a.tsp"; + import "./b.tsp"; + `); + expectDiagnosticEmpty(diagnostics); + }); + + it("throws errors for duplicate usings inside a blockless namespace", async () => { + const diagnostics = await Tester.files({ + "a.tsp": `namespace A { model AModel {} }`, + "b.tsp": ` + import "./a.tsp"; + namespace B; + using A; + using A; + `, + }).diagnose(` + import "./a.tsp"; + import "./b.tsp"; + `); + expectDiagnostics(diagnostics, [ + { code: "duplicate-using", message: 'duplicate using of "A" namespace' }, + { code: "duplicate-using", message: 'duplicate using of "A" namespace' }, + ]); + }); + it("throws errors for duplicate imported usings", async () => { const diagnostics = await Tester.files({ "a.tsp": ` From 0531b8ce75c12ad234296fec6eafcf06f09c41cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:07:36 +0000 Subject: [PATCH 3/6] chore: revert incorrect duplicate-using fix; will address real issue Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- ...fix-using-blockless-namespace-2026-7-30.md | 7 ---- packages/compiler/src/core/checker.ts | 15 +------- packages/compiler/test/checker/using.test.ts | 35 ------------------- 3 files changed, 1 insertion(+), 56 deletions(-) delete mode 100644 .chronus/changes/fix-using-blockless-namespace-2026-7-30.md diff --git a/.chronus/changes/fix-using-blockless-namespace-2026-7-30.md b/.chronus/changes/fix-using-blockless-namespace-2026-7-30.md deleted file mode 100644 index 2dbae74095e..00000000000 --- a/.chronus/changes/fix-using-blockless-namespace-2026-7-30.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -changeKind: fix -packages: - - "@typespec/compiler" ---- - -Fix false-positive `duplicate-using` diagnostic when a `using` statement appears before a file-level (blockless) namespace declaration and the same namespace is also used inside the file namespace. diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index d6956d07532..30f44f4c4c5 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -4962,22 +4962,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return newTacker; } - // The file-level (blockless) namespace node, if any. A using that appears after - // this namespace declaration is scoped to it (like a using inside a block namespace), - // and should be tracked separately from usings that appear before it. - const fileNamespaceNode = file.inScopeNamespaces[0]; - for (const using of file.usings) { const ns = using.parent!; - // If the using appears after the blockless namespace declaration, treat it as - // scoped to that namespace for duplicate detection. This is consistent with how - // usings inside block namespaces are handled and avoids false-positive - // duplicate-using errors when the same namespace is imported both before and - // after a blockless namespace declaration. - const sym = - fileNamespaceNode !== undefined && using.pos >= fileNamespaceNode.pos - ? getMergedSymbol(fileNamespaceNode.symbol) - : getMergedSymbol(ns.symbol); + const sym = getMergedSymbol(ns.symbol); const tracker = getTracker(sym); const targetSym = resolver.getNodeLinks(using.name).resolvedSymbol; if (!targetSym) continue; diff --git a/packages/compiler/test/checker/using.test.ts b/packages/compiler/test/checker/using.test.ts index 85cc27ab0c5..a6a4c7669da 100644 --- a/packages/compiler/test/checker/using.test.ts +++ b/packages/compiler/test/checker/using.test.ts @@ -123,41 +123,6 @@ describe("compiler: using statements", () => { expectDiagnosticEmpty(diagnostics); }); - it("doesn't consider using before file namespace and using inside file namespace as duplicate", async () => { - const diagnostics = await Tester.files({ - "a.tsp": `namespace A { model AModel {} }`, - "b.tsp": ` - import "./a.tsp"; - using A; - namespace B; - using A; - `, - }).diagnose(` - import "./a.tsp"; - import "./b.tsp"; - `); - expectDiagnosticEmpty(diagnostics); - }); - - it("throws errors for duplicate usings inside a blockless namespace", async () => { - const diagnostics = await Tester.files({ - "a.tsp": `namespace A { model AModel {} }`, - "b.tsp": ` - import "./a.tsp"; - namespace B; - using A; - using A; - `, - }).diagnose(` - import "./a.tsp"; - import "./b.tsp"; - `); - expectDiagnostics(diagnostics, [ - { code: "duplicate-using", message: 'duplicate using of "A" namespace' }, - { code: "duplicate-using", message: 'duplicate using of "A" namespace' }, - ]); - }); - it("throws errors for duplicate imported usings", async () => { const diagnostics = await Tester.files({ "a.tsp": ` From 49a7d7a207571094924ffbaae0bd6d9a6de048c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:21:58 +0000 Subject: [PATCH 4/6] fix(compiler): fix name resolution for using with multi-segment blockless namespace Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- ...ss-namespace-using-resolution-2026-7-30.md | 7 +++++ packages/compiler/src/core/name-resolver.ts | 21 ++++++++++---- .../compiler/test/checker/namespaces.test.ts | 28 ++++++++++++++++++- 3 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 .chronus/changes/fix-blockless-namespace-using-resolution-2026-7-30.md 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..06581249d4b 100644 --- a/packages/compiler/src/core/name-resolver.ts +++ b/packages/compiler/src/core/name-resolver.ts @@ -1093,11 +1093,12 @@ 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); - + // check the leaf (innermost) blockless namespace decl first — this mirrors how + // block-namespace scoping works: the current namespace's own exports take priority. + if (scope.inScopeNamespaces.length > 0) { + const leafNs = scope.inScopeNamespaces[0]; + const mergedLeafSymbol = getMergedSymbol(leafNs.symbol); + binding = tableLookup(mergedLeafSymbol.exports!, node, options.resolveDecorators); if (binding) return resolvedResult(binding); } @@ -1128,6 +1129,16 @@ export function createResolver(program: Program): NameResolver { } return resolvedResult(usingBinding.symbolSource!); } + + // check parent blockless namespace decls (after global scope and usings so that + // global names are not shadowed by intermediate namespace segments in a multi-segment + // blockless namespace path such as `namespace A.TypeSpec.B;`) + for (let i = 1; i < scope.inScopeNamespaces.length; i++) { + const ns = scope.inScopeNamespaces[i]; + const mergedSymbol = getMergedSymbol(ns.symbol); + binding = tableLookup(mergedSymbol.exports!, node, options.resolveDecorators); + if (binding) return resolvedResult(binding); + } } return failedResult(ResolutionResultFlags.Unknown); diff --git a/packages/compiler/test/checker/namespaces.test.ts b/packages/compiler/test/checker/namespaces.test.ts index 7ddc446dd80..f4a6701458a 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,26 @@ 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 _Specs_.TypeSpec.Bar;` creates `_Specs_.TypeSpec` as an + // intermediate segment. Previously `using TypeSpec.Http` would incorrectly resolve + // `TypeSpec` to `_Specs_.TypeSpec` (found via inScopeNamespaces ancestor) instead of + // the global `TypeSpec` namespace. + const diagnostics = await Tester.files({ + "http.tsp": ` + namespace TypeSpec.Http { + model HttpModel {} + } + `, + }).diagnose(` + import "./http.tsp"; + using TypeSpec.Http; + namespace _Specs_.TypeSpec.Bar; + model M extends HttpModel {} + `); + expectDiagnosticEmpty(diagnostics); + }); }); describe("compiler: namespace type name", () => { From 7ab810505aa8d8693c20056fde4d90747ea0f54d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:31:04 +0000 Subject: [PATCH 5/6] test(compiler): simplify namespace names in blockless namespace regression test Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../compiler/test/checker/namespaces.test.ts | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/compiler/test/checker/namespaces.test.ts b/packages/compiler/test/checker/namespaces.test.ts index f4a6701458a..b849d7510f0 100644 --- a/packages/compiler/test/checker/namespaces.test.ts +++ b/packages/compiler/test/checker/namespaces.test.ts @@ -349,21 +349,20 @@ describe("compiler: blockless namespaces", () => { }); it("does not let intermediate namespace segments shadow global namespace names in using statements", async () => { - // Regression test: `namespace _Specs_.TypeSpec.Bar;` creates `_Specs_.TypeSpec` as an - // intermediate segment. Previously `using TypeSpec.Http` would incorrectly resolve - // `TypeSpec` to `_Specs_.TypeSpec` (found via inScopeNamespaces ancestor) instead of - // the global `TypeSpec` namespace. + // 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({ - "http.tsp": ` - namespace TypeSpec.Http { - model HttpModel {} + "lib.tsp": ` + namespace A.B { + model BModel {} } `, }).diagnose(` - import "./http.tsp"; - using TypeSpec.Http; - namespace _Specs_.TypeSpec.Bar; - model M extends HttpModel {} + import "./lib.tsp"; + using A.B; + namespace X.A.C; + model M extends BModel {} `); expectDiagnosticEmpty(diagnostics); }); From 53c4076736d6f0d5053537a1b25b35e292c21f4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:59:08 +0000 Subject: [PATCH 6/6] fix(compiler): use global-first resolution for using statement targets in blockless namespace files Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/compiler/src/core/name-resolver.ts | 92 ++++++++++++++------- 1 file changed, 60 insertions(+), 32 deletions(-) diff --git a/packages/compiler/src/core/name-resolver.ts b/packages/compiler/src/core/name-resolver.ts index 06581249d4b..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,15 +1100,6 @@ export function createResolver(program: Program): NameResolver { } if (!binding && scope && scope.kind === SyntaxKind.TypeSpecScript) { - // check the leaf (innermost) blockless namespace decl first — this mirrors how - // block-namespace scoping works: the current namespace's own exports take priority. - if (scope.inScopeNamespaces.length > 0) { - const leafNs = scope.inScopeNamespaces[0]; - const mergedLeafSymbol = getMergedSymbol(leafNs.symbol); - binding = tableLookup(mergedLeafSymbol.exports!, node, options.resolveDecorators); - if (binding) return resolvedResult(binding); - } - // check "global scope" declarations const globalBinding = tableLookup( globalNamespaceNode.symbol.exports!, @@ -1112,32 +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); } - if (usingBinding.flags & SymbolFlags.Using && usingBinding.symbolSource) { - usedUsingSym.get(scope)?.add(usingBinding.symbolSource) ?? - usedUsingSym.set(scope, new Set([usingBinding.symbolSource])); + + // 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); } - return resolvedResult(usingBinding.symbolSource!); - } - // check parent blockless namespace decls (after global scope and usings so that - // global names are not shadowed by intermediate namespace segments in a multi-segment - // blockless namespace path such as `namespace A.TypeSpec.B;`) - for (let i = 1; i < scope.inScopeNamespaces.length; i++) { - const ns = scope.inScopeNamespaces[i]; - const mergedSymbol = getMergedSymbol(ns.symbol); - binding = tableLookup(mergedSymbol.exports!, node, options.resolveDecorators); - if (binding) return resolvedResult(binding); + 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!); + } } } @@ -1253,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.