Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
83 changes: 61 additions & 22 deletions packages/compiler/src/core/name-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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!,
Expand All @@ -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!);
}
}

Expand Down Expand Up @@ -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.
Expand Down
27 changes: 26 additions & 1 deletion packages/compiler/test/checker/namespaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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 () => {
Comment thread
timotheeguerin marked this conversation as resolved.
// 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", () => {
Expand Down