Skip to content
Merged
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,10 @@
{
"changes": [
{
"packageName": "@typespec/compiler",
"comment": "**Fix** Compiler crash when using alias of namespace that has decorators",
"type": "none"
}
],
"packageName": "@typespec/compiler"
}
55 changes: 39 additions & 16 deletions packages/compiler/src/core/binder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { compilerAssert } from "./diagnostics.js";
import { FileLibraryMetadata, getLocationContext } from "./index.js";
import {
FileLibraryMetadata,
JsNamespaceDeclarationNode,
NodeFlags,
getLocationContext,
} from "./index.js";
import { visitChildren } from "./parser.js";
import { Program } from "./program.js";
import {
Expand Down Expand Up @@ -154,32 +159,37 @@ export function createBinder(program: Program): Binder {
kind = "function";
}

const memberNs: string = (member as any).namespace;
const nsParts = [];
if (rootNs) {
nsParts.push(...rootNs.split("."));
}

if (memberNs) {
nsParts.push(...memberNs.split("."));
}

const nsParts = resolveJSMemberNamespaceParts(rootNs, member);
for (const part of nsParts) {
const existingBinding = containerSymbol.exports!.get(part);
const jsNamespaceNode: JsNamespaceDeclarationNode = {
kind: SyntaxKind.JsNamespaceDeclaration,
id: {
kind: SyntaxKind.Identifier,
sv: part,
pos: 0,
end: 0,
flags: NodeFlags.None,
symbol: undefined!,
},
pos: sourceFile.pos,
end: sourceFile.end,
parent: sourceFile,
flags: NodeFlags.None,
symbol: undefined!,
};
const sym = createSymbol(jsNamespaceNode, part, SymbolFlags.Namespace, containerSymbol);
mutate(jsNamespaceNode).symbol = sym;
if (existingBinding) {
if (existingBinding.flags & SymbolFlags.Namespace) {
// since the namespace was "declared" as part of this source file,
// we can simply re-use it.
containerSymbol = existingBinding;
} else {
// we have some conflict, lets report a duplicate binding error.
mutate(containerSymbol.exports)!.set(
part,
createSymbol(sourceFile, part, SymbolFlags.Namespace, containerSymbol)
);
mutate(containerSymbol.exports)!.set(part, sym);
}
} else {
const sym = createSymbol(sourceFile, part, SymbolFlags.Namespace, containerSymbol);
mutate(sym).exports = createSymbolTable();
mutate(containerSymbol.exports!).set(part, sym);
containerSymbol = sym;
Expand Down Expand Up @@ -212,6 +222,19 @@ export function createBinder(program: Program): Binder {
}
}

function resolveJSMemberNamespaceParts(rootNs: string | undefined, member: any) {
const memberNs: string = member.namespace;
const nsParts = [];
if (rootNs) {
nsParts.push(...rootNs.split("."));
}

if (memberNs) {
nsParts.push(...memberNs.split("."));
}
return nsParts;
}

function bindSourceFile(script: TypeSpecScriptNode) {
if (script.locals !== undefined) {
return;
Expand Down
26 changes: 16 additions & 10 deletions packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
InterfaceStatementNode,
IntersectionExpressionNode,
IntrinsicScalarName,
JsNamespaceDeclarationNode,
JsSourceFileNode,
LiteralNode,
LiteralType,
Expand Down Expand Up @@ -632,6 +633,7 @@ export function createChecker(program: Program): Checker {
case SyntaxKind.UnionVariant:
return checkUnionVariant(node, mapper);
case SyntaxKind.NamespaceStatement:
case SyntaxKind.JsNamespaceDeclaration:
return checkNamespace(node);
case SyntaxKind.OperationStatement:
return checkOperation(node, mapper);
Expand Down Expand Up @@ -1479,23 +1481,25 @@ export function createChecker(program: Program): Checker {
return getOrInstantiateTemplate(arrayNode, [param], [elementType], undefined) as Model;
}

function checkNamespace(node: NamespaceStatementNode) {
function checkNamespace(node: NamespaceStatementNode | JsNamespaceDeclarationNode) {
const links = getSymbolLinks(getMergedSymbol(node.symbol));
let type = links.type as Namespace;
if (!type) {
type = initializeTypeForNamespace(node);
}

if (isArray(node.statements)) {
node.statements.forEach((x) => getTypeForNode(x));
} else if (node.statements) {
const subNs = checkNamespace(node.statements);
type.namespaces.set(subNs.name, subNs);
if (node.kind === SyntaxKind.NamespaceStatement) {
if (isArray(node.statements)) {
node.statements.forEach((x) => getTypeForNode(x));
} else if (node.statements) {
const subNs = checkNamespace(node.statements);
type.namespaces.set(subNs.name, subNs);
}
}
return type;
}

function initializeTypeForNamespace(node: NamespaceStatementNode) {
function initializeTypeForNamespace(node: NamespaceStatementNode | JsNamespaceDeclarationNode) {
compilerAssert(node.symbol, "Namespace is unbound.", node);
const mergedSymbol = getMergedSymbol(node.symbol)!;
const symbolLinks = getSymbolLinks(mergedSymbol);
Expand All @@ -1507,7 +1511,7 @@ export function createChecker(program: Program): Checker {
kind: "Namespace",
name,
namespace,
node,
node: node,
models: new Map(),
scalars: new Map(),
operations: new Map(),
Expand Down Expand Up @@ -1539,6 +1543,7 @@ export function createChecker(program: Program): Checker {
| ModelStatementNode
| ScalarStatementNode
| NamespaceStatementNode
| JsNamespaceDeclarationNode
| OperationStatementNode
| EnumStatementNode
| InterfaceStatementNode
Expand Down Expand Up @@ -1602,7 +1607,8 @@ export function createChecker(program: Program): Checker {
// refers to a model in another namespace. In this case, we need to evaluate
// the namespace here.
const namespaceNode = mergedSymbol.declarations.find(
(x): x is NamespaceStatementNode => x.kind === SyntaxKind.NamespaceStatement
(x): x is NamespaceStatementNode =>
x.kind === SyntaxKind.NamespaceStatement || x.kind === SyntaxKind.JsNamespaceDeclaration
);
compilerAssert(namespaceNode, "Can't find namespace declaration node.", node);
symbolLinks.type = initializeTypeForNamespace(namespaceNode);
Expand Down Expand Up @@ -2294,7 +2300,7 @@ export function createChecker(program: Program): Checker {
default:
// get the symbol from the node aliased type's node, or just return the base
// if it doesn't have a symbol (which will likely result in an error later on)
return aliasType.node!.symbol ?? aliasSymbol;
return getMergedSymbol(aliasType.node!.symbol) ?? aliasSymbol;
}
}
function checkStringLiteral(str: StringLiteralNode): StringLiteral {
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/core/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3148,6 +3148,7 @@ export function visitChildren<T>(node: Node, cb: NodeCallback<T>): T | undefined
case SyntaxKind.ExternKeyword:
case SyntaxKind.UnknownKeyword:
case SyntaxKind.JsSourceFile:
case SyntaxKind.JsNamespaceDeclaration:
case SyntaxKind.DocText:
return;

Expand Down
8 changes: 7 additions & 1 deletion packages/compiler/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ export interface Namespace extends BaseType, DecoratedType {
kind: "Namespace";
name: string;
namespace?: Namespace;
node: NamespaceStatementNode;
node: NamespaceStatementNode | JsNamespaceDeclarationNode;

/**
* The models in the namespace.
Expand Down Expand Up @@ -783,6 +783,7 @@ export enum SyntaxKind {
ProjectionStatement,
ProjectionDecoratorReferenceExpression,
Return,
JsNamespaceDeclaration,
}

export const enum NodeFlags {
Expand Down Expand Up @@ -838,6 +839,7 @@ export interface TemplateDeclarationNode {
export type Node =
| TypeSpecScriptNode
| JsSourceFileNode
| JsNamespaceDeclarationNode
| TemplateParameterDeclarationNode
| ProjectionParameterDeclarationNode
| ProjectionLambdaParameterDeclarationNode
Expand Down Expand Up @@ -1616,6 +1618,10 @@ export interface JsSourceFileNode extends DeclarationNode, BaseNode {
readonly namespaceSymbols: Sym[];
}

export interface JsNamespaceDeclarationNode extends DeclarationNode, BaseNode {
readonly kind: SyntaxKind.JsNamespaceDeclaration;
}

export type EmitterFunc = (context: EmitContext) => Promise<void> | void;

export interface SourceFile {
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/formatter/print/printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ export function printNode(
case SyntaxKind.EmptyStatement:
return "";
case SyntaxKind.JsSourceFile:
case SyntaxKind.JsNamespaceDeclaration:
case SyntaxKind.InvalidStatement:
return getRawText(node, options);
default:
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler/test/binder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,11 +390,11 @@ describe("compiler: binder", () => {
assertBindings("jsFile", sourceFile.symbol.exports!, {
Foo: {
flags: SymbolFlags.Namespace,
declarations: [SyntaxKind.JsSourceFile],
declarations: [SyntaxKind.JsNamespaceDeclaration],
exports: {
Bar: {
flags: SymbolFlags.Namespace,
declarations: [SyntaxKind.JsSourceFile],
declarations: [SyntaxKind.JsNamespaceDeclaration],
exports: {
"@myDec2": {
flags: SymbolFlags.Decorator | SymbolFlags.Implementation,
Expand Down
28 changes: 27 additions & 1 deletion packages/compiler/test/checker/alias.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { ok, strictEqual } from "assert";
import { Model, Type, Union } from "../../src/core/types.js";
import { TestHost, createTestHost, expectDiagnostics } from "../../src/testing/index.js";
import {
TestHost,
createTestHost,
expectDiagnosticEmpty,
expectDiagnostics,
} from "../../src/testing/index.js";

describe("compiler: aliases", () => {
let testHost: TestHost;
Expand Down Expand Up @@ -231,4 +236,25 @@ describe("compiler: aliases", () => {
message: "Alias type 'A' recursively references itself.",
});
});

// REGRESSION TEST: https://github.com/Azure/typespec-azure/issues/3365
it("alias an namespace in JS file shouldn't crash", async () => {
testHost.addJsFile("lib.js", {
namespace: "Foo.Bar",
$foo: () => {},
});
testHost.addTypeSpecFile(
"main.tsp",
`
import "./lib.js";
namespace Foo.Bar { op abc(): void;}

alias Aliased = Foo.Bar;
op getSmurf is Aliased.abc;

`
);
const diagnostics = await testHost.diagnose("main.tsp");
expectDiagnosticEmpty(diagnostics);
});
});
5 changes: 4 additions & 1 deletion packages/compiler/test/server/completion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,10 @@ describe("compiler: server: completion", () => {
label: "Inner",
insertText: "Inner",
kind: CompletionItemKind.Module,
documentation: undefined,
documentation: {
kind: MarkupKind.Markdown,
value: "```typespec\nnamespace Outer.Inner\n```",
},
},
{
label: "outerDecorator",
Expand Down