From b39abe5e6371c2ddae69539bfe5b32c1f6f09cf8 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 4 Nov 2024 15:50:10 -0800 Subject: [PATCH] Symbol refactor (#4849) ### Augment decorator issues Fix https://github.com/microsoft/typespec/issues/4749 Augment/ref alias properties Fix https://github.com/microsoft/typespec/issues/2867 Augment/ref `model is` properties Fix https://github.com/microsoft/typespec/issues/4818 Augment/ref nested model properties ### Circular reference issues Fix https://github.com/microsoft/typespec/issues/4908 freeze when using circular model extends with aliases ### Others FIx https://github.com/microsoft/typespec/issues/4915 (Duplicate usings not scoped per namespace) Doesn't resolve the alias circular issue https://github.com/microsoft/typespec/issues/2824 but I think setup the solution for fixing it later in the name resolver where we could detect that cycle instead of the checker --------- Co-authored-by: Brian Terlson --- .../symbol-refactor-2-2024-9-29-20-20-29.md | 8 + .../symbol-refactor-2-2024-9-29-20-20-30.md | 7 + cspell.yaml | 1 + .../compiler/.scripts/gen-extern-signature.ts | 30 +- .../generated-defs/TypeSpec.Prototypes.ts | 7 + .../TypeSpec.Prototypes.ts-test.ts | 5 + packages/compiler/lib/intrinsics.tsp | 3 +- packages/compiler/lib/prototypes.tsp | 18 + packages/compiler/package.json | 5 +- packages/compiler/src/core/binder.ts | 168 +- packages/compiler/src/core/checker.ts | 1279 ++++---------- packages/compiler/src/core/diagnostics.ts | 2 +- .../src/core/helpers/operation-utils.ts | 9 +- .../compiler/src/core/helpers/syntax-utils.ts | 14 + packages/compiler/src/core/inspector/node.ts | 38 + .../compiler/src/core/inspector/symbol.ts | 105 ++ packages/compiler/src/core/messages.ts | 16 +- packages/compiler/src/core/name-resolver.ts | 1291 ++++++++++++++ packages/compiler/src/core/program.ts | 12 +- packages/compiler/src/core/semantic-walker.ts | 4 +- packages/compiler/src/core/types.ts | 201 ++- packages/compiler/src/index.ts | 20 +- .../compiler/src/lib/intrinsic-decorators.ts | 27 - .../compiler/src/lib/intrinsic/decorators.ts | 41 + .../compiler/src/lib/intrinsic/tsp-index.ts | 12 + packages/compiler/src/server/completion.ts | 13 +- packages/compiler/src/server/type-details.ts | 5 +- .../compiler/src/server/type-signature.ts | 3 +- packages/compiler/src/testing/test-host.ts | 2 +- packages/compiler/src/testing/test-utils.ts | 22 +- packages/compiler/src/utils/misc.ts | 4 +- packages/compiler/test/binder.test.ts | 110 +- packages/compiler/test/checker/alias.test.ts | 6 +- .../test/checker/augment-decorators.test.ts | 635 ++++--- packages/compiler/test/checker/model.test.ts | 67 +- .../compiler/test/checker/operations.test.ts | 26 +- .../compiler/test/checker/references.test.ts | 92 +- .../checker/resolve-type-reference.test.ts | 26 +- packages/compiler/test/checker/scalar.test.ts | 32 + packages/compiler/test/checker/spread.test.ts | 4 +- .../compiler/test/checker/templates.test.ts | 2 +- packages/compiler/test/checker/using.test.ts | 118 +- packages/compiler/test/name-resolver.test.ts | 1488 +++++++++++++++++ .../test/tsp-openapi3/parameters.test.ts | 1 + .../gen-extern-signatures.ts | 27 +- .../decorators-signatures.test.ts | 6 +- pnpm-lock.yaml | 184 +- 47 files changed, 4604 insertions(+), 1592 deletions(-) create mode 100644 .chronus/changes/symbol-refactor-2-2024-9-29-20-20-29.md create mode 100644 .chronus/changes/symbol-refactor-2-2024-9-29-20-20-30.md create mode 100644 packages/compiler/generated-defs/TypeSpec.Prototypes.ts create mode 100644 packages/compiler/generated-defs/TypeSpec.Prototypes.ts-test.ts create mode 100644 packages/compiler/lib/prototypes.tsp create mode 100644 packages/compiler/src/core/inspector/node.ts create mode 100644 packages/compiler/src/core/inspector/symbol.ts create mode 100644 packages/compiler/src/core/name-resolver.ts delete mode 100644 packages/compiler/src/lib/intrinsic-decorators.ts create mode 100644 packages/compiler/src/lib/intrinsic/decorators.ts create mode 100644 packages/compiler/src/lib/intrinsic/tsp-index.ts create mode 100644 packages/compiler/test/name-resolver.test.ts diff --git a/.chronus/changes/symbol-refactor-2-2024-9-29-20-20-29.md b/.chronus/changes/symbol-refactor-2-2024-9-29-20-20-29.md new file mode 100644 index 0000000000..6343a78670 --- /dev/null +++ b/.chronus/changes/symbol-refactor-2-2024-9-29-20-20-29.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Overall of the symbol resolution. TypeSpec is able to resolve anything that can be statically linked. Augment decorators in turn are able to target any statically linkable types. diff --git a/.chronus/changes/symbol-refactor-2-2024-9-29-20-20-30.md b/.chronus/changes/symbol-refactor-2-2024-9-29-20-20-30.md new file mode 100644 index 0000000000..58e6e83866 --- /dev/null +++ b/.chronus/changes/symbol-refactor-2-2024-9-29-20-20-30.md @@ -0,0 +1,7 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: internal +packages: + - "@typespec/openapi3" +--- +Fix test host diff --git a/cspell.yaml b/cspell.yaml index 534cf79046..8bd06fe5d0 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -97,6 +97,7 @@ words: - Jacoco - jdwp - jobject + - Johan - jsyaml - keyer - killpg diff --git a/packages/compiler/.scripts/gen-extern-signature.ts b/packages/compiler/.scripts/gen-extern-signature.ts index f69d1d8314..c34facd1ce 100644 --- a/packages/compiler/.scripts/gen-extern-signature.ts +++ b/packages/compiler/.scripts/gen-extern-signature.ts @@ -2,7 +2,14 @@ import { format, resolveConfig } from "prettier"; import { fileURLToPath } from "url"; import { generateExternDecorators } from "../../tspd/dist/src/gen-extern-signatures/gen-extern-signatures.js"; -import { NodeHost, compile, resolvePath } from "../dist/src/index.js"; +import { + Namespace, + NodeHost, + compile, + formatDiagnostic, + logDiagnostics, + resolvePath, +} from "../dist/src/index.js"; const root = fileURLToPath(new URL("..", import.meta.url).href); const outDir = resolvePath(root, "generated-defs"); @@ -12,8 +19,27 @@ try { await NodeHost.mkdirp(outDir); const program = await compile(NodeHost, root, {}); +logDiagnostics(program.diagnostics, NodeHost.logSink); +if (program.hasError()) { + console.log("Has error not continuing"); + process.exit(1); +} + +const resolved = [ + program.resolveTypeReference("TypeSpec"), + program.resolveTypeReference("TypeSpec.Prototypes"), +]; +const namespaces: Namespace[] = []; +for (const [namespace, diagnostics] of resolved) { + if (namespace === undefined) { + throw new Error(`Cannot resolve namespace: \n${diagnostics.map(formatDiagnostic).join("\n")}`); + } else if (namespace.kind !== "Namespace") { + throw new Error(`Expected namespace but got ${namespace.kind}`); + } + namespaces.push(namespace); +} -const files = await generateExternDecorators(program, "@typespec/compiler"); +const files = await generateExternDecorators(program, "@typespec/compiler", { namespaces }); for (const [name, content] of Object.entries(files)) { const updatedContent = content.replace(/from "\@typespec\/compiler"/g, `from "../src/index.js"`); const prettierConfig = await resolveConfig(root); diff --git a/packages/compiler/generated-defs/TypeSpec.Prototypes.ts b/packages/compiler/generated-defs/TypeSpec.Prototypes.ts new file mode 100644 index 0000000000..0819a6017d --- /dev/null +++ b/packages/compiler/generated-defs/TypeSpec.Prototypes.ts @@ -0,0 +1,7 @@ +import type { DecoratorContext, Type } from "../src/index.js"; + +export type GetterDecorator = (context: DecoratorContext, target: Type) => void; + +export type TypeSpecPrototypesDecorators = { + getter: GetterDecorator; +}; diff --git a/packages/compiler/generated-defs/TypeSpec.Prototypes.ts-test.ts b/packages/compiler/generated-defs/TypeSpec.Prototypes.ts-test.ts new file mode 100644 index 0000000000..fd72052101 --- /dev/null +++ b/packages/compiler/generated-defs/TypeSpec.Prototypes.ts-test.ts @@ -0,0 +1,5 @@ +/** An error here would mean that the decorator is not exported or doesn't have the right name. */ +import { $decorators } from "../src/index.js"; +import type { TypeSpecPrototypesDecorators } from "./TypeSpec.Prototypes.js"; +/** An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ +const _: TypeSpecPrototypesDecorators = $decorators["TypeSpec.Prototypes"]; diff --git a/packages/compiler/lib/intrinsics.tsp b/packages/compiler/lib/intrinsics.tsp index 0750833714..d233fd7936 100644 --- a/packages/compiler/lib/intrinsics.tsp +++ b/packages/compiler/lib/intrinsics.tsp @@ -1,4 +1,5 @@ -import "../dist/src/lib/intrinsic-decorators.js"; +import "../dist/src/lib/intrinsic/tsp-index.js"; +import "./prototypes.tsp"; // This file contains all the intrinsic types of typespec. Everything here will always be loaded namespace TypeSpec; diff --git a/packages/compiler/lib/prototypes.tsp b/packages/compiler/lib/prototypes.tsp new file mode 100644 index 0000000000..f315dd5908 --- /dev/null +++ b/packages/compiler/lib/prototypes.tsp @@ -0,0 +1,18 @@ +namespace TypeSpec.Prototypes; + +extern dec getter(target: unknown); + +namespace Types { + interface ModelProperty { + @getter type(): unknown; + } + + interface Operation { + @getter returnType(): unknown; + @getter parameters(): unknown; + } + + interface Array { + @getter elementType(): TElementType; + } +} diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 6acad5eb33..14fba7a595 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -67,13 +67,13 @@ ], "scripts": { "clean": "rimraf ./dist ./temp", - "build:init-templates-index": "node --no-warnings --experimental-specifier-resolution=node --loader ts-node/esm ./.scripts/build-init-templates.ts", + "build:init-templates-index": "tsx ./.scripts/build-init-templates.ts", "build": "npm run gen-manifest && npm run build:init-templates-index && npm run compile && npm run generate-tmlanguage", "compile": "tsc -p .", "watch": "tsc -p . --watch", "watch-tmlanguage": "node scripts/watch-tmlanguage.js", "generate-tmlanguage": "node scripts/generate-tmlanguage.js", - "gen-extern-signature": "node --no-warnings --experimental-specifier-resolution=node --loader ts-node/esm ./.scripts/gen-extern-signature.ts", + "gen-extern-signature": "tsx ./.scripts/gen-extern-signature.ts", "dogfood": "node scripts/dogfood.js", "test": "vitest run", "test:ui": "vitest --ui", @@ -117,7 +117,6 @@ "rimraf": "~6.0.1", "source-map-support": "~0.5.21", "tmlanguage-generator": "workspace:~", - "ts-node": "~10.9.2", "typescript": "~5.6.3", "vitest": "^2.1.2", "vscode-oniguruma": "~2.0.1", diff --git a/packages/compiler/src/core/binder.ts b/packages/compiler/src/core/binder.ts index c79c7f049c..c23d235034 100644 --- a/packages/compiler/src/core/binder.ts +++ b/packages/compiler/src/core/binder.ts @@ -9,15 +9,19 @@ import { Declaration, DecoratorDeclarationStatementNode, DecoratorImplementations, + EnumMemberNode, EnumStatementNode, FileLibraryMetadata, FunctionDeclarationStatementNode, FunctionParameterNode, InterfaceStatementNode, + IntersectionExpressionNode, JsNamespaceDeclarationNode, JsSourceFileNode, ModelExpressionNode, + ModelPropertyNode, ModelStatementNode, + MutableSymbolTable, NamespaceStatementNode, Node, NodeFlags, @@ -27,6 +31,7 @@ import { ProjectionNode, ProjectionParameterDeclarationNode, ProjectionStatementNode, + ScalarConstructorNode, ScalarStatementNode, ScopeNode, Sym, @@ -36,38 +41,43 @@ import { TemplateParameterDeclarationNode, TypeSpecScriptNode, UnionStatementNode, + UnionVariantNode, UsingStatementNode, } from "./types.js"; // Use a regular expression to define the prefix for TypeSpec-exposed functions // defined in JavaScript modules const DecoratorFunctionPattern = /^\$/; -const SymbolTable = class extends Map implements SymbolTable { +const SymbolTable = class extends Map implements MutableSymbolTable { duplicates = new Map>(); constructor(source?: SymbolTable) { super(); if (source) { - for (const [key, value] of source) { - // Note: shallow copy of value here so we can mutate flags on set. - super.set(key, { ...value }); - } - for (const [key, value] of source.duplicates) { - this.duplicates.set(key, new Set(value)); - } + this.include(source); + } + } + + /** {@inheritdoc MutableSymboleTable} */ + include(source: SymbolTable, parentSym?: Sym) { + for (const [key, value] of source) { + super.set(key, { ...value, parent: parentSym ?? value.parent }); + } + for (const [key, value] of source.duplicates) { + this.duplicates.set(key, new Set(value)); } } // First set for a given key wins, but record all duplicates for diagnostics. set(key: string, value: Sym) { const existing = super.get(key); + if (existing === undefined) { super.set(key, value); } else { if (existing.flags & SymbolFlags.Using) { mutate(existing).flags |= SymbolFlags.DuplicateUsing; } - const duplicateArray = this.duplicates.get(existing); if (duplicateArray) { duplicateArray.add(value); @@ -126,7 +136,7 @@ export function createBinder(program: Program): Binder { mutate(sourceFile).symbol = createSymbol( sourceFile, sourceFile.file.path, - SymbolFlags.SourceFile, + SymbolFlags.SourceFile | SymbolFlags.Declaration, ); const rootNs = sourceFile.esmExports["namespace"]; @@ -218,7 +228,12 @@ export function createBinder(program: Program): Binder { flags: NodeFlags.None, symbol: undefined!, }; - const sym = createSymbol(jsNamespaceNode, part, SymbolFlags.Namespace, containerSymbol); + const sym = createSymbol( + jsNamespaceNode, + part, + SymbolFlags.Namespace | SymbolFlags.Declaration, + containerSymbol, + ); mutate(jsNamespaceNode).symbol = sym; if (existingBinding) { if (existingBinding.flags & SymbolFlags.Namespace) { @@ -241,7 +256,7 @@ export function createBinder(program: Program): Binder { sym = createSymbol( sourceFile, "@" + name, - SymbolFlags.Decorator | SymbolFlags.Implementation, + SymbolFlags.Decorator | SymbolFlags.Declaration | SymbolFlags.Implementation, containerSymbol, ); } else { @@ -249,7 +264,7 @@ export function createBinder(program: Program): Binder { sym = createSymbol( sourceFile, name, - SymbolFlags.Function | SymbolFlags.Implementation, + SymbolFlags.Function | SymbolFlags.Declaration | SymbolFlags.Implementation, containerSymbol, ); } @@ -296,9 +311,18 @@ export function createBinder(program: Program): Binder { case SyntaxKind.ModelExpression: bindModelExpression(node); break; + case SyntaxKind.ModelProperty: + bindModelProperty(node); + break; + case SyntaxKind.IntersectionExpression: + bindIntersectionExpression(node); + break; case SyntaxKind.ScalarStatement: bindScalarStatement(node); break; + case SyntaxKind.ScalarConstructor: + bindScalarConstructor(node); + break; case SyntaxKind.InterfaceStatement: bindInterfaceStatement(node); break; @@ -314,6 +338,12 @@ export function createBinder(program: Program): Binder { case SyntaxKind.EnumStatement: bindEnumStatement(node); break; + case SyntaxKind.EnumMember: + bindEnumMember(node); + break; + case SyntaxKind.UnionVariant: + bindUnionVariant(node); + break; case SyntaxKind.NamespaceStatement: bindNamespaceStatement(node); break; @@ -405,7 +435,12 @@ export function createBinder(program: Program): Binder { } mutate(sym.declarations).push(node); } else { - sym = createSymbol(node, name, SymbolFlags.Projection, scope.symbol); + sym = createSymbol( + node, + name, + SymbolFlags.Projection | SymbolFlags.Declaration, + scope.symbol, + ); mutate(table).set(name, sym); } @@ -465,13 +500,13 @@ export function createBinder(program: Program): Binder { } function bindProjectionParameterDeclaration(node: ProjectionParameterDeclarationNode) { - declareSymbol(node, SymbolFlags.ProjectionParameter); + declareSymbol(node, SymbolFlags.ProjectionParameter | SymbolFlags.Declaration); } function bindProjectionLambdaParameterDeclaration( node: ProjectionLambdaParameterDeclarationNode, ) { - declareSymbol(node, SymbolFlags.FunctionParameter); + declareSymbol(node, SymbolFlags.FunctionParameter | SymbolFlags.Declaration); } function bindProjectionLambdaExpression(node: ProjectionLambdaExpressionNode) { @@ -479,11 +514,11 @@ export function createBinder(program: Program): Binder { } function bindTemplateParameterDeclaration(node: TemplateParameterDeclarationNode) { - declareSymbol(node, SymbolFlags.TemplateParameter); + declareSymbol(node, SymbolFlags.TemplateParameter | SymbolFlags.Declaration); } function bindModelStatement(node: ModelStatementNode) { - declareSymbol(node, SymbolFlags.Model); + declareSymbol(node, SymbolFlags.Model | SymbolFlags.Declaration); // Initialize locals for type parameters mutate(node).locals = new SymbolTable(); } @@ -492,33 +527,55 @@ export function createBinder(program: Program): Binder { bindSymbol(node, SymbolFlags.Model); } + function bindModelProperty(node: ModelPropertyNode) { + declareMember(node, SymbolFlags.Member, node.id.sv); + } + + function bindIntersectionExpression(node: IntersectionExpressionNode) { + bindSymbol(node, SymbolFlags.Model); + } + function bindScalarStatement(node: ScalarStatementNode) { - declareSymbol(node, SymbolFlags.Scalar); + declareSymbol(node, SymbolFlags.Scalar | SymbolFlags.Declaration); // Initialize locals for type parameters mutate(node).locals = new SymbolTable(); } + function bindScalarConstructor(node: ScalarConstructorNode) { + declareMember(node, SymbolFlags.Member, node.id.sv); + } + function bindInterfaceStatement(node: InterfaceStatementNode) { - declareSymbol(node, SymbolFlags.Interface); + declareSymbol(node, SymbolFlags.Interface | SymbolFlags.Declaration); mutate(node).locals = new SymbolTable(); } function bindUnionStatement(node: UnionStatementNode) { - declareSymbol(node, SymbolFlags.Union); + declareSymbol(node, SymbolFlags.Union | SymbolFlags.Declaration); mutate(node).locals = new SymbolTable(); } function bindAliasStatement(node: AliasStatementNode) { - declareSymbol(node, SymbolFlags.Alias); + declareSymbol(node, SymbolFlags.Alias | SymbolFlags.Declaration); // Initialize locals for type parameters mutate(node).locals = new SymbolTable(); } function bindConstStatement(node: ConstStatementNode) { - declareSymbol(node, SymbolFlags.Const); + declareSymbol(node, SymbolFlags.Const | SymbolFlags.Declaration); } function bindEnumStatement(node: EnumStatementNode) { - declareSymbol(node, SymbolFlags.Enum); + declareSymbol(node, SymbolFlags.Enum | SymbolFlags.Declaration); + } + + function bindEnumMember(node: EnumMemberNode) { + declareMember(node, SymbolFlags.Member, node.id.sv); + } + function bindUnionVariant(node: UnionVariantNode) { + // cannot bind non named variant `union A { "a", "b"}` + if (node.id) { + declareMember(node, SymbolFlags.Member, node.id.sv); + } } function bindNamespaceStatement(statement: NamespaceStatementNode) { @@ -533,7 +590,7 @@ export function createBinder(program: Program): Binder { } else { // Initialize locals for non-exported symbols mutate(statement).locals = createSymbolTable(); - declareSymbol(statement, SymbolFlags.Namespace); + declareSymbol(statement, SymbolFlags.Namespace | SymbolFlags.Declaration); } currentFile.namespaces.push(statement); @@ -553,8 +610,14 @@ export function createBinder(program: Program): Binder { } function bindOperationStatement(statement: OperationStatementNode) { - if (scope.kind !== SyntaxKind.InterfaceStatement) { - declareSymbol(statement, SymbolFlags.Operation); + if (scope.kind === SyntaxKind.InterfaceStatement) { + declareMember( + statement, + SymbolFlags.Operation | SymbolFlags.Member | SymbolFlags.Declaration, + statement.id.sv, + ); + } else { + declareSymbol(statement, SymbolFlags.Operation | SymbolFlags.Declaration); } mutate(statement).locals = createSymbolTable(); } @@ -568,18 +631,24 @@ export function createBinder(program: Program): Binder { } function bindFunctionParameter(node: FunctionParameterNode) { - const symbol = createSymbol(node, node.id.sv, SymbolFlags.FunctionParameter, scope.symbol); + const symbol = createSymbol( + node, + node.id.sv, + SymbolFlags.FunctionParameter | SymbolFlags.Declaration, + scope.symbol, + ); mutate(node).symbol = symbol; } /** - * Declare a symbole for the given node in the current scope. + * Declare a symbol for the given node in the current scope. * @param node Node * @param flags Symbol flags * @param name Optional symbol name, default to the node id. * @returns Created Symbol */ function declareSymbol(node: Declaration, flags: SymbolFlags, name?: string) { + compilerAssert(flags & SymbolFlags.Declaration, `Expected declaration symbol: ${name}`, node); switch (scope.kind) { case SyntaxKind.NamespaceStatement: return declareNamespaceMember(node, flags, name); @@ -630,6 +699,28 @@ export function createBinder(program: Program): Binder { return symbol; } + /** + * Declare a member of a model, enum, union, or interface. + * @param node node of the member + * @param flags symbol flags + * @param name name of the symbol + */ + function declareMember( + node: + | ModelPropertyNode + | OperationStatementNode + | EnumMemberNode + | UnionVariantNode + | ScalarConstructorNode, + flags: SymbolFlags, + name: string, + ) { + const symbol = createSymbol(node, name, flags, scope.symbol); + mutate(node).symbol = symbol; + mutate(scope.symbol.members!).set(name, symbol); + return symbol; + } + function mergeNamespaceDeclarations(node: NamespaceStatementNode, scope: ScopeNode) { // we are declaring a namespace in either global scope, or a blockless namespace. const existingBinding = scope.symbol.exports!.get(node.id.sv); @@ -646,6 +737,7 @@ export function createBinder(program: Program): Binder { function hasScope(node: Node): node is ScopeNode { switch (node.kind) { case SyntaxKind.ModelStatement: + case SyntaxKind.ModelExpression: case SyntaxKind.ScalarStatement: case SyntaxKind.ConstStatement: case SyntaxKind.AliasStatement: @@ -655,6 +747,7 @@ function hasScope(node: Node): node is ScopeNode { case SyntaxKind.UnionStatement: case SyntaxKind.Projection: case SyntaxKind.ProjectionLambdaExpression: + case SyntaxKind.EnumStatement: return true; case SyntaxKind.NamespaceStatement: return node.statements !== undefined; @@ -679,8 +772,14 @@ export function createSymbol( members = createSymbolTable(); } + compilerAssert( + !(flags & SymbolFlags.Declaration) || node !== undefined, + "Declaration without node", + ); + return { - declarations: node ? [node] : [], + declarations: flags & SymbolFlags.Declaration ? [node!] : [], + node: !(flags & SymbolFlags.Declaration) ? node : (undefined as any), name, exports, members, @@ -690,3 +789,12 @@ export function createSymbol( metatypeMembers: createSymbolTable(), }; } + +/** + * Get the node attached to this symbol. + * If a declaration symbol get the first one `.declarations[0]` + * Otherwise get `.node` + */ +export function getSymNode(sym: Sym): Node { + return sym.flags & SymbolFlags.Declaration ? sym.declarations[0] : sym.node; +} diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index dbe140a45f..25dbc4efcd 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1,6 +1,7 @@ -import { $docFromComment, getIndexer } from "../lib/intrinsic-decorators.js"; +import { docFromCommentDecorator, getIndexer } from "../lib/intrinsic/decorators.js"; +import { DuplicateTracker } from "../utils/duplicate-tracker.js"; import { MultiKeyMap, Mutable, createRekeyableMap, isArray, mutate } from "../utils/misc.js"; -import { createSymbol, createSymbolTable } from "./binder.js"; +import { createSymbol, getSymNode } from "./binder.js"; import { createChangeIdentifierCodeFix } from "./compiler-code-fixes/change-identifier.codefix.js"; import { createModelToObjectValueCodeFix } from "./compiler-code-fixes/model-to-object-literal.codefix.js"; import { createTupleToArrayValueCodeFix } from "./compiler-code-fixes/tuple-to-array-value.codefix.js"; @@ -9,6 +10,7 @@ import { ProjectionError, compilerAssert, ignoreDiagnostics } from "./diagnostic import { validateInheritanceDiscriminatedUnions } from "./helpers/discriminator-utils.js"; import { getLocationContext } from "./helpers/location-context.js"; import { explainStringTemplateNotSerializable } from "./helpers/string-template-utils.js"; +import { typeReferenceToString } from "./helpers/syntax-utils.js"; import { getEntityName, getNamespaceFullName, @@ -17,6 +19,7 @@ import { } from "./helpers/type-name-utils.js"; import { legacyMarshallTypeForJS, marshallTypeForJS } from "./js-marshaller.js"; import { createDiagnostic } from "./messages.js"; +import { NameResolver } from "./name-resolver.js"; import { Numeric } from "./numeric.js"; import { exprIsBareIdentifier, @@ -50,7 +53,6 @@ import { CallExpressionNode, CodeFix, ConstStatementNode, - DecoratedType, Decorator, DecoratorApplication, DecoratorArgument, @@ -80,7 +82,6 @@ import { IntersectionExpressionNode, IntrinsicScalarName, JsNamespaceDeclarationNode, - JsSourceFileNode, LiteralNode, LiteralType, MemberContainerNode, @@ -101,7 +102,6 @@ import { NamespaceStatementNode, NeverType, Node, - NodeFlags, NullType, NullValue, NumericLiteral, @@ -132,6 +132,7 @@ import { ProjectionStatementItem, ProjectionStatementNode, ProjectionUnaryExpressionNode, + ResolutionResultFlags, ReturnExpressionNode, ReturnRecord, Scalar, @@ -179,6 +180,7 @@ import { UnionVariant, UnionVariantNode, UnknownType, + UsingStatementNode, Value, VoidType, } from "./types.js"; @@ -189,13 +191,16 @@ export interface Checker { typePrototype: TypePrototype; getTypeForNode(node: Node): Type; - setUsingsForFile(file: TypeSpecScriptNode): void; + + // TODO: decide if we expose resolver and deprecate those marked with @internal @deprecated checkProgram(): void; checkSourceFile(file: TypeSpecScriptNode): void; getGlobalNamespaceType(): Namespace; + /** @internal @deprecated */ getGlobalNamespaceNode(): NamespaceStatementNode; + /** @internal @deprecated */ getMergedSymbol(sym: Sym | undefined): Sym | undefined; - mergeSourceFile(file: TypeSpecScriptNode | JsSourceFileNode): void; + getLiteralType(node: StringLiteralNode): StringLiteral; getLiteralType(node: NumericLiteralNode): NumericLiteral; getLiteralType(node: BooleanLiteralNode): BooleanLiteral; @@ -328,15 +333,9 @@ const TypeInstantiationMap = class extends MultiKeyMap implements TypeInstantiationMap {}; -let currentSymbolId = 0; - -export function createChecker(program: Program): Checker { +export function createChecker(program: Program, resolver: NameResolver): Checker { const stdTypes: Partial = {}; - const symbolLinks = new Map(); - const mergedSymbols = new Map(); const docFromCommentForSym = new Map(); - const augmentDecoratorsForSym = new Map(); - const augmentedSymbolTables = new Map(); const referenceSymCache = new WeakMap< TypeReferenceNode | MemberExpressionNode | IdentifierNode, Sym | undefined @@ -356,7 +355,6 @@ export function createChecker(program: Program): Checker { return this.projections.filter((p) => p.id.sv === name); }, }; - const globalNamespaceNode = createGlobalNamespaceNode(); const globalNamespaceType = createGlobalNamespaceType(); // Caches the deprecation test of nodes in the program @@ -367,7 +365,6 @@ export function createChecker(program: Program): Checker { const neverType = createType({ kind: "Intrinsic", name: "never" } as const); const unknownType = createType({ kind: "Intrinsic", name: "unknown" } as const); const nullType = createType({ kind: "Intrinsic", name: "null" } as const); - const nullSym = createSymbol(undefined, "null", SymbolFlags.None); const projectionsByTypeKind = new Map([ ["Model", []], @@ -393,25 +390,11 @@ export function createChecker(program: Program): Checker { */ const pendingResolutions = new PendingResolutions(); - for (const file of program.jsSourceFiles.values()) { - mergeSourceFile(file); - } - - for (const file of program.sourceFiles.values()) { - mergeSourceFile(file); - } - - const typespecNamespaceBinding = globalNamespaceNode.symbol.exports!.get("TypeSpec"); + const typespecNamespaceBinding = resolver.symbols.global.exports!.get("TypeSpec"); if (typespecNamespaceBinding) { initializeTypeSpecIntrinsics(); - for (const file of program.sourceFiles.values()) { - addUsingSymbols(typespecNamespaceBinding.exports!, file.locals); - } } - for (const file of program.sourceFiles.values()) { - setUsingsForFile(file); - } let evalContext: EvalContext | undefined = undefined; const checker: Checker = { @@ -423,9 +406,7 @@ export function createChecker(program: Program): Checker { getNamespaceString: getNamespaceFullName, getGlobalNamespaceType, getGlobalNamespaceNode, - setUsingsForFile, getMergedSymbol, - mergeSourceFile, cloneType, resolveIdentifier, resolveCompletions, @@ -487,12 +468,12 @@ export function createChecker(program: Program): Checker { return voidType; }, declarations: [], + node: undefined as any, // TODO: is this correct? }); // Until we have an `unit` type for `null` - mutate(typespecNamespaceBinding!.exports).set("null", nullSym); - mutate(nullSym).type = nullType; - getSymbolLinks(nullSym).type = nullType; + mutate(resolver.symbols.null).type = nullType; + getSymbolLinks(resolver.symbols.null).type = nullType; } function getStdType(name: T): StdTypes[T] { @@ -502,10 +483,11 @@ export function createChecker(program: Program): Checker { } const sym = typespecNamespaceBinding?.exports?.get(name); - if (sym && sym.flags & SymbolFlags.Model) { + compilerAssert(sym, `Unexpected missing symbol to std type "${name}"`); + if (sym.flags & SymbolFlags.Model) { checkModelStatement(sym!.declarations[0] as any, undefined); } else { - checkScalar(sym!.declarations[0] as any, undefined); + checkScalar(sym.declarations[0] as any, undefined); } const loadedType = stdTypes[name]; @@ -516,119 +498,6 @@ export function createChecker(program: Program): Checker { return loadedType as any; } - function mergeSourceFile(file: TypeSpecScriptNode | JsSourceFileNode) { - mergeSymbolTable(file.symbol.exports!, mutate(globalNamespaceNode.symbol.exports!)); - } - - function setUsingsForFile(file: TypeSpecScriptNode) { - const usedUsing = new Set(); - - for (const using of file.usings) { - const parentNs = using.parent!; - const sym = resolveTypeReferenceSym(using.name, undefined); - if (!sym) { - continue; - } - - if (!(sym.flags & SymbolFlags.Namespace)) { - reportCheckerDiagnostic(createDiagnostic({ code: "using-invalid-ref", target: using })); - continue; - } - - const namespaceSym = getMergedSymbol(sym)!; - - if (usedUsing.has(namespaceSym)) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "duplicate-using", - format: { usingName: memberExpressionToString(using.name) }, - target: using, - }), - ); - continue; - } - usedUsing.add(namespaceSym); - addUsingSymbols(sym.exports!, parentNs.locals!); - } - } - - function applyAugmentDecorators(node: TypeSpecScriptNode | NamespaceStatementNode) { - if (!node.statements || !isArray(node.statements)) { - return; - } - - const augmentDecorators = node.statements.filter( - (x): x is AugmentDecoratorStatementNode => x.kind === SyntaxKind.AugmentDecoratorStatement, - ); - - for (const decNode of augmentDecorators) { - const ref = resolveTypeReferenceSym(decNode.targetType, undefined); - if (ref) { - let args: readonly TemplateArgumentNode[] = []; - if (ref.declarations[0].kind === SyntaxKind.AliasStatement) { - const aliasNode = ref.declarations[0] as AliasStatementNode; - if (aliasNode.value.kind === SyntaxKind.TypeReference) { - args = aliasNode.value.arguments; - } - } else { - args = decNode.targetType.arguments; - } - if (ref.flags & SymbolFlags.Namespace) { - const links = getSymbolLinks(getMergedSymbol(ref)); - const type: Type & DecoratedType = links.type! as any; - const decApp = checkDecoratorApplication(type, decNode, undefined); - if (decApp) { - type.decorators.push(decApp); - applyDecoratorToType(program, decApp, type); - } - } else if (args.length > 0 || ref.flags & SymbolFlags.LateBound) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "augment-decorator-target", - messageId: "noInstance", - target: decNode.target, - }), - ); - } else { - let list = augmentDecoratorsForSym.get(ref); - if (list === undefined) { - list = []; - augmentDecoratorsForSym.set(ref, list); - } - list.unshift(decNode); - } - } - } - } - - function addUsingSymbols(source: SymbolTable, destination: SymbolTable): void { - const augmented = getOrCreateAugmentedSymbolTable(destination); - for (const symbolSource of source.values()) { - const sym: Sym = { - flags: SymbolFlags.Using, - declarations: [], - name: symbolSource.name, - symbolSource: symbolSource, - }; - augmented.set(sym.name, sym); - } - } - - /** - * We cannot inject symbols into the symbol tables hanging off syntax tree nodes as - * syntax tree nodes can be shared by other programs. This is called as a copy-on-write - * to inject using and late-bound symbols, and then we use the copy when resolving - * in the table. - */ - function getOrCreateAugmentedSymbolTable(table: SymbolTable): Mutable { - let augmented = augmentedSymbolTables.get(table); - if (!augmented) { - augmented = createSymbolTable(table); - augmentedSymbolTables.set(table, augmented); - } - return mutate(augmented); - } - /** * Create the link for the given type to the symbol links. * If currently instantiating a template it will link to the instantiations. @@ -660,13 +529,14 @@ export function createChecker(program: Program): Checker { */ function checkMemberSym(sym: Sym, mapper: TypeMapper | undefined): Type { const symbolLinks = getSymbolLinks(sym); - const memberContainer = getTypeForNode(sym.parent!.declarations[0], mapper); + const memberContainer = getTypeForNode(getSymNode(sym.parent!), mapper); const type = symbolLinks.declaredType ?? symbolLinks.type; + if (type) { return type; } else { return checkMember( - sym.declarations[0] as MemberNode, + getSymNode(sym) as MemberNode, mapper, memberContainer as MemberContainerType, )!; @@ -1121,6 +991,10 @@ export function createChecker(program: Program): Checker { return checkCallExpression(node, mapper); case SyntaxKind.TypeOfExpression: return checkTypeOfExpression(node, mapper); + case SyntaxKind.AugmentDecoratorStatement: + return checkAugmentDecorator(node); + case SyntaxKind.UsingStatement: + return checkUsings(node); default: return errorType; } @@ -1129,7 +1003,7 @@ export function createChecker(program: Program): Checker { /** * Return a fully qualified id of node */ - function getNodeSymId( + function getNodeSym( node: | ModelStatementNode | ScalarStatementNode @@ -1139,14 +1013,13 @@ export function createChecker(program: Program): Checker { | OperationStatementNode | TemplateParameterDeclarationNode | UnionStatementNode, - ): number { + ): Sym { const symbol = node.kind === SyntaxKind.OperationStatement && node.parent?.kind === SyntaxKind.InterfaceStatement ? getSymbolForMember(node) : node.symbol; - // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - return symbol?.id!; + return symbol!; } /** @@ -1189,7 +1062,7 @@ export function createChecker(program: Program): Checker { const grandParentNode = parentNode.parent; const links = getSymbolLinks(node.symbol); - if (pendingResolutions.has(getNodeSymId(node), ResolutionKind.Constraint)) { + if (pendingResolutions.has(getNodeSym(node), ResolutionKind.Constraint)) { if (mapper === undefined) { reportCheckerDiagnostic( createDiagnostic({ @@ -1222,9 +1095,9 @@ export function createChecker(program: Program): Checker { }); if (node.constraint) { - pendingResolutions.start(getNodeSymId(node), ResolutionKind.Constraint); + pendingResolutions.start(getNodeSym(node), ResolutionKind.Constraint); type.constraint = getParamConstraintEntityForNode(node.constraint); - pendingResolutions.finish(getNodeSymId(node), ResolutionKind.Constraint); + pendingResolutions.finish(getNodeSym(node), ResolutionKind.Constraint); } if (node.default) { type.default = checkTemplateParameterDefault( @@ -1664,13 +1537,14 @@ export function createChecker(program: Program): Checker { const symbolLinks = getSymbolLinks(sym); let baseType: Type | IndeterminateEntity; if ( + sym.flags & SymbolFlags.Declaration && sym.flags & - (SymbolFlags.Model | - SymbolFlags.Scalar | - SymbolFlags.Alias | - SymbolFlags.Interface | - SymbolFlags.Operation | - SymbolFlags.Union) + (SymbolFlags.Model | + SymbolFlags.Scalar | + SymbolFlags.Alias | + SymbolFlags.Interface | + SymbolFlags.Operation | + SymbolFlags.Union) ) { const decl = sym.declarations[0] as TemplateableNode; if (!isTemplatedNode(decl)) { @@ -1715,6 +1589,7 @@ export function createChecker(program: Program): Checker { ); } } else { + const symNode = getSymNode(sym); // some other kind of reference if (argumentNodes.length > 0) { reportCheckerDiagnostic( @@ -1731,7 +1606,7 @@ export function createChecker(program: Program): Checker { return sym.type; } else if (sym.flags & SymbolFlags.TemplateParameter) { const mapped = checkTemplateParameterDeclaration( - sym.declarations[0] as TemplateParameterDeclarationNode, + symNode as TemplateParameterDeclarationNode, mapper, ); baseType = mapped as any; @@ -1745,7 +1620,7 @@ export function createChecker(program: Program): Checker { baseType = checkMemberSym(sym, mapper); } else { // don't have a cached type for this symbol, so go grab it and cache it - baseType = getTypeForNode(sym.declarations[0], mapper); + baseType = getTypeForNode(symNode, mapper); symbolLinks.type = baseType; } } @@ -1754,7 +1629,7 @@ export function createChecker(program: Program): Checker { // Check for deprecations here, first on symbol, then on type. However, // don't raise deprecation when the usage site is also a deprecated // declaration. - const declarationNode = sym?.declarations[0]; + const declarationNode = getSymNode(sym); if (declarationNode && mapper === undefined && isType(baseType)) { if (!isTypeReferenceContextDeprecated(node.parent!)) { checkDeprecated(baseType, declarationNode, node); @@ -2017,8 +1892,17 @@ export function createChecker(program: Program): Checker { node: IntersectionExpressionNode, mapper: TypeMapper | undefined, ) { + const links = getSymbolLinks(node.symbol); + + if (links.declaredType && mapper === undefined) { + // we're not instantiating this model and we've already checked it + return links.declaredType as any; + } + const options = node.options.map((o): [Expression, Type] => [o, getTypeForNode(o, mapper)]); - return mergeModelTypes(node, options, mapper); + const type = mergeModelTypes(node.symbol, node, options, mapper); + linkType(links, type, mapper); + return type; } function checkDecoratorDeclaration( @@ -2207,11 +2091,8 @@ export function createChecker(program: Program): Checker { } function mergeModelTypes( - node: - | ModelStatementNode - | ModelExpressionNode - | IntersectionExpressionNode - | ProjectionModelExpressionNode, + parentModelSym: Sym | undefined, + node: ModelStatementNode | ModelExpressionNode | IntersectionExpressionNode, options: [Node, Type][], mapper: TypeMapper | undefined, ) { @@ -2272,11 +2153,17 @@ export function createChecker(program: Program): Checker { continue; } - const newPropType = cloneType(prop, { + const memberSym = parentModelSym && getMemberSymbol(parentModelSym, prop.name); + const overrides: Partial = { sourceProperty: prop, model: intersection, - }); + }; + const newPropType = memberSym + ? cloneTypeForSymbol(memberSym, prop, overrides) + : cloneType(prop, overrides); properties.set(prop.name, newPropType); + linkIndirectMember(node, newPropType, mapper); + for (const indexer of indexers.filter((x) => x !== option.indexer)) { checkPropertyCompatibleWithIndexer(indexer, prop, node); } @@ -2289,6 +2176,7 @@ export function createChecker(program: Program): Checker { intersection.indexer = { key: indexers[0].key, value: mergeModelTypes( + undefined, node, indexers.map((x) => [x.value.node!, x.value]), mapper, @@ -2484,12 +2372,13 @@ export function createChecker(program: Program): Checker { const name = node.id.sv; let decorators: DecoratorApplication[] = []; - const parameterModelSym = getOrCreateAugmentedSymbolTable(symbol!.metatypeMembers!).get( + const { resolvedSymbol: parameterModelSym } = resolver.resolveMetaMemberByName( + symbol!, "parameters", ); if (parameterModelSym?.members) { - const members = getOrCreateAugmentedSymbolTable(parameterModelSym.members); + const members = resolver.getAugmentedSymbolTable(parameterModelSym.members); const paramDocs = extractParamDocs(node); for (const [name, memberSym] of members) { const doc = paramDocs.get(name); @@ -2591,25 +2480,20 @@ export function createChecker(program: Program): Checker { ): Operation | undefined { if (!opReference) return undefined; // Ensure that we don't end up with a circular reference to the same operation - const opSymId = getNodeSymId(operation); + const opSymId = getNodeSym(operation); if (opSymId) { pendingResolutions.start(opSymId, ResolutionKind.BaseType); } - const target = resolveTypeReferenceSym(opReference, mapper); - if (target === undefined) { - return undefined; - } + const target = resolver.getNodeLinks(opReference).resolvedSymbol; // Did we encounter a circular operation reference? - if ( - pendingResolutions.has(getNodeSymId(target.declarations[0] as any), ResolutionKind.BaseType) - ) { + if (target && pendingResolutions.has(target, ResolutionKind.BaseType)) { if (mapper === undefined) { reportCheckerDiagnostic( createDiagnostic({ code: "circular-op-signature", - format: { typeName: (target.declarations[0] as any).id.sv }, + format: { typeName: target.name }, target: opReference, }), ); @@ -2619,7 +2503,7 @@ export function createChecker(program: Program): Checker { } // Resolve the base operation type - const baseOperation = checkTypeReferenceSymbol(target, opReference, mapper); + const baseOperation = getTypeForNode(opReference, mapper); if (opSymId) { pendingResolutions.finish(opSymId, ResolutionKind.BaseType); } @@ -2641,8 +2525,8 @@ export function createChecker(program: Program): Checker { return globalNamespaceType; } - function getGlobalNamespaceNode() { - return globalNamespaceNode; + function getGlobalNamespaceNode(): NamespaceStatementNode { + return resolver.symbols.global.declarations[0] as any; } function checkTupleExpression(node: TupleExpressionNode, mapper: TypeMapper | undefined): Tuple { @@ -2654,60 +2538,7 @@ export function createChecker(program: Program): Checker { } function getSymbolLinks(s: Sym): SymbolLinks { - const id = getSymbolId(s); - if (symbolLinks.has(id)) { - return symbolLinks.get(id)!; - } - - const links = {}; - symbolLinks.set(id, links); - - return links; - } - - function getSymbolId(s: Sym) { - if (s.id === undefined) { - mutate(s).id = currentSymbolId++; - } - return s.id!; - } - - function resolveIdentifierInTable( - node: IdentifierNode, - table: SymbolTable | undefined, - options: SymbolResolutionOptions, - ): Sym | undefined { - if (!table) { - return undefined; - } - table = augmentedSymbolTables.get(table) ?? table; - let sym; - if (options.resolveDecorators) { - sym = table.get("@" + node.sv); - } else { - sym = table.get(node.sv); - } - - if (!sym) return sym; - - if (sym.flags & SymbolFlags.DuplicateUsing) { - reportAmbiguousIdentifier(node, [...((table.duplicates.get(sym) as any) ?? [])]); - return sym; - } - return getMergedSymbol(sym); - } - - function reportAmbiguousIdentifier(node: IdentifierNode, symbols: Sym[]) { - const duplicateNames = symbols.map((s) => - getFullyQualifiedSymbolName(s, { useGlobalPrefixAtTopLevel: true }), - ); - reportCheckerDiagnostic( - createDiagnostic({ - code: "ambiguous-symbol", - format: { name: node.sv, duplicateNames: duplicateNames.join(", ") }, - target: node, - }), - ); + return resolver.getSymbolLinks(s); } function resolveIdentifier(id: IdentifierNode, mapper?: TypeMapper): Sym | undefined { @@ -2726,37 +2557,8 @@ export function createChecker(program: Program): Checker { break; case IdentifierKind.ModelStatementProperty: case IdentifierKind.Declaration: - if (node.symbol && (!isTemplatedNode(node) || mapper === undefined)) { - sym = getMergedSymbol(node.symbol); - break; - } - - compilerAssert(node.parent, "Parent expected."); - const containerType = getTypeOrValueForNode(node.parent, mapper); - if (containerType === null || isValue(containerType)) { - return undefined; - } - if (isAnonymous(containerType)) { - return undefined; // member of anonymous type cannot be referenced. - } - - lateBindMemberContainer(containerType); - let container = node.parent.symbol; - if (!container && "symbol" in containerType && containerType.symbol) { - container = containerType.symbol; - } - - if (!container) { - return undefined; - } - - lateBindMembers(containerType, container); - sym = resolveIdentifierInTable( - id, - container.exports ?? container.members, - defaultSymbolResolutionOptions, - ); - break; + const links = resolver.getNodeLinks(id); + return links.resolvedSymbol; case IdentifierKind.Other: return undefined; @@ -3125,19 +2927,12 @@ export function createChecker(program: Program): Checker { } } } else if (identifier.parent && identifier.parent.kind === SyntaxKind.MemberExpression) { - let base = resolveTypeReferenceSym(identifier.parent.base, undefined, false); + let base = resolver.getNodeLinks(identifier.parent.base).resolvedSymbol; if (base) { if (base.flags & SymbolFlags.Alias) { - base = getAliasedSymbol(base, undefined, defaultSymbolResolutionOptions); + base = getAliasedSymbol(base, undefined); } if (base) { - if (isTemplatedNode(base.declarations[0])) { - const type = base.type ?? getTypeForNode(base.declarations[0], undefined); - if (isTemplateInstance(type)) { - lateBindMemberContainer(type); - lateBindMembers(type, base); - } - } addCompletions(base.exports ?? base.members); } } @@ -3182,7 +2977,7 @@ export function createChecker(program: Program): Checker { } // check "global scope" declarations - addCompletions(globalNamespaceNode.symbol.exports); + addCompletions(resolver.symbols.global.exports); // check "global scope" usings addCompletions(scope.locals); @@ -3196,7 +2991,7 @@ export function createChecker(program: Program): Checker { return; } - table = augmentedSymbolTables.get(table) ?? table; + table = resolver.getAugmentedSymbolTable(table); for (const [key, sym] of table) { if (sym.flags & SymbolFlags.DuplicateUsing) { const duplicates = table.duplicates.get(sym)!; @@ -3232,7 +3027,7 @@ export function createChecker(program: Program): Checker { case IdentifierKind.ModelExpressionProperty: case IdentifierKind.ModelStatementProperty: case IdentifierKind.ObjectLiteralProperty: - return !!(sym.flags & SymbolFlags.ModelProperty); + return !!(sym.flags & SymbolFlags.Member); case IdentifierKind.Decorator: // Only return decorators and namespaces when completing decorator return !!(sym.flags & (SymbolFlags.Decorator | SymbolFlags.Namespace)); @@ -3250,82 +3045,6 @@ export function createChecker(program: Program): Checker { } } - function resolveIdentifierInScope( - node: IdentifierNode, - mapper: TypeMapper | undefined, - options: SymbolResolutionOptions, - ): Sym | undefined { - compilerAssert( - node.parent?.kind !== SyntaxKind.MemberExpression || node.parent.id !== node, - "This function should not be used to resolve Y in member expression X.Y. Use resolveIdentifier() to resolve an arbitrary identifier.", - ); - - if (hasParseError(node)) { - // Don't report synthetic identifiers used for parser error recovery. - // The parse error is the root cause and will already have been logged. - return undefined; - } - - let scope: Node | undefined = node.parent; - let binding: Sym | undefined; - - while (scope && scope.kind !== SyntaxKind.TypeSpecScript) { - if (scope.symbol && "exports" in scope.symbol) { - const mergedSymbol = getMergedSymbol(scope.symbol); - binding = resolveIdentifierInTable(node, mergedSymbol.exports, options); - if (binding) return binding; - } - - if ("locals" in scope) { - binding = resolveIdentifierInTable(node, scope.locals, options); - if (binding) return binding; - } - - scope = scope.parent; - } - - if (!binding && scope && scope.kind === SyntaxKind.TypeSpecScript) { - // check any blockless namespace decls - for (const ns of scope.inScopeNamespaces) { - const mergedSymbol = getMergedSymbol(ns.symbol); - binding = resolveIdentifierInTable(node, mergedSymbol.exports, options); - - if (binding) return binding; - } - - // check "global scope" declarations - const globalBinding = resolveIdentifierInTable( - node, - globalNamespaceNode.symbol.exports, - options, - ); - - // check using types - const usingBinding = resolveIdentifierInTable(node, scope.locals, options); - - if (globalBinding && usingBinding) { - reportAmbiguousIdentifier(node, [globalBinding, usingBinding]); - return globalBinding; - } else if (globalBinding) { - return globalBinding; - } else if (usingBinding) { - return usingBinding.flags & SymbolFlags.DuplicateUsing ? undefined : usingBinding; - } - } - - if (mapper === undefined) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "unknown-identifier", - format: { id: node.sv }, - target: node, - codefixes: getCodefixesForUnknownIdentifier(node), - }), - ); - } - return undefined; - } - function getCodefixesForUnknownIdentifier(node: IdentifierNode): CodeFix[] | undefined { switch (node.sv) { case "number": @@ -3364,20 +3083,44 @@ export function createChecker(program: Program): Checker { // The parse error is the root cause and will already have been logged. return undefined; } - if (node.kind === SyntaxKind.TypeReference) { return resolveTypeReferenceSym(node.target, mapper, options); - } + } else if (node.kind === SyntaxKind.Identifier) { + const links = resolver.getNodeLinks(node); + if (mapper === undefined && links.resolutionResult) { + if ( + mapper === undefined && // do not report error when instantiating + links.resolutionResult & (ResolutionResultFlags.NotFound | ResolutionResultFlags.Unknown) + ) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-ref", + messageId: options.resolveDecorators ? "decorator" : "identifier", + format: { id: printTypeReferenceNode(node) }, + target: node, + codefixes: getCodefixesForUnknownIdentifier(node), + }), + ); + } else if (links.resolutionResult & ResolutionResultFlags.Ambiguous) { + reportAmbiguousIdentifier(node, links.ambiguousSymbols!); + } + } + + const sym = links.resolvedSymbol; + return sym?.symbolSource ?? sym; + } else if (node.kind === SyntaxKind.MemberExpression) { + let base = resolveTypeReferenceSym(node.base, mapper, { + ...options, + resolveDecorators: false, // when resolving decorator the base cannot also be one + }); - if (node.kind === SyntaxKind.MemberExpression) { - let base = resolveTypeReferenceSym(node.base, mapper); if (!base) { return undefined; } // when resolving a type reference based on an alias, unwrap the alias. if (base.flags & SymbolFlags.Alias) { - const aliasedSym = getAliasedSymbol(base, mapper, options); + const aliasedSym = getAliasedSymbol(base, mapper); if (!aliasedSym) { reportCheckerDiagnostic( createDiagnostic({ @@ -3396,47 +3139,52 @@ export function createChecker(program: Program): Checker { } base = aliasedSym; } - - if (node.selector === ".") { - return resolveMemberInContainer(node, base, mapper, options); - } else { - return resolveMetaProperty(node, base); - } - } - - if (node.kind === SyntaxKind.Identifier) { - const sym = resolveIdentifierInScope(node, mapper, options); - if (!sym) return undefined; - - return sym.flags & SymbolFlags.Using ? sym.symbolSource : sym; + return resolveMemberInContainer(base, node, options); } compilerAssert(false, `Unknown type reference kind "${SyntaxKind[(node as any).kind]}"`, node); } + function reportAmbiguousIdentifier(node: IdentifierNode, symbols: Sym[]) { + const duplicateNames = symbols.map((s) => + getFullyQualifiedSymbolName(s, { useGlobalPrefixAtTopLevel: true }), + ); + program.reportDiagnostic( + createDiagnostic({ + code: "ambiguous-symbol", + format: { name: node.sv, duplicateNames: duplicateNames.join(", ") }, + target: node, + }), + ); + } + function resolveMemberInContainer( - node: MemberExpressionNode, base: Sym, - mapper: TypeMapper | undefined, + node: MemberExpressionNode, options: SymbolResolutionOptions, ) { - if (base.flags & SymbolFlags.Namespace) { - const symbol = resolveIdentifierInTable(node.id, base.exports, options); - if (!symbol) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "invalid-ref", - messageId: "underNamespace", - format: { - namespace: getFullyQualifiedSymbolName(base), - id: node.id.sv, - }, - target: node, - }), - ); - return undefined; - } + const { finalSymbol: sym, resolvedSymbol: nextSym } = resolver.resolveMemberExpressionForSym( + base, + node, + options, + ); + const symbol = nextSym ?? sym; + if (symbol) { return symbol; + } + + if (base.flags & SymbolFlags.Namespace) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-ref", + messageId: "underNamespace", + format: { + namespace: getFullyQualifiedSymbolName(base), + id: node.id.sv, + }, + target: node, + }), + ); } else if (base.flags & SymbolFlags.Decorator) { reportCheckerDiagnostic( createDiagnostic({ @@ -3446,7 +3194,6 @@ export function createChecker(program: Program): Checker { target: node, }), ); - return undefined; } else if (base.flags & SymbolFlags.Function) { reportCheckerDiagnostic( createDiagnostic({ @@ -3456,65 +3203,30 @@ export function createChecker(program: Program): Checker { target: node, }), ); - - return undefined; } else if (base.flags & SymbolFlags.MemberContainer) { - if (options.checkTemplateTypes && isTemplatedNode(base.declarations[0])) { - const type = - base.flags & SymbolFlags.LateBound - ? base.type! - : getTypeForNode(base.declarations[0], mapper); - if (isTemplateInstance(type)) { - lateBindMembers(type, base); - } - } - const sym = resolveIdentifierInTable(node.id, base.members!, options); - if (!sym) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "invalid-ref", - messageId: "underContainer", - format: { kind: getMemberKindName(base.declarations[0]), id: node.id.sv }, - target: node, - }), - ); - return undefined; - } - return sym; - } else { reportCheckerDiagnostic( createDiagnostic({ code: "invalid-ref", - messageId: "node", - format: { - id: node.id.sv, - nodeName: base.declarations[0] ? SyntaxKind[base.declarations[0].kind] : "Unknown node", - }, + messageId: node.selector === "." ? "member" : "metaProperty", + format: { kind: getMemberKindName(getSymNode(base)), id: node.id.sv }, target: node, }), ); - - return undefined; - } - } - - function resolveMetaProperty(node: MemberExpressionNode, base: Sym) { - const resolved = resolveIdentifierInTable(node.id, base.metatypeMembers, { - resolveDecorators: false, - checkTemplateTypes: false, - }); - if (!resolved) { + } else { + const symNode = getSymNode(base); reportCheckerDiagnostic( createDiagnostic({ code: "invalid-ref", - messageId: "metaProperty", - format: { kind: getMemberKindName(base.declarations[0]), id: node.id.sv }, + messageId: "node", + format: { + id: node.id.sv, + nodeName: symNode ? SyntaxKind[symNode.kind] : "Unknown node", + }, target: node, }), ); } - - return resolved; + return undefined; } function getMemberKindName(node: Node) { @@ -3541,37 +3253,14 @@ export function createChecker(program: Program): Checker { * (i.e. they contain symbols we don't know until we've instantiated the type and the type is an * instantiation) we late bind the container which creates the symbol that will hold its members. */ - function getAliasedSymbol( - aliasSymbol: Sym, - mapper: TypeMapper | undefined, - options: SymbolResolutionOptions, - ): Sym | undefined { - let current = aliasSymbol; - while (current.flags & SymbolFlags.Alias) { - const node = current.declarations[0]; - const targetNode = node.kind === SyntaxKind.AliasStatement ? node.value : node; - if ( - targetNode.kind === SyntaxKind.TypeReference || - targetNode.kind === SyntaxKind.MemberExpression || - targetNode.kind === SyntaxKind.Identifier - ) { - const sym = resolveTypeReferenceSymInternal(targetNode, mapper, options); - if (sym === undefined) { - return undefined; - } - current = sym; - } else { - return undefined; - } - } - const sym = current; - const node = aliasSymbol.declarations[0]; - - const resolvedTargetNode = sym.declarations[0]; - if (!options.checkTemplateTypes || !isTemplatedNode(resolvedTargetNode)) { - return sym; + function getAliasedSymbol(aliasSymbol: Sym, mapper: TypeMapper | undefined): Sym | undefined { + const node = getSymNode(aliasSymbol); + const links = resolver.getSymbolLinks(aliasSymbol); + if (!links.aliasResolutionIsTemplate) { + return links.aliasedSymbol ?? resolver.getNodeLinks(node).resolvedSymbol; } + // Otherwise for templates we need to get the type and retrieve the late bound symbol. const aliasType = getTypeForNode(node as AliasStatementNode, mapper); if (isErrorType(aliasType)) { return undefined; @@ -3759,25 +3448,14 @@ export function createChecker(program: Program): Checker { } function checkProgram() { - program.reportDuplicateSymbols(globalNamespaceNode.symbol.exports); - for (const file of program.sourceFiles.values()) { - bindAllMembers(file); - } - for (const file of program.sourceFiles.values()) { - bindMetaTypes(file); - } + checkDuplicateSymbols(); for (const file of program.sourceFiles.values()) { + checkDuplicateUsings(file); for (const ns of file.namespaces) { - const exports = mergedSymbols.get(ns.symbol)?.exports ?? ns.symbol.exports; - program.reportDuplicateSymbols(exports); initializeTypeForNamespace(ns); } } - for (const file of program.sourceFiles.values()) { - applyAugmentDecoratorsInScope(file); - } - for (const file of program.sourceFiles.values()) { checkSourceFile(file); } @@ -3785,29 +3463,59 @@ export function createChecker(program: Program): Checker { internalDecoratorValidation(); } - /** - * Post checking validation for internal decorators. - */ - function internalDecoratorValidation() { - validateInheritanceDiscriminatedUnions(program); + function checkDuplicateSymbols() { + program.reportDuplicateSymbols(resolver.symbols.global.exports); + for (const file of program.sourceFiles.values()) { + for (const ns of file.namespaces) { + const exports = getMergedSymbol(ns.symbol).exports ?? ns.symbol.exports; + program.reportDuplicateSymbols(exports); + } + } } - function applyAugmentDecoratorsInScope(scope: TypeSpecScriptNode | NamespaceStatementNode) { - applyAugmentDecorators(scope); - if (scope.statements === undefined) { - return; + /** Report error with duplicate using in the same scope. */ + function checkDuplicateUsings(file: TypeSpecScriptNode) { + const duplicateTrackers = new Map>(); + function getTracker(sym: Sym): DuplicateTracker { + const existing = duplicateTrackers.get(sym); + + if (existing) return existing; + + const newTacker = new DuplicateTracker(); + duplicateTrackers.set(sym, newTacker); + return newTacker; + } + + for (const using of file.usings) { + const ns = using.parent!; + const sym = getMergedSymbol(ns.symbol); + const tracker = getTracker(sym); + const targetSym = resolver.getNodeLinks(using.name).resolvedSymbol; + if (!targetSym) continue; + + tracker.track(targetSym, using); } - if (isArray(scope.statements)) { - for (const statement of scope.statements) { - if (statement.kind === SyntaxKind.NamespaceStatement) { - applyAugmentDecoratorsInScope(statement); + for (const tracker of duplicateTrackers.values()) { + for (const [_, nodes] of tracker.entries()) { + for (const node of nodes) { + program.reportDiagnostic( + createDiagnostic({ + code: "duplicate-using", + format: { usingName: typeReferenceToString(node.name) }, + target: node, + }), + ); } } - } else { - applyAugmentDecoratorsInScope(scope.statements); } } + /** + * Post checking validation for internal decorators. + */ + function internalDecoratorValidation() { + validateInheritanceDiscriminatedUnions(program); + } function checkSourceFile(file: TypeSpecScriptNode) { for (const statement of file.statements) { @@ -3863,7 +3571,7 @@ export function createChecker(program: Program): Checker { linkType(links, type, mapper); if (node.symbol.members) { - const members = getOrCreateAugmentedSymbolTable(node.symbol.members); + const members = resolver.getAugmentedSymbolTable(node.symbol.members); const propDocs = extractPropDocs(node); for (const [name, memberSym] of members) { const doc = propDocs.get(name); @@ -3936,6 +3644,8 @@ export function createChecker(program: Program): Checker { if (indexer) { type.indexer = indexer; } + lateBindMemberContainer(type); + lateBindMembers(type); return type; } @@ -3962,6 +3672,13 @@ export function createChecker(program: Program): Checker { } function checkModelExpression(node: ModelExpressionNode, mapper: TypeMapper | undefined) { + const links = getSymbolLinks(node.symbol); + + if (links.declaredType && mapper === undefined) { + // we're not instantiating this model and we've already checked it + return links.declaredType as any; + } + const properties = createRekeyableMap(); const type: Model = createType({ kind: "Model", @@ -3974,6 +3691,8 @@ export function createChecker(program: Program): Checker { derivedModels: [], sourceModels: [], }); + linkType(links, type, mapper); + linkMapper(type, mapper); checkModelProperties(node, properties, type, mapper); return finishType(type); @@ -4405,6 +4124,7 @@ export function createChecker(program: Program): Checker { mapper: TypeMapper | undefined, ): ScalarConstructor | Scalar | null { const target = checkTypeReference(node.target, mapper); + if (target.kind === "Scalar" || target.kind === "ScalarConstructor") { return target; } else { @@ -4709,225 +4429,13 @@ export function createChecker(program: Program): Checker { properties.set(newProp.name, newProp); } - function bindAllMembers(node: Node) { - const bound = new Set(); - if (node.symbol) { - bindMembers(node, node.symbol); - } - visitChildren(node, (child) => { - bindAllMembers(child); - }); - - function bindMembers(node: Node, containerSym: Sym) { - if (bound.has(containerSym)) { - return; - } - bound.add(containerSym); - let containerMembers: Mutable; - - switch (node.kind) { - case SyntaxKind.ModelStatement: - if (node.extends && node.extends.kind === SyntaxKind.TypeReference) { - resolveAndCopyMembers(node.extends); - } - if (node.is && node.is.kind === SyntaxKind.TypeReference) { - resolveAndCopyMembers(node.is); - } - for (const prop of node.properties) { - if (prop.kind === SyntaxKind.ModelSpreadProperty) { - resolveAndCopyMembers(prop.target); - } else { - const name = prop.id.sv; - bindMember(name, prop, SymbolFlags.ModelProperty); - } - } - break; - case SyntaxKind.ScalarStatement: - if (node.extends && node.extends.kind === SyntaxKind.TypeReference) { - resolveAndCopyMembers(node.extends); - } - for (const member of node.members) { - const name = member.id.sv; - bindMember(name, member, SymbolFlags.ScalarMember); - } - break; - case SyntaxKind.ModelExpression: - for (const prop of node.properties) { - if (prop.kind === SyntaxKind.ModelSpreadProperty) { - resolveAndCopyMembers(prop.target); - } else { - const name = prop.id.sv; - bindMember(name, prop, SymbolFlags.ModelProperty); - } - } - break; - case SyntaxKind.EnumStatement: - for (const member of node.members.values()) { - if (member.kind === SyntaxKind.EnumSpreadMember) { - resolveAndCopyMembers(member.target); - } else { - const name = member.id.sv; - bindMember(name, member, SymbolFlags.EnumMember); - } - } - break; - case SyntaxKind.InterfaceStatement: - for (const member of node.operations.values()) { - bindMember(member.id.sv, member, SymbolFlags.InterfaceMember | SymbolFlags.Operation); - } - if (node.extends) { - for (const ext of node.extends) { - resolveAndCopyMembers(ext); - } - } - break; - case SyntaxKind.UnionStatement: - for (const variant of node.options.values()) { - if (!variant.id) { - continue; - } - const name = variant.id.sv; - bindMember(name, variant, SymbolFlags.UnionVariant); - } - break; - } - - function resolveAndCopyMembers(node: TypeReferenceNode) { - let ref = resolveTypeReferenceSym(node, undefined); - if (ref && ref.flags & SymbolFlags.Alias) { - ref = resolveAliasedSymbol(ref); - } - if (ref && ref.members) { - bindMembers(ref.declarations[0], ref); - copyMembers(ref.members); - } - } - - function resolveAliasedSymbol(ref: Sym): Sym | undefined { - const node = ref.declarations[0] as AliasStatementNode; - switch (node.value.kind) { - case SyntaxKind.MemberExpression: - case SyntaxKind.TypeReference: - const resolvedSym = resolveTypeReferenceSym(node.value, undefined); - if (resolvedSym && resolvedSym.flags & SymbolFlags.Alias) { - return resolveAliasedSymbol(resolvedSym); - } - return resolvedSym; - default: - return undefined; - } - } - - function copyMembers(table: SymbolTable) { - const members = augmentedSymbolTables.get(table) ?? table; - for (const member of members.values()) { - bindMember(member.name, member.declarations[0], member.flags); - } - } - - function bindMember(name: string, node: Node, kind: SymbolFlags) { - const sym = createSymbol(node, name, kind, containerSym); - compilerAssert(containerSym.members, "containerSym.members is undefined"); - containerMembers ??= getOrCreateAugmentedSymbolTable(containerSym.members); - containerMembers.set(name, sym); - } - } - } - - function copyMembersToContainer(targetContainerSym: Sym, table: SymbolTable) { - const members = augmentedSymbolTables.get(table) ?? table; - compilerAssert(targetContainerSym.members, "containerSym.members is undefined"); - const containerMembers = getOrCreateAugmentedSymbolTable(targetContainerSym.members); - - for (const member of members.values()) { - bindMemberToContainer( - targetContainerSym, - containerMembers, - member.name, - member.declarations[0], - member.flags, - ); - } - } - - function bindMemberToContainer( - containerSym: Sym, - containerMembers: Mutable, - name: string, - node: Node, - kind: SymbolFlags, - ) { - const sym = createSymbol(node, name, kind, containerSym); - compilerAssert(containerSym.members, "containerSym.members is undefined"); - containerMembers.set(name, sym); - } - - function bindMetaTypes(node: Node) { - const visited = new Set(); - function visit(node: Node, symbol?: Sym) { - if (visited.has(node)) { - return; - } - visited.add(node); - switch (node.kind) { - case SyntaxKind.ModelProperty: { - const sym = getSymbolForMember(node); - if (sym) { - const table = getOrCreateAugmentedSymbolTable(sym.metatypeMembers!); - - table.set( - "type", - node.value.kind === SyntaxKind.TypeReference - ? createSymbol(node.value, "", SymbolFlags.Alias) - : node.value.symbol, - ); - } - break; - } - - case SyntaxKind.OperationStatement: { - const sym = symbol ?? node.symbol ?? getSymbolForMember(node); - const table = getOrCreateAugmentedSymbolTable(sym.metatypeMembers!); - if (node.signature.kind === SyntaxKind.OperationSignatureDeclaration) { - table.set("parameters", node.signature.parameters.symbol); - table.set("returnType", node.signature.returnType.symbol); - } else { - const sig = resolveTypeReferenceSym(node.signature.baseOperation, undefined, { - checkTemplateTypes: false, - }); - if (sig) { - visit(sig.declarations[0], sig); - const sigTable = getOrCreateAugmentedSymbolTable(sig.metatypeMembers!); - const sigParameterSym = sigTable.get("parameters")!; - if (sigParameterSym !== undefined) { - const parametersSym = createSymbol( - sigParameterSym.declarations[0], - "parameters", - SymbolFlags.Model & SymbolFlags.MemberContainer, - ); - copyMembersToContainer(parametersSym, sigParameterSym.members!); - table.set("parameters", parametersSym); - table.set("returnType", sigTable.get("returnType")!); - } - } - } - - break; - } - } - visitChildren(node, (child) => { - bindMetaTypes(child); - }); - } - visit(node); - } - /** * Initializes a late bound symbol for the type. This is generally necessary when attempting to * access a symbol for a type that is created during the check phase. */ function lateBindMemberContainer(type: Type) { if ((type as any).symbol) return; + switch (type.kind) { case "Model": type.symbol = createSymbol(type.node, type.name, SymbolFlags.Model | SymbolFlags.LateBound); @@ -4939,6 +4447,9 @@ export function createChecker(program: Program): Checker { type.name, SymbolFlags.Interface | SymbolFlags.LateBound, ); + if (isTemplateInstance(type) && type.name === "Foo") { + getSymbolLinks(type.symbol); + } mutate(type.symbol).type = type; break; case "Union": @@ -4948,33 +4459,41 @@ export function createChecker(program: Program): Checker { break; } } - function lateBindMembers(type: Type, containerSym: Sym) { - let containerMembers: Mutable | undefined; + function lateBindMembers(type: Interface | Model | Union | Enum | Scalar) { + compilerAssert(type.symbol, "Type must have a symbol to late bind members"); + const containerSym = type.symbol; + compilerAssert(containerSym.members, "Container symbol didn't have members at late-bind"); + const containerMembers: Mutable = resolver.getAugmentedSymbolTable( + containerSym.members, + ); switch (type.kind) { case "Model": for (const prop of walkPropertiesInherited(type)) { - lateBindMember(prop, SymbolFlags.ModelProperty); + lateBindMember(prop, SymbolFlags.Member | SymbolFlags.Declaration); } break; case "Scalar": for (const member of type.constructors.values()) { - lateBindMember(member, SymbolFlags.Member); + lateBindMember(member, SymbolFlags.Member | SymbolFlags.Declaration); } break; case "Enum": for (const member of type.members.values()) { - lateBindMember(member, SymbolFlags.EnumMember); + lateBindMember(member, SymbolFlags.Member | SymbolFlags.Declaration); } break; case "Interface": for (const member of type.operations.values()) { - lateBindMember(member, SymbolFlags.InterfaceMember | SymbolFlags.Operation); + lateBindMember( + member, + SymbolFlags.Member | SymbolFlags.Operation | SymbolFlags.Declaration, + ); } break; case "Union": for (const variant of type.variants.values()) { - lateBindMember(variant, SymbolFlags.UnionVariant); + lateBindMember(variant, SymbolFlags.Member | SymbolFlags.Declaration); } break; } @@ -4995,7 +4514,6 @@ export function createChecker(program: Program): Checker { ); mutate(sym).type = member; compilerAssert(containerSym.members, "containerSym.members is undefined"); - containerMembers ??= getOrCreateAugmentedSymbolTable(containerSym.members); containerMembers.set(member.name, sym); } } @@ -5023,29 +4541,23 @@ export function createChecker(program: Program): Checker { ); return undefined; } - const modelSymId = getNodeSymId(model); + const modelSymId = getNodeSym(model); pendingResolutions.start(modelSymId, ResolutionKind.BaseType); - const target = resolveTypeReferenceSym(heritageRef, mapper); - if (target === undefined) { - return undefined; - } - - if ( - pendingResolutions.has(getNodeSymId(target.declarations[0] as any), ResolutionKind.BaseType) - ) { + const target = resolver.getNodeLinks(heritageRef).resolvedSymbol; + if (target && pendingResolutions.has(target, ResolutionKind.BaseType)) { if (mapper === undefined) { reportCheckerDiagnostic( createDiagnostic({ code: "circular-base-type", - format: { typeName: (target.declarations[0] as any).id.sv }, + format: { typeName: target.name }, target: target, }), ); } return undefined; } - const heritageType = checkTypeReferenceSymbol(target, heritageRef, mapper); + const heritageType = getTypeForNode(heritageRef, mapper); pendingResolutions.finish(modelSymId, ResolutionKind.BaseType); if (isErrorType(heritageType)) { compilerAssert(program.hasError(), "Should already have reported an error.", heritageRef); @@ -5077,7 +4589,7 @@ export function createChecker(program: Program): Checker { ): Model | undefined { if (!isExpr) return undefined; - const modelSymId = getNodeSymId(model); + const modelSymId = getNodeSym(model); pendingResolutions.start(modelSymId, ResolutionKind.BaseType); let isType; if (isExpr.kind === SyntaxKind.ModelExpression) { @@ -5092,25 +4604,20 @@ export function createChecker(program: Program): Checker { } else if (isExpr.kind === SyntaxKind.ArrayExpression) { isType = checkArrayExpression(isExpr, mapper); } else if (isExpr.kind === SyntaxKind.TypeReference) { - const target = resolveTypeReferenceSym(isExpr, mapper); - if (target === undefined) { - return undefined; - } - if ( - pendingResolutions.has(getNodeSymId(target.declarations[0] as any), ResolutionKind.BaseType) - ) { + const target = resolver.getNodeLinks(isExpr).resolvedSymbol; + if (target && pendingResolutions.has(target, ResolutionKind.BaseType)) { if (mapper === undefined) { reportCheckerDiagnostic( createDiagnostic({ code: "circular-base-type", - format: { typeName: (target.declarations[0] as any).id.sv }, + format: { typeName: target.name }, target: target, }), ); } return undefined; } - isType = checkTypeReferenceSymbol(target, isExpr, mapper); + isType = getTypeForNode(isExpr, mapper); } else { reportCheckerDiagnostic(createDiagnostic({ code: "is-model", target: isExpr })); return undefined; @@ -5204,15 +4711,10 @@ export function createChecker(program: Program): Checker { if (containerNode.symbol === undefined) { return; } - compilerAssert( - containerNode.symbol.members, - `Expected container node ${SyntaxKind[containerNode.kind]} to have members.`, - ); - const memberSym = getOrCreateAugmentedSymbolTable(containerNode.symbol.members).get( - member.name, - ); + + const memberSym = getMemberSymbol(containerNode.symbol, member.name); if (memberSym) { - const links = getSymbolLinks(memberSym); + const links = resolver.getSymbolLinks(memberSym); linkMemberType(links, member, mapper); } } @@ -5222,7 +4724,6 @@ export function createChecker(program: Program): Checker { mapper: TypeMapper | undefined, ): ModelProperty { const sym = getSymbolForMember(prop)!; - const symId = getSymbolId(sym); const links = getSymbolLinksForMember(prop); if (links && links.declaredType && mapper === undefined) { @@ -5239,7 +4740,7 @@ export function createChecker(program: Program): Checker { decorators: [], }); - if (pendingResolutions.has(symId, ResolutionKind.Type) && mapper === undefined) { + if (pendingResolutions.has(sym, ResolutionKind.Type) && mapper === undefined) { reportCheckerDiagnostic( createDiagnostic({ code: "circular-prop", @@ -5249,7 +4750,7 @@ export function createChecker(program: Program): Checker { ); type.type = errorType; } else { - pendingResolutions.start(symId, ResolutionKind.Type); + pendingResolutions.start(sym, ResolutionKind.Type); type.type = getTypeForNode(prop.value, mapper); if (prop.default) { const defaultValue = checkDefaultValue(prop.default, type.type); @@ -5276,14 +4777,14 @@ export function createChecker(program: Program): Checker { finishType(type); } - pendingResolutions.finish(symId, ResolutionKind.Type); + pendingResolutions.finish(sym, ResolutionKind.Type); return type; } function createDocFromCommentDecorator(key: "self" | "returns" | "errors", doc: string) { return { - decorator: $docFromComment, + decorator: docFromCommentDecorator, args: [ { value: createLiteralType(key), jsValue: key }, { value: createLiteralType(doc), jsValue: doc }, @@ -5339,12 +4840,7 @@ export function createChecker(program: Program): Checker { ): DecoratorApplication | undefined { const sym = resolveTypeReferenceSym(decNode.target, undefined, true); if (!sym) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "unknown-decorator", - target: decNode, - }), - ); + // Error should already have been reported above return undefined; } if (!(sym.flags & SymbolFlags.Decorator)) { @@ -5374,7 +4870,7 @@ export function createChecker(program: Program): Checker { if (symbolLinks.declaredType) { compilerAssert( symbolLinks.declaredType.kind === "Decorator", - "Expected to find a decorator type.", + `Expected to find a decorator type but got ${symbolLinks.declaredType.kind}`, ); if (!checkDecoratorTarget(targetType, symbolLinks.declaredType, decNode)) { hasError = true; @@ -5637,8 +5133,12 @@ export function createChecker(program: Program): Checker { return valid; } - function checkAugmentDecorators(sym: Sym, targetType: Type, mapper: TypeMapper | undefined) { - const augmentDecoratorNodes = augmentDecoratorsForSym.get(sym) ?? []; + function checkAugmentDecorators( + sym: Sym, + targetType: Type, + mapper: TypeMapper | undefined, + ): DecoratorApplication[] { + const augmentDecoratorNodes = resolver.getAugmentDecoratorsForSym(sym); const decorators: DecoratorApplication[] = []; for (const decNode of augmentDecoratorNodes) { @@ -5649,15 +5149,52 @@ export function createChecker(program: Program): Checker { } return decorators; } + + /** + * Check that augment decorator are targeting valid symbols. + */ + function checkAugmentDecorator(node: AugmentDecoratorStatementNode) { + // This will validate the target type is pointing to a valid ref. + resolveTypeReferenceSym(node.targetType, undefined); + const links = resolver.getNodeLinks(node.targetType); + if (links.isTemplateInstantiation) { + program.reportDiagnostic( + createDiagnostic({ + code: "augment-decorator-target", + messageId: "noInstance", + target: node.targetType, + }), + ); + } + + // If this was used to get a type this is invalid, only used for validation. + return errorType; + } + + /** + * Check that using statements are targeting valid symbols. + */ + function checkUsings(node: UsingStatementNode) { + const usedSym = resolveTypeReferenceSym(node.name, undefined); + if (usedSym) { + if (~usedSym.flags & SymbolFlags.Namespace) { + reportCheckerDiagnostic(createDiagnostic({ code: "using-invalid-ref", target: node.name })); + } + } + // If this was used to get a type this is invalid, only used for validation. + return errorType; + } function checkDecorators( targetType: Type, node: Node & { decorators: readonly DecoratorExpressionNode[] }, mapper: TypeMapper | undefined, ) { - const sym = isMemberNode(node) ? (getSymbolForMember(node) ?? node.symbol) : node.symbol; + const sym = isMemberNode(node) + ? (getSymbolForMember(node) ?? node.symbol) + : getMergedSymbol(node.symbol); const decorators: DecoratorApplication[] = []; - const augmentDecoratorNodes = augmentDecoratorsForSym.get(sym) ?? []; + const augmentDecoratorNodes = resolver.getAugmentDecoratorsForSym(sym); const decoratorNodes = [ ...augmentDecoratorNodes, // the first decorator will be executed at last, so augmented decorator should be placed at first. ...node.decorators, @@ -5738,17 +5275,12 @@ export function createChecker(program: Program): Checker { extendsRef: TypeReferenceNode, mapper: TypeMapper | undefined, ): Scalar | undefined { - const symId = getNodeSymId(scalar); + const symId = getNodeSym(scalar); pendingResolutions.start(symId, ResolutionKind.BaseType); - const target = resolveTypeReferenceSym(extendsRef, mapper); - if (target === undefined) { - return undefined; - } + const target = resolver.getNodeLinks(extendsRef).resolvedSymbol; - if ( - pendingResolutions.has(getNodeSymId(target.declarations[0] as any), ResolutionKind.BaseType) - ) { + if (target && pendingResolutions.has(target, ResolutionKind.BaseType)) { if (mapper === undefined) { reportCheckerDiagnostic( createDiagnostic({ @@ -5760,7 +5292,7 @@ export function createChecker(program: Program): Checker { } return undefined; } - const extendsType = checkTypeReferenceSymbol(target, extendsRef, mapper); + const extendsType = getTypeForNode(extendsRef, mapper); pendingResolutions.finish(symId, ResolutionKind.BaseType); if (isErrorType(extendsType)) { compilerAssert(program.hasError(), "Should already have reported an error.", extendsRef); @@ -5851,7 +5383,7 @@ export function createChecker(program: Program): Checker { } checkTemplateDeclaration(node, mapper); - const aliasSymId = getNodeSymId(node); + const aliasSymId = getNodeSym(node); if (pendingResolutions.has(aliasSymId, ResolutionKind.Type)) { if (mapper === undefined) { reportCheckerDiagnostic( @@ -5891,8 +5423,7 @@ export function createChecker(program: Program): Checker { const type = node.type ? getTypeForNode(node.type, undefined) : undefined; - const symId = getSymbolId(node.symbol); - if (pendingResolutions.has(symId, ResolutionKind.Value)) { + if (pendingResolutions.has(node.symbol, ResolutionKind.Value)) { reportCheckerDiagnostic( createDiagnostic({ code: "circular-const", @@ -5903,9 +5434,9 @@ export function createChecker(program: Program): Checker { return null; } - pendingResolutions.start(symId, ResolutionKind.Value); + pendingResolutions.start(node.symbol, ResolutionKind.Value); const value = getValueForNode(node.value, undefined, type && { kind: "assignment", type }); - pendingResolutions.finish(symId, ResolutionKind.Value); + pendingResolutions.finish(node.symbol, ResolutionKind.Value); if (value === null || (type && !checkValueOfType(value, type, node.id))) { links.value = null; return links.value; @@ -6061,6 +5592,8 @@ export function createChecker(program: Program): Checker { interfaceType.namespace?.interfaces.set(interfaceType.name, interfaceType); } + lateBindMemberContainer(interfaceType); + lateBindMembers(interfaceType); return interfaceType; } @@ -6125,6 +5658,8 @@ export function createChecker(program: Program): Checker { unionType.namespace?.unions.set(unionType.name!, unionType); } + lateBindMemberContainer(unionType); + lateBindMembers(unionType); return unionType; } @@ -6185,16 +5720,11 @@ export function createChecker(program: Program): Checker { } function isMemberNode(node: Node): node is MemberNode { - return ( - node.kind === SyntaxKind.ModelProperty || - node.kind === SyntaxKind.EnumMember || - node.kind === SyntaxKind.OperationStatement || - node.kind === SyntaxKind.UnionVariant - ); + return node.symbol && !!(node.symbol.flags & SymbolFlags.Member); } function getMemberSymbol(parentSym: Sym, name: string): Sym | undefined { - return parentSym ? getOrCreateAugmentedSymbolTable(parentSym.members!).get(name) : undefined; + return parentSym ? resolver.getAugmentedSymbolTable(parentSym.members!).get(name) : undefined; } function getSymbolForMember(node: MemberNode): Sym | undefined { @@ -6203,12 +5733,12 @@ export function createChecker(program: Program): Checker { } const name = node.id.sv; const parentSym = node.parent?.symbol; - return parentSym ? getOrCreateAugmentedSymbolTable(parentSym.members!).get(name) : undefined; + return parentSym ? getMemberSymbol(parentSym, name) : undefined; } function getSymbolLinksForMember(node: MemberNode): SymbolLinks | undefined { const sym = getSymbolForMember(node); - return sym ? (sym.declarations[0] === node ? getSymbolLinks(sym) : undefined) : undefined; + return sym ? (getSymNode(sym) === node ? getSymbolLinks(sym) : undefined) : undefined; } function checkEnumMember( @@ -6373,123 +5903,18 @@ export function createChecker(program: Program): Checker { return createLiteralType(node.value, node); } - function mergeSymbolTable(source: SymbolTable, target: Mutable) { - for (const [sym, duplicates] of source.duplicates) { - const targetSet = target.duplicates.get(sym); - if (targetSet === undefined) { - mutate(target.duplicates).set(sym, new Set([...duplicates])); - } else { - for (const duplicate of duplicates) { - mutate(targetSet).add(duplicate); - } - } - } - - for (const [key, sourceBinding] of source) { - if (sourceBinding.flags & SymbolFlags.Namespace) { - let targetBinding = target.get(key); - if (!targetBinding) { - targetBinding = { - ...sourceBinding, - declarations: [], - exports: createSymbolTable(), - }; - target.set(key, targetBinding); - } - if (targetBinding.flags & SymbolFlags.Namespace) { - mergedSymbols.set(sourceBinding, targetBinding); - mutate(targetBinding.declarations).push(...sourceBinding.declarations); - mergeSymbolTable(sourceBinding.exports!, mutate(targetBinding.exports!)); - } else { - // this will set a duplicate error - target.set(key, sourceBinding); - } - } else if ( - sourceBinding.flags & SymbolFlags.Declaration || - sourceBinding.flags & SymbolFlags.Implementation - ) { - if (sourceBinding.flags & SymbolFlags.Decorator) { - mergeDeclarationOrImplementation(key, sourceBinding, target, SymbolFlags.Decorator); - } else if (sourceBinding.flags & SymbolFlags.Function) { - mergeDeclarationOrImplementation(key, sourceBinding, target, SymbolFlags.Function); - } else { - target.set(key, sourceBinding); - } - } else { - target.set(key, sourceBinding); - } - } - } - - function mergeDeclarationOrImplementation( - key: string, - sourceBinding: Sym, - target: Mutable, - expectTargetFlags: SymbolFlags, - ) { - const targetBinding = target.get(key); - if (!targetBinding || !(targetBinding.flags & expectTargetFlags)) { - target.set(key, sourceBinding); - return; - } - const isSourceDeclaration = sourceBinding.flags & SymbolFlags.Declaration; - const isSourceImplementation = sourceBinding.flags & SymbolFlags.Implementation; - const isTargetDeclaration = targetBinding.flags & SymbolFlags.Declaration; - const isTargetImplementation = targetBinding.flags & SymbolFlags.Implementation; - if (isTargetDeclaration && isTargetImplementation) { - // If the target already has both a declration and implementation set the symbol which will mark it as duplicate - target.set(key, sourceBinding); - } else if (isTargetDeclaration && isSourceImplementation) { - mergedSymbols.set(sourceBinding, targetBinding); - mutate(targetBinding).value = sourceBinding.value; - mutate(targetBinding).flags |= sourceBinding.flags; - mutate(targetBinding.declarations).push(...sourceBinding.declarations); - } else if (isTargetImplementation && isSourceDeclaration) { - mergedSymbols.set(sourceBinding, targetBinding); - mutate(targetBinding).flags |= sourceBinding.flags; - mutate(targetBinding.declarations).unshift(...sourceBinding.declarations); - } else { - // this will set a duplicate error - target.set(key, sourceBinding); - } - } - function getMergedSymbol(sym: Sym): Sym { - if (!sym) return sym; - return mergedSymbols.get(sym) || sym; - } - - function createGlobalNamespaceNode(): NamespaceStatementNode { - const nsId: IdentifierNode = { - kind: SyntaxKind.Identifier, - pos: 0, - end: 0, - sv: "global", - symbol: undefined!, - flags: NodeFlags.Synthetic, - }; - - const nsNode: NamespaceStatementNode = { - kind: SyntaxKind.NamespaceStatement, - decorators: [], - pos: 0, - end: 0, - id: nsId, - symbol: undefined!, - locals: createSymbolTable(), - flags: NodeFlags.Synthetic, - }; - - mutate(nsNode).symbol = createSymbol(nsNode, nsId.sv, SymbolFlags.Namespace); - mutate(nsNode.symbol.exports).set(nsId.sv, nsNode.symbol); - return nsNode; + // if (!sym) return sym; + // return mergedSymbols.get(sym) || sym; + return resolver.getMergedSymbol(sym); } function createGlobalNamespaceType(): Namespace { - const type = createAndFinishType({ + const sym = resolver.symbols.global; + const type: Namespace = createType({ kind: "Namespace", name: "", - node: globalNamespaceNode, + node: getGlobalNamespaceNode(), models: new Map(), scalars: new Map(), operations: new Map(), @@ -6501,8 +5926,9 @@ export function createChecker(program: Program): Checker { functionDeclarations: new Map(), decorators: [], }); - getSymbolLinks(globalNamespaceNode.symbol).type = type; - return type; + getSymbolLinks(sym).type = type; + type.decorators = checkAugmentDecorators(sym, type, undefined); + return finishType(type); } function initializeClone(type: T, additionalProps: Partial): T { @@ -7268,7 +6694,7 @@ export function createChecker(program: Program): Checker { } // next, resolve outside - const ref = resolveTypeReferenceSym(node, undefined); + const { finalSymbol: ref } = resolver.resolveTypeReference(node); if (!ref) throw new ProjectionError("Unknown identifier " + node.sv); if (ref.flags & SymbolFlags.Decorator) { @@ -7367,20 +6793,6 @@ export function createChecker(program: Program): Checker { ); } - function memberExpressionToString(expr: IdentifierNode | MemberExpressionNode) { - let current = expr; - const parts = []; - - while (current.kind === SyntaxKind.MemberExpression) { - parts.push(current.id.sv); - current = current.base; - } - - parts.push(current.sv); - - return parts.reverse().join("."); - } - /** * Check if the source type can be assigned to the target type and emit diagnostics * @param source Type of a value @@ -7466,10 +6878,6 @@ export function createChecker(program: Program): Checker { } } -function isAnonymous(type: Type) { - return !("name" in type) || typeof type.name !== "string" || !type.name; -} - /** * Find all named models that could have been the source of the given * property. This includes the named parents of all property sources in a @@ -7939,9 +7347,9 @@ enum ResolutionKind { } class PendingResolutions { - #data = new Map>(); + #data = new Map>(); - start(symId: number, kind: ResolutionKind) { + start(symId: Sym, kind: ResolutionKind) { let existing = this.#data.get(symId); if (existing === undefined) { existing = new Set(); @@ -7950,11 +7358,11 @@ class PendingResolutions { existing.add(kind); } - has(symId: number, kind: ResolutionKind): boolean { + has(symId: Sym, kind: ResolutionKind): boolean { return this.#data.get(symId)?.has(kind) ?? false; } - finish(symId: number, kind: ResolutionKind) { + finish(symId: Sym, kind: ResolutionKind) { const existing = this.#data.get(symId); if (existing === undefined) { return; @@ -7999,3 +7407,16 @@ function unsafe_projectionArgumentMarshalForJS(arg: Type): any { } return arg as any; } + +function printTypeReferenceNode( + node: TypeReferenceNode | IdentifierNode | MemberExpressionNode, +): string { + switch (node.kind) { + case SyntaxKind.MemberExpression: + return `${printTypeReferenceNode(node.base)}.${printTypeReferenceNode(node.id)}`; + case SyntaxKind.TypeReference: + return printTypeReferenceNode(node.target); + case SyntaxKind.Identifier: + return node.sv; + } +} diff --git a/packages/compiler/src/core/diagnostics.ts b/packages/compiler/src/core/diagnostics.ts index 66ca2dfba7..f234a97512 100644 --- a/packages/compiler/src/core/diagnostics.ts +++ b/packages/compiler/src/core/diagnostics.ts @@ -104,7 +104,7 @@ export function getSourceLocation( if (!("kind" in target) && !("entityKind" in target)) { // TemplateInstanceTarget - if ("node" in target) { + if (!("declarations" in target)) { return getSourceLocationOfNode(target.node, options); } diff --git a/packages/compiler/src/core/helpers/operation-utils.ts b/packages/compiler/src/core/helpers/operation-utils.ts index 6a3d6e3e71..e938297deb 100644 --- a/packages/compiler/src/core/helpers/operation-utils.ts +++ b/packages/compiler/src/core/helpers/operation-utils.ts @@ -33,7 +33,14 @@ export function listOperationsIn( } } - if (current.kind === "Namespace") { + if ( + current.kind === "Namespace" && + !( + current.name === "Prototypes" && + current.namespace?.name === "TypeSpec" && + current.namespace.namespace?.name === "" + ) + ) { const recursive = options.recursive ?? true; const children = [ diff --git a/packages/compiler/src/core/helpers/syntax-utils.ts b/packages/compiler/src/core/helpers/syntax-utils.ts index ad8ac560ee..3d11f8319c 100644 --- a/packages/compiler/src/core/helpers/syntax-utils.ts +++ b/packages/compiler/src/core/helpers/syntax-utils.ts @@ -1,5 +1,6 @@ import { isIdentifierContinue, isIdentifierStart, utf16CodeUnits } from "../charcode.js"; import { Keywords } from "../scanner.js"; +import { IdentifierNode, MemberExpressionNode, SyntaxKind, TypeReferenceNode } from "../types.js"; /** * Print a string as a TypeSpec identifier. If the string is a valid identifier, return it as is otherwise wrap it into backticks. @@ -43,3 +44,16 @@ function needBacktick(sv: string) { } while (pos < sv.length && isIdentifierContinue((cp = sv.codePointAt(pos)!))); return pos < sv.length; } + +export function typeReferenceToString( + node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, +): string { + switch (node.kind) { + case SyntaxKind.MemberExpression: + return `${typeReferenceToString(node.base)}${node.selector}${typeReferenceToString(node.id)}`; + case SyntaxKind.TypeReference: + return typeReferenceToString(node.target); + case SyntaxKind.Identifier: + return node.sv; + } +} diff --git a/packages/compiler/src/core/inspector/node.ts b/packages/compiler/src/core/inspector/node.ts new file mode 100644 index 0000000000..af7feb718d --- /dev/null +++ b/packages/compiler/src/core/inspector/node.ts @@ -0,0 +1,38 @@ +import { relative } from "path/posix"; +import pc from "picocolors"; +import { getSourceLocation } from "../diagnostics.js"; +import { typeReferenceToString } from "../helpers/syntax-utils.js"; +import { SyntaxKind, type Node } from "../types.js"; + +/** @internal */ +export function inspectNode(node: Node): string { + const loc = getSourceLocation(node); + const pos = loc.file.getLineAndCharacterOfPosition(loc.pos); + const kind = pc.yellow(`[${SyntaxKind[node.kind]}]`); + const locString = pc.cyan( + `${relative(process.cwd(), loc.file.path)}:${pos.line + 1}:${pos.character + 1}`, + ); + return `${kind} ${printNodeInfoInternal(node)} ${locString}`; +} + +function printNodeInfoInternal(node: Node): string { + switch (node.kind) { + case SyntaxKind.MemberExpression: + case SyntaxKind.TypeReference: + case SyntaxKind.Identifier: + return typeReferenceToString(node); + case SyntaxKind.DecoratorExpression: + return `@${printNodeInfoInternal(node.target)}`; + case SyntaxKind.JsNamespaceDeclaration: + case SyntaxKind.NamespaceStatement: + case SyntaxKind.ModelStatement: + case SyntaxKind.OperationStatement: + case SyntaxKind.EnumStatement: + case SyntaxKind.AliasStatement: + case SyntaxKind.ConstStatement: + case SyntaxKind.UnionStatement: + return node.id.sv; + default: + return ""; + } +} diff --git a/packages/compiler/src/core/inspector/symbol.ts b/packages/compiler/src/core/inspector/symbol.ts new file mode 100644 index 0000000000..3934b79a1a --- /dev/null +++ b/packages/compiler/src/core/inspector/symbol.ts @@ -0,0 +1,105 @@ +import pc from "picocolors"; +import { Sym, SymbolFlags, SymbolLinks, SyntaxKind } from "../types.js"; + +/** + * @internal + */ +export function inspectSymbol(sym: Sym, links: SymbolLinks = {}) { + let output = ` +${pc.blue(pc.inverse(` sym `))} ${pc.white(sym.name)} +${pc.dim("flags")} ${inspectSymbolFlags(sym.flags)} + `.trim(); + + if (sym.declarations && sym.declarations.length > 0) { + const decls = sym.declarations.map((d) => SyntaxKind[d.kind]).join("\n"); + output += `\n${pc.dim("declarations")} ${decls}`; + } + + if (sym.exports) { + output += `\n${pc.dim("exports")} ${[...sym.exports.keys()].join(", ")}`; + } + + if (sym.id) { + output += `\n${pc.dim("id")} ${sym.id}`; + } + + if (sym.members) { + output += `\n${pc.dim("members")} ${[...sym.members.keys()].join(", ")}`; + } + + if (sym.metatypeMembers) { + output += `\n${pc.dim("metatypeMembers")} ${[...sym.metatypeMembers.keys()].join(", ")}`; + } + + if (sym.parent) { + output += `\n${pc.dim("parent")} ${sym.parent.name}`; + } + + if (sym.symbolSource) { + output += `\n${pc.dim("symbolSource")} ${sym.symbolSource.name}`; + } + + if (sym.type) { + output += `\n${pc.dim("type")} ${ + "name" in sym.type && sym.type.name ? String(sym.type.name) : sym.type.kind + }`; + } + + if (sym.value) { + output += `\n${pc.dim("value")} present`; + } + + if (Object.keys(links).length > 0) { + output += `\nlinks\n`; + + if (links.declaredType) { + output += `\n${pc.dim("declaredType")} ${ + "name" in links.declaredType && links.declaredType.name + ? String(links.declaredType.name) + : links.declaredType.kind + }`; + } + + if (links.instantiations) { + output += `\n${pc.dim("instantiations")} initialized`; + } + + if (links.type) { + output += `\n${pc.dim("type")} ${ + "name" in links.type && links.type.name ? String(links.type.name) : links.type.kind + }`; + } + } + + return output; +} + +const flagsNames = [ + [SymbolFlags.Model, "Model"], + [SymbolFlags.Scalar, "Scalar"], + [SymbolFlags.Operation, "Operation"], + [SymbolFlags.Enum, "Enum"], + [SymbolFlags.Interface, "Interface"], + [SymbolFlags.Union, "Union"], + [SymbolFlags.Alias, "Alias"], + [SymbolFlags.Namespace, "Namespace"], + [SymbolFlags.Projection, "Projection"], + [SymbolFlags.Decorator, "Decorator"], + [SymbolFlags.TemplateParameter, "TemplateParameter"], + [SymbolFlags.ProjectionParameter, "ProjectionParameter"], + [SymbolFlags.Function, "Function"], + [SymbolFlags.FunctionParameter, "FunctionParameter"], + [SymbolFlags.Using, "Using"], + [SymbolFlags.DuplicateUsing, "DuplicateUsing"], + [SymbolFlags.SourceFile, "SourceFile"], + [SymbolFlags.Member, "Member"], + [SymbolFlags.Const, "Const"], +] as const; + +export function inspectSymbolFlags(flags: SymbolFlags) { + const names: string[] = []; + for (const [flag, name] of flagsNames) { + if (flags & flag) names.push(name); + } + return names.join(", "); +} diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index d0033888ca..acfb3fd424 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -321,18 +321,6 @@ const diagnostics = { default: paramMessage`Intersection contains duplicate property definitions for ${"propName"}`, }, }, - "unknown-identifier": { - severity: "error", - messages: { - default: paramMessage`Unknown identifier ${"id"}`, - }, - }, - "unknown-decorator": { - severity: "error", - messages: { - default: "Unknown decorator", - }, - }, "invalid-decorator": { severity: "error", messages: { @@ -343,9 +331,11 @@ const diagnostics = { severity: "error", messages: { default: paramMessage`Cannot resolve ${"id"}`, + identifier: paramMessage`Unknown identifier ${"id"}`, + decorator: paramMessage`Unknown decorator @${"id"}`, inDecorator: paramMessage`Cannot resolve ${"id"} in decorator`, underNamespace: paramMessage`Namespace ${"namespace"} doesn't have member ${"id"}`, - underContainer: paramMessage`${"kind"} doesn't have member ${"id"}`, + member: paramMessage`${"kind"} doesn't have member ${"id"}`, metaProperty: paramMessage`${"kind"} doesn't have meta property ${"id"}`, node: paramMessage`Cannot resolve '${"id"}' in node ${"nodeName"} since it has no members. Did you mean to use "::" instead of "."?`, }, diff --git a/packages/compiler/src/core/name-resolver.ts b/packages/compiler/src/core/name-resolver.ts new file mode 100644 index 0000000000..3641bdb78b --- /dev/null +++ b/packages/compiler/src/core/name-resolver.ts @@ -0,0 +1,1291 @@ +/** + * The name resolver is responsible for resolving identifiers to symbols and + * creating symbols for types that become known during this process. After name + * resolution, we can do some limited analysis of the reference graph in order + * to support e.g. augment decorators. + * + * Name resolution does not alter any AST nodes or attached symbols in order to + * ensure AST nodes and attached symbols can be trivially reused between + * compilations. Instead, symbols created here are either stored in augmented + * symbol tables or as merged symbols. Any metadata about symbols and nodes are + * stored in symbol links and node links respectively. The resolver provides + * APIs for managing this metadata which is useful during later phases. + * + * While we resolve some identifiers to symbols during this phase, we often + * cannot say for sure that an identifier does not exist. Some symbols must be + * late-bound because the symbol does not become known until after the program + * has been checked. A common example is members of a model template which often + * cannot be known until the template is instantiated. Instead, we mark that the + * reference is unknown and will resolve the symbol (or report an error if it + * doesn't exist) in later phases. These unknown references cannot be used as + * the target of an augment decorator. + * + * There are some errors we can detect because we have complete symbol + * information, but we do not report them from here. For example, because we + * know all namespace bindings and all the declarations inside of them, we could + * in principle report an error when we attempt to `using` something that isn't + * a namespace. However, giving a good error message sometimes requires knowing + * what type was mistakenly referenced, so we merely mark that resolution has + * failed and move on. Even in cases where we could give a good error we chose + * not to in order to uniformly handle error reporting in the checker. + * + * Name resolution has three sub-phases: + * + * 1. Merge namespace symbols and decorator implementation/declaration symbols + * 2. Resolve using references to namespaces and create namespace-local bindings + * for used symbols + * 3. Resolve type references and bind members + * + * The reference resolution and member binding phase implements a deferred + * resolution strategy. Often we cannot resolve a reference without binding + * members, but we often cannot bind members without resolving references. In + * such situations, we stop resolving or binding the current reference or type + * and attempt to resolve or bind the reference or type it depends on. Once we + * have done so, we return to the original reference or type and complete our + * work. + * + * This is accomplished by doing a depth-first traversal of the reference graph. + * On the way down, we discover any dependencies that need to be resolved or + * bound for the current node, and recurse into the AST nodes, so that on the + * way back up, all of our dependencies are bound and resolved and we can + * complete. So while we start with a depth-first traversal of the ASTs in order + * to discover work to do, most of the actual work is done while following the + * reference graph, binding and resolving along the way. Circular references are + * discovered during the reference graph walk and marked as such. Symbol and + * node links are used to ensure we never resolve the same reference twice. The + * checker implements a very similar algorithm to evaluate the types of the + * program. + **/ + +import { Mutable, mutate } from "../utils/misc.js"; +import { createSymbol, createSymbolTable, getSymNode } from "./binder.js"; +import { compilerAssert } from "./diagnostics.js"; +import { visitChildren } from "./parser.js"; +import { Program } from "./program.js"; +import { + AliasStatementNode, + AugmentDecoratorStatementNode, + DecoratorExpressionNode, + EnumStatementNode, + Expression, + IdentifierNode, + InterfaceStatementNode, + IntersectionExpressionNode, + MemberExpressionNode, + ModelExpressionNode, + ModelPropertyNode, + ModelStatementNode, + NamespaceStatementNode, + Node, + NodeFlags, + NodeLinks, + OperationStatementNode, + ProjectionDecoratorReferenceExpressionNode, + ProjectionStatementNode, + ResolutionResult, + ResolutionResultFlags, + ScalarStatementNode, + Sym, + SymbolFlags, + SymbolLinks, + SymbolTable, + SyntaxKind, + TemplateParameterDeclarationNode, + TypeReferenceNode, + TypeSpecScriptNode, + UnionStatementNode, +} from "./types.js"; + +export interface NameResolver { + /** + * Resolve all static symbol links in the program. + */ + resolveProgram(): void; + + /** + * Get the merged symbol or itself if not merged. + * This is the case for Namespace which have multiple nodes and symbol but all reference the same merged one. + */ + getMergedSymbol(sym: Sym): Sym; + + /** + * Get augmented symbol table. + */ + getAugmentedSymbolTable(table: SymbolTable): Mutable; + /** + * Get node links for the given syntax node. + * This returns links to which symbol the node reference if applicable(TypeReference, Identifier nodes) + */ + getNodeLinks(node: Node): NodeLinks; + + /** Get symbol links for the given symbol */ + getSymbolLinks(symbol: Sym): SymbolLinks; + + /** Return augment decorator nodes that are bound to this symbol */ + getAugmentDecoratorsForSym(symbol: Sym): AugmentDecoratorStatementNode[]; + + /** + * Resolve the member expression using the given symbol as base. + * This can be used to follow the name resolution for template instance which are not statically linked. + */ + resolveMemberExpressionForSym( + sym: Sym, + node: MemberExpressionNode, + options?: ResolveTypReferenceOptions, + ): ResolutionResult; + + /** Get the meta member by name */ + resolveMetaMemberByName(sym: Sym, name: string): ResolutionResult; + + /** Resolve the given type reference. This should only need to be called on dynamically created nodes that want to resolve which symbol they reference */ + resolveTypeReference( + node: TypeReferenceNode | IdentifierNode | MemberExpressionNode, + ): ResolutionResult; + + /** Built-in symbols. */ + readonly symbols: { + /** Symbol for the global namespace */ + readonly global: Sym; + + /** Symbol for the null type */ + readonly null: Sym; + }; +} + +interface ResolveTypReferenceOptions { + resolveDecorators?: boolean; +} + +// This needs to be global to be sure to not reallocate per program. +let currentNodeId = 0; +let currentSymbolId = 0; + +export function createResolver(program: Program): NameResolver { + const mergedSymbols = new Map(); + const augmentedSymbolTables = new Map(); + const nodeLinks = new Map(); + const symbolLinks = new Map(); + + const globalNamespaceNode = createGlobalNamespaceNode(); + const globalNamespaceSym = createSymbol( + globalNamespaceNode, + "global", + SymbolFlags.Namespace | SymbolFlags.Declaration, + ); + mutate(globalNamespaceNode).symbol = globalNamespaceSym; + mutate(globalNamespaceSym.exports).set(globalNamespaceNode.id.sv, globalNamespaceSym); + + const metaTypePrototypes = createMetaTypePrototypes(); + + const nullSym = createSymbol(undefined, "null", SymbolFlags.None); + const augmentDecoratorsForSym = new Map(); + + return { + symbols: { global: globalNamespaceSym, null: nullSym }, + resolveProgram() { + // Merge namespace symbols and decorator implementation/declaration symbols + for (const file of program.jsSourceFiles.values()) { + mergeSymbolTable(file.symbol.exports!, mutate(globalNamespaceSym.exports!)); + } + + for (const file of program.sourceFiles.values()) { + mergeSymbolTable(file.symbol.exports!, mutate(globalNamespaceSym.exports!)); + } + + const typespecNamespaceBinding = globalNamespaceSym.exports!.get("TypeSpec"); + if (typespecNamespaceBinding) { + mutate(typespecNamespaceBinding!.exports).set("null", nullSym); + for (const file of program.sourceFiles.values()) { + addUsingSymbols(typespecNamespaceBinding.exports!, file.locals); + } + } + + // Bind usings to namespaces, create namespace-local bindings for used symbols + for (const file of program.sourceFiles.values()) { + setUsingsForFile(file); + } + + // Begin reference graph walk starting at each node to ensure we visit all possible + // references and types that need binding. + for (const file of program.sourceFiles.values()) { + bindAndResolveNode(file); + } + }, + + getMergedSymbol, + getAugmentedSymbolTable, + getNodeLinks, + getSymbolLinks, + + resolveMemberExpressionForSym, + resolveMetaMemberByName, + resolveTypeReference, + + getAugmentDecoratorsForSym, + }; + + function getAugmentDecoratorsForSym(sym: Sym) { + return augmentDecoratorsForSym.get(sym) ?? []; + } + + function getMergedSymbol(sym: Sym) { + if (!sym) return sym; + return mergedSymbols.get(sym) || sym; + } + + /** + * @internal + */ + function getNodeLinks(n: Node): NodeLinks { + const id = getNodeId(n); + + if (nodeLinks.has(id)) { + return nodeLinks.get(id)!; + } + + const links = {}; + nodeLinks.set(id, links); + + return links; + } + + function getNodeId(n: Node) { + if (n._id === undefined) { + mutate(n)._id = currentNodeId++; + } + return n._id!; + } + + /** + * @internal + */ + function getSymbolLinks(s: Sym): SymbolLinks { + const id = getSymbolId(s); + + if (symbolLinks.has(id)) { + return symbolLinks.get(id)!; + } + + const links = {}; + symbolLinks.set(id, links); + + return links; + } + + function getSymbolId(s: Sym) { + if (s.id === undefined) { + mutate(s).id = currentSymbolId++; + } + return s.id!; + } + + function resolveTypeReference( + node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, + options: ResolveTypReferenceOptions = {}, + ): ResolutionResult { + const links = getNodeLinks(node); + if (links.resolutionResult) { + return links as any; + } + + let result = resolveTypeReferenceWorker(node, options); + + const resolvedSym = result.resolvedSymbol; + Object.assign(links, result); + + if (resolvedSym && resolvedSym.flags & SymbolFlags.Alias) { + // unwrap aliases + const aliasNode = resolvedSym.declarations[0] as AliasStatementNode; + const aliasResult = resolveAlias(aliasNode); + // For alias if the alias itself is a template declaration then its not actually instantiating the reference + const isTemplateInstantiation = + aliasResult.isTemplateInstantiation && aliasNode.templateParameters.length === 0; + if (isTemplateInstantiation) { + links.isTemplateInstantiation = true; + } + if (aliasResult.finalSymbol) { + links.finalSymbol = aliasResult.finalSymbol; + } + result = { + ...aliasResult, + finalSymbol: links.finalSymbol, + isTemplateInstantiation: result.isTemplateInstantiation || isTemplateInstantiation, + }; + } else if (resolvedSym && resolvedSym.flags & SymbolFlags.TemplateParameter) { + // references to template parameters with constraints can reference the + // constraint type members + const templateNode = resolvedSym.declarations[0] as TemplateParameterDeclarationNode; + if (templateNode.constraint) { + result = resolveTemplateParameter(templateNode); + } + } + + // make sure we've bound and fully resolved the referenced + // node before returning it. + if (resolvedSym) { + if ( + resolvedSym.flags & SymbolFlags.Declaration && + ~resolvedSym.flags & SymbolFlags.Namespace + ) { + bindAndResolveNode(resolvedSym.declarations[0]); + } + } + + return result; + } + + function resolveTypeReferenceWorker( + node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, + options: ResolveTypReferenceOptions, + ): ResolutionResult { + if (node.kind === SyntaxKind.TypeReference) { + const result = resolveTypeReference(node.target, options); + return node.arguments.length > 0 ? { ...result, isTemplateInstantiation: true } : result; + } else if (node.kind === SyntaxKind.MemberExpression) { + return resolveMemberExpression(node, options); + } else if (node.kind === SyntaxKind.Identifier) { + return resolveIdentifier(node, options); + } + + compilerAssert(false, "Unexpected node kind"); + } + + function resolveMemberExpression( + node: MemberExpressionNode, + options: ResolveTypReferenceOptions, + ): ResolutionResult { + const baseResult = resolveTypeReference(node.base, { + ...options, + resolveDecorators: false, // When resolving the base it can never be a decorator + }); + + if (baseResult.resolutionResult & ResolutionResultFlags.ResolutionFailed) { + return baseResult; + } + const baseSym = baseResult.finalSymbol; + + compilerAssert(baseSym, "Base symbol must be defined if resolution did not fail"); + const memberResult = resolveMemberExpressionForSym(baseSym, node, options); + + const idNodeLinks = getNodeLinks(node.id); + idNodeLinks.resolvedSymbol = memberResult.resolvedSymbol; + idNodeLinks.resolutionResult = memberResult.resolutionResult; + const isTemplateInstantiation = + baseResult.isTemplateInstantiation || memberResult.isTemplateInstantiation; + idNodeLinks.isTemplateInstantiation = isTemplateInstantiation; + return { + ...memberResult, + isTemplateInstantiation, + }; + } + + function resolveMemberExpressionForSym( + baseSym: Sym, + node: MemberExpressionNode, + options: ResolveTypReferenceOptions = {}, + ): ResolutionResult { + if (node.selector === ".") { + if (baseSym.flags & SymbolFlags.MemberContainer) { + return resolveMember(baseSym, node.id); + } else if (baseSym.flags & SymbolFlags.ExportContainer) { + const res = resolveExport(getMergedSymbol(baseSym), node.id, options); + return res; + } + } else { + return resolveMetaMember(baseSym, node.id); + } + + return failedResult(ResolutionResultFlags.NotFound); + } + + function resolveMember(baseSym: Sym, id: IdentifierNode): ResolutionResult { + const baseNode = baseSym.node ?? baseSym.declarations[0]; + compilerAssert(baseNode, "Base symbol must have an associated node"); + + bindMemberContainer(baseNode); + + switch (baseNode.kind) { + case SyntaxKind.ModelStatement: + case SyntaxKind.ModelExpression: + case SyntaxKind.IntersectionExpression: + return resolveModelMember(baseSym, baseNode, id); + case SyntaxKind.InterfaceStatement: + return resolveInterfaceMember(baseSym, id); + case SyntaxKind.EnumStatement: + return resolveEnumMember(baseSym, id); + case SyntaxKind.UnionStatement: + return resolveUnionVariant(baseSym, id); + case SyntaxKind.ScalarStatement: + return resolveScalarConstructor(baseSym, id); + } + + compilerAssert(false, "Unknown member container kind: " + SyntaxKind[baseNode.kind]); + } + + function resolvedResult(resolvedSymbol: Sym): ResolutionResult { + return { + resolvedSymbol, + finalSymbol: resolvedSymbol, + resolutionResult: ResolutionResultFlags.Resolved, + }; + } + function failedResult(resolutionResult: ResolutionResultFlags): ResolutionResult { + return { + resolvedSymbol: undefined, + finalSymbol: undefined, + resolutionResult, + }; + } + + function ambiguousResult(symbols: Sym[]): ResolutionResult { + return { + resolutionResult: ResolutionResultFlags.Ambiguous, + resolvedSymbol: undefined, + finalSymbol: undefined, + ambiguousSymbols: symbols, + }; + } + + function resolveModelMember( + modelSym: Sym, + modelNode: ModelStatementNode | ModelExpressionNode | IntersectionExpressionNode, + id: IdentifierNode, + ): ResolutionResult { + // step 1: check direct members + // spreads have already been bound + const memberSym = tableLookup(modelSym.members!, id); + if (memberSym) { + return resolvedResult(memberSym); + } + + const modelSymLinks = getSymbolLinks(modelSym); + + // step 2: check extends. Don't look up to extends references if we have + // unknown members, and resolve any property as unknown if we extend + // something unknown. + const extendsRef = modelNode.kind === SyntaxKind.ModelStatement ? modelNode.extends : undefined; + if ( + extendsRef && + extendsRef.kind === SyntaxKind.TypeReference && + !modelSymLinks.hasUnknownMembers + ) { + const { finalSymbol: extendsSym, resolutionResult: extendsResult } = + resolveTypeReference(extendsRef); + if (extendsResult & ResolutionResultFlags.Resolved) { + return resolveMember(extendsSym!, id); + } + + if (extendsResult & ResolutionResultFlags.Unknown) { + modelSymLinks.hasUnknownMembers = true; + return failedResult(ResolutionResultFlags.Unknown); + } + } + + // step 3: return either unknown or not found depending on whether we have + // unknown members + return failedResult( + modelSymLinks.hasUnknownMembers + ? ResolutionResultFlags.Unknown + : ResolutionResultFlags.NotFound, + ); + } + + function resolveInterfaceMember(ifaceSym: Sym, id: IdentifierNode): ResolutionResult { + const slinks = getSymbolLinks(ifaceSym); + const memberSym = tableLookup(ifaceSym.members!, id); + if (memberSym) { + return resolvedResult(memberSym); + } + + return failedResult( + slinks.hasUnknownMembers ? ResolutionResultFlags.Unknown : ResolutionResultFlags.NotFound, + ); + } + + function resolveEnumMember(enumSym: Sym, id: IdentifierNode): ResolutionResult { + const memberSym = tableLookup(enumSym.members!, id); + if (memberSym) { + return resolvedResult(memberSym); + } + + return failedResult(ResolutionResultFlags.NotFound); + } + + function resolveUnionVariant(unionSym: Sym, id: IdentifierNode): ResolutionResult { + const memberSym = tableLookup(unionSym.members!, id); + if (memberSym) { + return resolvedResult(memberSym); + } + return failedResult(ResolutionResultFlags.NotFound); + } + + function resolveScalarConstructor(scalarSym: Sym, id: IdentifierNode): ResolutionResult { + const memberSym = tableLookup(scalarSym.members!, id); + if (memberSym) { + return resolvedResult(memberSym); + } + + return failedResult(ResolutionResultFlags.NotFound); + } + + function resolveExport( + baseSym: Sym, + id: IdentifierNode, + options: ResolveTypReferenceOptions, + ): ResolutionResult { + const node = baseSym.declarations[0]; + compilerAssert( + node.kind === SyntaxKind.NamespaceStatement || + node.kind === SyntaxKind.TypeSpecScript || + node.kind === SyntaxKind.JsNamespaceDeclaration, + `Unexpected node kind ${SyntaxKind[node.kind]}`, + ); + const exportSym = tableLookup(baseSym.exports!, id, options.resolveDecorators); + if (!exportSym) { + return failedResult(ResolutionResultFlags.NotFound); + } + return resolvedResult(exportSym); + } + + function resolveAlias(node: AliasStatementNode): ResolutionResult { + const symbol = node.symbol; + const slinks = getSymbolLinks(symbol); + + if (slinks.aliasResolutionResult) { + return { + resolutionResult: slinks.aliasResolutionResult, + resolvedSymbol: slinks.aliasedSymbol, + finalSymbol: slinks.aliasedSymbol, + isTemplateInstantiation: slinks.aliasResolutionIsTemplate, + }; + } + + if (node.value.kind === SyntaxKind.TypeReference) { + const result = resolveTypeReference(node.value); + if (result.finalSymbol && result.finalSymbol.flags & SymbolFlags.Alias) { + const aliasLinks = getSymbolLinks(result.finalSymbol); + slinks.aliasedSymbol = aliasLinks.aliasedSymbol + ? aliasLinks.aliasedSymbol + : result.finalSymbol; + } else { + slinks.aliasedSymbol = result.finalSymbol; + } + slinks.aliasResolutionResult = result.resolutionResult; + slinks.aliasResolutionIsTemplate = result.isTemplateInstantiation; + return { + resolvedSymbol: result.resolvedSymbol, + finalSymbol: slinks.aliasedSymbol, + resolutionResult: slinks.aliasResolutionResult, + isTemplateInstantiation: result.isTemplateInstantiation, + }; + } else if (node.value.symbol) { + // a type literal + slinks.aliasedSymbol = node.value.symbol; + slinks.aliasResolutionResult = ResolutionResultFlags.Resolved; + return resolvedResult(node.value.symbol); + } else { + // a computed type + slinks.aliasResolutionResult = ResolutionResultFlags.Unknown; + return failedResult(ResolutionResultFlags.Unknown); + } + } + + function resolveTemplateParameter(node: TemplateParameterDeclarationNode): ResolutionResult { + const symbol = node.symbol; + const slinks = getSymbolLinks(symbol); + + if (!node.constraint) { + return resolvedResult(node.symbol); + } + + if (slinks.constraintResolutionResult) { + return { + finalSymbol: slinks.constraintSymbol, + resolvedSymbol: slinks.constraintSymbol, + resolutionResult: slinks.constraintResolutionResult, + }; + } + + if (node.constraint && node.constraint.kind === SyntaxKind.TypeReference) { + const result = resolveTypeReference(node.constraint); + slinks.constraintSymbol = result.finalSymbol; + slinks.constraintResolutionResult = result.resolutionResult; + return result; + } else if (node.constraint.symbol) { + // a type literal + slinks.constraintSymbol = node.constraint.symbol; + slinks.constraintResolutionResult = ResolutionResultFlags.Resolved; + return resolvedResult(node.constraint.symbol); + } else { + // a computed type, just resolve to the template parameter symbol itself. + slinks.constraintSymbol = node.symbol; + slinks.constraintResolutionResult = ResolutionResultFlags.Resolved; + return resolvedResult(node.symbol); + } + } + function resolveExpression(node: Expression): ResolutionResult { + if (node.kind === SyntaxKind.TypeReference) { + return resolveTypeReference(node); + } + + if (node.symbol) { + return resolvedResult(node.symbol); + } + + return failedResult(ResolutionResultFlags.Unknown); + } + + function resolveMetaMember(baseSym: Sym, id: IdentifierNode): ResolutionResult { + return resolveMetaMemberByName(baseSym, id.sv); + } + + function resolveMetaMemberByName(baseSym: Sym, sv: string): ResolutionResult { + const baseNode = getSymNode(baseSym); + + const prototype = metaTypePrototypes.get(baseNode.kind); + + if (!prototype) { + return failedResult(ResolutionResultFlags.NotFound); + } + + const getter = prototype.get(sv); + + if (!getter) { + return failedResult(ResolutionResultFlags.NotFound); + } + + return getter(baseSym); + } + + function tableLookup(table: SymbolTable, node: IdentifierNode, resolveDecorator = false) { + table = augmentedSymbolTables.get(table) ?? table; + let sym; + if (resolveDecorator) { + sym = table.get("@" + node.sv); + } else { + sym = table.get(node.sv); + } + + if (!sym) return sym; + + return getMergedSymbol(sym); + } + + /** + * This method will take a member container and compute all the known member + * symbols. It will determine whether it has unknown members and set the + * symbol link value appropriately. This is used during resolution to know if + * member resolution should return `unknown` when a member isn't found. + */ + function bindMemberContainer(node: Node) { + const sym = node.symbol!; + const symLinks = getSymbolLinks(sym); + + if (symLinks.membersBound) { + return; + } + + symLinks.membersBound = true; + + switch (node.kind) { + case SyntaxKind.ModelStatement: + case SyntaxKind.ModelExpression: + bindModelMembers(node); + return; + case SyntaxKind.IntersectionExpression: + bindIntersectionMembers(node); + return; + case SyntaxKind.InterfaceStatement: + bindInterfaceMembers(node); + return; + case SyntaxKind.EnumStatement: + bindEnumMembers(node); + return; + case SyntaxKind.UnionStatement: + bindUnionMembers(node); + return; + case SyntaxKind.ScalarStatement: + bindScalarMembers(node); + return; + } + } + + // TODO: had to keep the metaTypeMembers which this pr originally tried to get rid as we need for ops parameters to be cloned and have a new reference + function bindOperationStatementParameters(node: OperationStatementNode) { + const targetTable = getAugmentedSymbolTable(node.symbol!.metatypeMembers!); + if (node.signature.kind === SyntaxKind.OperationSignatureDeclaration) { + const { finalSymbol: sym } = resolveExpression(node.signature.parameters); + if (sym) { + targetTable.set("parameters", sym); + } + } else { + const { finalSymbol: sig } = resolveTypeReference(node.signature.baseOperation); + if (sig) { + const sigTable = getAugmentedSymbolTable(sig.metatypeMembers!); + const sigParameterSym = sigTable.get("parameters")!; + if (sigParameterSym !== undefined) { + const parametersSym = createSymbol( + sigParameterSym.node, + "parameters", + SymbolFlags.Model & SymbolFlags.MemberContainer, + ); + getAugmentedSymbolTable(parametersSym.members!).include( + getAugmentedSymbolTable(sigParameterSym.members!), + parametersSym, + ); + targetTable.set("parameters", parametersSym); + targetTable.set("returnType", sigTable.get("returnType")!); + } + } + } + } + + function bindDeclarationIdentifier(node: Node & { id: IdentifierNode }) { + if (node.kind === SyntaxKind.TypeSpecScript || node.kind === SyntaxKind.JsSourceFile) return; + const links = getNodeLinks(node.id); + let sym; + if (node.symbol === undefined) { + return; + } + if (node.symbol.flags & SymbolFlags.Member) { + compilerAssert(node.parent, "Node should have a parent"); + const parentSym = getMergedSymbol(node.parent.symbol); + const table = parentSym.exports ?? getAugmentedSymbolTable(parentSym.members!); + sym = table.get(node.id.sv); + } else { + sym = node.symbol; + } + compilerAssert(sym, "Should have a symbol"); + links.resolvedSymbol = sym; + links.resolutionResult = ResolutionResultFlags.Resolved; + } + function bindModelMembers(node: ModelStatementNode | ModelExpressionNode) { + const modelSym = node.symbol!; + + const modelSymLinks = getSymbolLinks(modelSym); + + const targetTable = getAugmentedSymbolTable(modelSym.members!); + + const isRef = node.kind === SyntaxKind.ModelStatement ? node.is : undefined; + if (isRef && isRef.kind === SyntaxKind.TypeReference) { + const { finalSymbol: isSym, resolutionResult: isResult } = resolveTypeReference(isRef); + + setUnknownMembers(modelSymLinks, isSym, isResult); + + if (isResult & ResolutionResultFlags.Resolved && isSym!.flags & SymbolFlags.Model) { + const sourceTable = getAugmentedSymbolTable(isSym!.members!); + targetTable.include(sourceTable, modelSym); + } + } + + // here we just need to check if we're extending something with unknown symbols + const extendsRef = node.kind === SyntaxKind.ModelStatement ? node.extends : undefined; + if (extendsRef && extendsRef.kind === SyntaxKind.TypeReference) { + const { finalSymbol: sym, resolutionResult: result } = resolveTypeReference(extendsRef); + setUnknownMembers(modelSymLinks, sym, result); + } + + // here we just need to include spread properties, since regular properties + // were bound by the binder. + for (const propertyNode of node.properties) { + if (propertyNode.kind !== SyntaxKind.ModelSpreadProperty) { + continue; + } + + const { finalSymbol: sourceSym, resolutionResult: sourceResult } = resolveTypeReference( + propertyNode.target, + ); + + setUnknownMembers(modelSymLinks, sourceSym, sourceResult); + + if (~sourceResult & ResolutionResultFlags.Resolved) { + continue; + } + compilerAssert(sourceSym, "Spread symbol must be defined if resolution succeeded"); + + if (~sourceSym.flags & SymbolFlags.Model) { + // will be a checker error + continue; + } + + const sourceTable = getAugmentedSymbolTable(sourceSym.members!); + targetTable.include(sourceTable, modelSym); + } + } + + function bindIntersectionMembers(node: IntersectionExpressionNode) { + const intersectionSym = node.symbol!; + const intersectionSymLinks = getSymbolLinks(intersectionSym); + + const targetTable = getAugmentedSymbolTable(intersectionSym.members!); + + // here we just need to include spread properties, since regular properties + // were bound by the binder. + for (const expr of node.options) { + const { finalSymbol: sourceSym, resolutionResult: sourceResult } = resolveExpression(expr); + + setUnknownMembers(intersectionSymLinks, sourceSym, sourceResult); + + if (~sourceResult & ResolutionResultFlags.Resolved) { + continue; + } + compilerAssert(sourceSym, "Spread symbol must be defined if resolution succeeded"); + + if (~sourceSym.flags & SymbolFlags.Model) { + // will be a checker error + continue; + } + + const sourceTable = getAugmentedSymbolTable(sourceSym.members!); + targetTable.include(sourceTable, intersectionSym); + } + } + + function setUnknownMembers( + targetSymLinks: SymbolLinks, + sym: Sym | undefined, + result: ResolutionResultFlags, + ) { + if (result & ResolutionResultFlags.Unknown) { + targetSymLinks.hasUnknownMembers = true; + } else if (result & ResolutionResultFlags.Resolved) { + const isSymLinks = getSymbolLinks(sym!); + if (isSymLinks.hasUnknownMembers) { + targetSymLinks.hasUnknownMembers = true; + } + } + } + + function bindInterfaceMembers(node: InterfaceStatementNode) { + const ifaceSym = node.symbol!; + const ifaceSymLinks = getSymbolLinks(ifaceSym); + for (const extendsRef of node.extends) { + const { finalSymbol: extendsSym, resolutionResult: extendsResult } = + resolveTypeReference(extendsRef); + setUnknownMembers(ifaceSymLinks, extendsSym, extendsResult); + + if (~extendsResult & ResolutionResultFlags.Resolved) { + continue; + } + + compilerAssert(extendsSym, "Extends symbol must be defined if resolution succeeded"); + + if (~extendsSym.flags & SymbolFlags.Interface) { + // will be a checker error + continue; + } + + const sourceTable = getAugmentedSymbolTable(extendsSym.members!); + const targetTable = getAugmentedSymbolTable(ifaceSym.members!); + targetTable.include(sourceTable, ifaceSym); + } + } + + function bindEnumMembers(node: EnumStatementNode) { + const enumSym = node.symbol!; + const enumSymLinks = getSymbolLinks(enumSym); + const targetTable = getAugmentedSymbolTable(enumSym.members!); + + for (const memberNode of node.members) { + if (memberNode.kind !== SyntaxKind.EnumSpreadMember) { + continue; + } + + const { finalSymbol: sourceSym, resolutionResult: sourceResult } = resolveTypeReference( + memberNode.target, + ); + + setUnknownMembers(enumSymLinks, sourceSym, sourceResult); + + if (~sourceResult & ResolutionResultFlags.Resolved) { + continue; + } + + compilerAssert(sourceSym, "Spread symbol must be defined if resolution succeeded"); + + if (~sourceSym.flags & SymbolFlags.Enum) { + // will be a checker error + continue; + } + + const sourceTable = getAugmentedSymbolTable(sourceSym.members!); + targetTable.include(sourceTable, enumSym); + } + } + + function bindUnionMembers(node: UnionStatementNode) { + // Everything is already bound in binder.ts + } + function bindScalarMembers(node: ScalarStatementNode) { + const scalarSym = node.symbol!; + const targetTable = getAugmentedSymbolTable(scalarSym.members!); + const scalarSymLinks = getSymbolLinks(scalarSym); + + if (node.extends) { + const { finalSymbol: extendsSym, resolutionResult: extendsResult } = resolveTypeReference( + node.extends, + ); + setUnknownMembers(scalarSymLinks, extendsSym, extendsResult); + + if (~extendsResult & ResolutionResultFlags.Resolved) { + return; + } + compilerAssert(extendsSym, "Scalar extends symbol must be defined if resolution succeeded"); + + const sourceTable = getAugmentedSymbolTable(extendsSym.members!); + targetTable.include(sourceTable, scalarSym); + } + } + + function bindTemplateParameter(node: TemplateParameterDeclarationNode) { + const sym = node.symbol; + const links = getSymbolLinks(sym); + links.hasUnknownMembers = true; + } + + function resolveIdentifier( + node: IdentifierNode, + options: ResolveTypReferenceOptions, + ): ResolutionResult { + let scope: Node | undefined = node.parent; + let binding: Sym | undefined; + + while (scope && scope.kind !== SyntaxKind.TypeSpecScript) { + if (scope.symbol && scope.symbol.flags & SymbolFlags.ExportContainer) { + const mergedSymbol = getMergedSymbol(scope.symbol); + binding = tableLookup(mergedSymbol.exports!, node, options.resolveDecorators); + if (binding) return resolvedResult(binding); + } + + if ("locals" in scope && scope.locals !== undefined) { + binding = tableLookup(scope.locals, node, options.resolveDecorators); + if (binding) { + return resolvedResult(binding); + } + } + + scope = scope.parent; + } + + 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!, + node, + options.resolveDecorators, + ); + + // 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) ?? + []), + ]); + } + return resolvedResult(usingBinding.symbolSource!); + } + } + + return failedResult(ResolutionResultFlags.Unknown); + } + + /** + * We cannot inject symbols into the symbol tables hanging off syntax tree nodes as + * syntax tree nodes can be shared by other programs. This is called as a copy-on-write + * to inject using and late-bound symbols, and then we use the copy when resolving + * in the table. + */ + function getAugmentedSymbolTable(table: SymbolTable): Mutable { + let augmented = augmentedSymbolTables.get(table); + if (!augmented) { + augmented = createSymbolTable(table); + augmentedSymbolTables.set(table, augmented); + } + return mutate(augmented); + } + + function mergeSymbolTable(source: SymbolTable, target: Mutable) { + for (const [sym, duplicates] of source.duplicates) { + const targetSet = target.duplicates.get(sym); + if (targetSet === undefined) { + mutate(target.duplicates).set(sym, new Set([...duplicates])); + } else { + for (const duplicate of duplicates) { + mutate(targetSet).add(duplicate); + } + } + } + + for (const [key, sourceBinding] of source) { + if (sourceBinding.flags & SymbolFlags.Namespace) { + let targetBinding = target.get(key); + if (!targetBinding) { + targetBinding = { + ...sourceBinding, + declarations: [], + exports: createSymbolTable(), + }; + target.set(key, targetBinding); + } + if (targetBinding.flags & SymbolFlags.Namespace) { + mergedSymbols.set(sourceBinding, targetBinding); + mutate(targetBinding.declarations).push(...sourceBinding.declarations); + + mergeSymbolTable(sourceBinding.exports!, mutate(targetBinding.exports!)); + } else { + // this will set a duplicate error + target.set(key, sourceBinding); + } + } else if (sourceBinding.flags & SymbolFlags.Decorator) { + mergeDeclarationOrImplementation(key, sourceBinding, target, SymbolFlags.Decorator); + } else if (sourceBinding.flags & SymbolFlags.Function) { + mergeDeclarationOrImplementation(key, sourceBinding, target, SymbolFlags.Function); + } else { + target.set(key, sourceBinding); + } + } + } + + function mergeDeclarationOrImplementation( + key: string, + sourceBinding: Sym, + target: Mutable, + expectTargetFlags: SymbolFlags, + ) { + const targetBinding = target.get(key); + if (!targetBinding || !(targetBinding.flags & expectTargetFlags)) { + target.set(key, sourceBinding); + return; + } + const isSourceImplementation = sourceBinding.flags & SymbolFlags.Implementation; + const isTargetImplementation = targetBinding.flags & SymbolFlags.Implementation; + if (!isTargetImplementation && isSourceImplementation) { + mergedSymbols.set(sourceBinding, targetBinding); + mutate(targetBinding).value = sourceBinding.value; + mutate(targetBinding).flags |= sourceBinding.flags; + mutate(targetBinding.declarations).push(...sourceBinding.declarations); + } else if (isTargetImplementation && !isSourceImplementation) { + mergedSymbols.set(sourceBinding, targetBinding); + mutate(targetBinding).flags |= sourceBinding.flags; + mutate(targetBinding.declarations).unshift(...sourceBinding.declarations); + } else { + // this will set a duplicate error + target.set(key, sourceBinding); + } + } + + function setUsingsForFile(file: TypeSpecScriptNode) { + const usedUsing = new Map>(); + function isAlreadyAddedIn(sym: Sym, target: Sym) { + let current: Sym | undefined = sym; + while (current) { + if (usedUsing.get(sym)?.has(target)) { + return true; + } + current = current.parent; + } + + let usingForScope = usedUsing.get(sym); + if (usingForScope === undefined) { + usingForScope = new Set(); + usedUsing.set(sym, usingForScope); + } + + usingForScope.add(target); + return false; + } + for (const using of file.usings) { + const parentNs = using.parent!; + const { finalSymbol: usedSym, resolutionResult: usedSymResult } = resolveTypeReference( + using.name, + ); + if (~usedSymResult & ResolutionResultFlags.Resolved) { + continue; // Keep going and count on checker to report those errors. + } + + compilerAssert(usedSym, "Used symbol must be defined if resolution succeeded"); + if (~usedSym.flags & SymbolFlags.Namespace) { + continue; // Keep going and count on checker to report those errors. + } + + const namespaceSym = getMergedSymbol(usedSym)!; + + if (isAlreadyAddedIn(getMergedSymbol(parentNs.symbol), namespaceSym)) { + continue; + } + addUsingSymbols(namespaceSym.exports!, parentNs.locals!); + } + } + + function addUsingSymbols(source: SymbolTable, destination: SymbolTable): void { + const augmented = getAugmentedSymbolTable(destination); + for (const symbolSource of source.values()) { + const sym: Sym = { + flags: SymbolFlags.Using, + declarations: [], + name: symbolSource.name, + symbolSource: symbolSource, + node: undefined as any, + }; + + augmented.set(sym.name, sym); + } + } + + function createGlobalNamespaceNode() { + const nsId: IdentifierNode = { + kind: SyntaxKind.Identifier, + pos: 0, + end: 0, + sv: "global", + symbol: undefined!, + flags: NodeFlags.Synthetic, + }; + + const nsNode: NamespaceStatementNode = { + kind: SyntaxKind.NamespaceStatement, + decorators: [], + pos: 0, + end: 0, + id: nsId, + symbol: undefined!, + locals: createSymbolTable(), + flags: NodeFlags.Synthetic, + }; + + return nsNode; + } + + function bindAndResolveNode(node: Node) { + switch (node.kind) { + case SyntaxKind.TypeReference: + resolveTypeReference(node); + break; + case SyntaxKind.ModelStatement: + case SyntaxKind.ModelExpression: + case SyntaxKind.InterfaceStatement: + case SyntaxKind.EnumStatement: + case SyntaxKind.ScalarStatement: + case SyntaxKind.UnionStatement: + case SyntaxKind.IntersectionExpression: + bindMemberContainer(node); + break; + case SyntaxKind.OperationStatement: + bindOperationStatementParameters(node); + break; + case SyntaxKind.AliasStatement: + resolveAlias(node); + break; + case SyntaxKind.TemplateParameterDeclaration: + bindTemplateParameter(node); + break; + case SyntaxKind.DecoratorExpression: + case SyntaxKind.ProjectionDecoratorReferenceExpression: + resolveDecoratorTarget(node); + break; + case SyntaxKind.AugmentDecoratorStatement: + resolveAugmentDecorator(node); + break; + case SyntaxKind.CallExpression: + resolveTypeReference(node.target); + break; + case SyntaxKind.ProjectionStatement: + resolveProjection(node); + break; + } + + if ("id" in node && node.kind !== SyntaxKind.MemberExpression && node.id) { + bindDeclarationIdentifier(node as any); + } + + visitChildren(node, bindAndResolveNode); + } + + function resolveProjection(projection: ProjectionStatementNode) { + switch (projection.selector.kind) { + case SyntaxKind.Identifier: + case SyntaxKind.MemberExpression: + resolveTypeReference(projection.selector); + } + } + + function resolveDecoratorTarget( + decorator: + | DecoratorExpressionNode + | AugmentDecoratorStatementNode + | ProjectionDecoratorReferenceExpressionNode, + ) { + resolveTypeReference(decorator.target, { resolveDecorators: true }); + } + + type SymbolGetter = (baseSym: Sym) => ResolutionResult; + type TypePrototype = Map; + type TypePrototypes = Map; + + function createMetaTypePrototypes(): TypePrototypes { + const nodeInterfaces: TypePrototypes = new Map(); + + // model properties + const modelPropertyPrototype: TypePrototype = new Map(); + modelPropertyPrototype.set("type", (baseSym) => { + const node = baseSym.node as ModelPropertyNode; + return resolveExpression(node.value); + }); + nodeInterfaces.set(SyntaxKind.ModelProperty, modelPropertyPrototype); + + // operations + const operationPrototype: TypePrototype = new Map(); + // For parameters it is a cloned symbol as all the params are spread + operationPrototype.set("parameters", (baseSym) => { + const sym = getAugmentedSymbolTable(baseSym.metatypeMembers!)?.get("parameters"); + return sym === undefined + ? failedResult(ResolutionResultFlags.ResolutionFailed) + : resolvedResult(sym); + }); + // For returnType we just return the reference so we can just do it dynamically + operationPrototype.set("returnType", (baseSym) => { + let node = baseSym.declarations[0] as OperationStatementNode; + while (node.signature.kind === SyntaxKind.OperationSignatureReference) { + const baseResult = resolveTypeReference(node.signature.baseOperation); + if (baseResult.resolutionResult & ResolutionResultFlags.Resolved) { + node = baseSym!.declarations[0] as OperationStatementNode; + } else { + return baseResult; + } + } + + return resolveExpression(node.signature.returnType); + }); + nodeInterfaces.set(SyntaxKind.OperationStatement, operationPrototype); + + return nodeInterfaces; + } + + function resolveAugmentDecorator(decNode: AugmentDecoratorStatementNode) { + resolveTypeReference(decNode.target, { resolveDecorators: true }); + const targetResult = resolveTypeReference(decNode.targetType); + if (targetResult.resolvedSymbol && !targetResult.isTemplateInstantiation) { + let list = augmentDecoratorsForSym.get(targetResult.resolvedSymbol); + if (list === undefined) { + list = []; + augmentDecoratorsForSym.set(targetResult.resolvedSymbol, list); + } + list.unshift(decNode); + } + } +} diff --git a/packages/compiler/src/core/program.ts b/packages/compiler/src/core/program.ts index aa4f925285..0370fe6eda 100644 --- a/packages/compiler/src/core/program.ts +++ b/packages/compiler/src/core/program.ts @@ -23,6 +23,7 @@ import { createLinter, resolveLinterDefinition } from "./linter.js"; import { createLogger } from "./logger/index.js"; import { createTracer } from "./logger/tracer.js"; import { createDiagnostic } from "./messages.js"; +import { createResolver } from "./name-resolver.js"; import { CompilerOptions } from "./options.js"; import { parse, parseStandaloneTypeReference } from "./parser.js"; import { getDirectoryPath, joinPaths, resolvePath } from "./path-utils.js"; @@ -229,7 +230,10 @@ export async function compile( if (options.linterRuleSet) { program.reportDiagnostics(await linter.extendRuleSet(options.linterRuleSet)); } - program.checker = createChecker(program); + + const resolver = createResolver(program); + resolver.resolveProgram(); + program.checker = createChecker(program, resolver); program.checker.checkProgram(); if (!continueToNextStage) { @@ -825,7 +829,7 @@ export async function compile( function getNode(target: Node | Entity | Sym | TemplateInstanceTarget): Node | undefined { if (!("kind" in target) && !("valueKind" in target) && !("entityKind" in target)) { // TemplateInstanceTarget - if ("node" in target) { + if (!("declarations" in target)) { return target.node; } // symbol @@ -875,8 +879,8 @@ export async function compile( } const binder = createBinder(program); binder.bindNode(node); - mutate(node).parent = program.checker.getGlobalNamespaceNode(); - + mutate(node).parent = resolver.symbols.global.declarations[0]; + resolver.resolveTypeReference(node); return program.checker.resolveTypeReference(node); } } diff --git a/packages/compiler/src/core/semantic-walker.ts b/packages/compiler/src/core/semantic-walker.ts index a9e689138b..693313c692 100644 --- a/packages/compiler/src/core/semantic-walker.ts +++ b/packages/compiler/src/core/semantic-walker.ts @@ -176,7 +176,9 @@ function navigateNamespaceType(namespace: Namespace, context: NavigationContext) } for (const subNamespace of namespace.namespaces.values()) { - navigateNamespaceType(subNamespace, context); + if (!(namespace.name === "TypeSpec" && subNamespace.name === "Prototypes")) { + navigateNamespaceType(subNamespace, context); + } } for (const union of namespace.unions.values()) { diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 0ed1bfc1a9..ec31817968 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -260,10 +260,28 @@ export type IntrinsicScalarName = | "boolean" | "url"; -export type NeverIndexer = { key: NeverType; value: undefined }; +/** + * Valid keys when looking up meta members for a particular type. + * Array is a special case because it doesn't have a unique type, but does + * carry unique meta-members. + */ +export type MetaMemberKey = Type["kind"] | "Array"; + +/** + * A table to ease lookup of meta member interfaces during identifier resolution. + * Only `type` exists today, but `value` will be added in the future. + */ +export interface MetaMembersTable { + type: Partial>; +} +export type NeverIndexer = { + readonly key: NeverType; + readonly value: undefined; +}; + export type ModelIndexer = { - key: Scalar; - value: Type; + readonly key: Scalar; + readonly value: Type; }; export interface ArrayModelType extends Model { @@ -515,6 +533,12 @@ export interface Enum extends BaseType, DecoratedType { * obtained via `...` are inserted where the spread appears in source. */ members: RekeyableMap; + + /** + * Late-bound symbol of this enum type. + * @internal + */ + symbol?: Sym; } export interface EnumMember extends BaseType, DecoratedType { @@ -752,10 +776,15 @@ export interface Sym { readonly flags: SymbolFlags; /** - * Nodes which contribute to this declaration + * Nodes which contribute to this declaration, present if SymbolFlags.Declaration is set. */ readonly declarations: readonly Node[]; + /** + * Node which resulted in this symbol, present if SymbolFlags.Declaration is not set. + */ + readonly node: Node; + /** * The name of the symbol */ @@ -816,6 +845,100 @@ export interface SymbolLinks { /** For const statements the value of the const */ value?: Value | null; + + /** + * When a symbol contains unknown members, symbol lookup during + * name resolution should always return unknown if it can't definitely + * find a member. + */ + hasUnknownMembers?: boolean; + + /** + * True if we have completed the early binding of member symbols for this model during + * the name resolution phase. + */ + membersBound?: boolean; + + /** + * The symbol aliased by an alias symbol. When present, guaranteed to be a + * non-alias symbol. Will not be present when the name resolver could not + * determine a symbol for the alias, e.g. when it is a computed type. + */ + aliasedSymbol?: Sym; + + /** + * The result of resolving the aliased reference. When resolved, aliasedSymbol + * will contain the resolved symbol. Otherwise, aliasedSymbol may be present + * if the alias is a type literal with a symbol, otherwise it will be + * undefined. + */ + aliasResolutionResult?: ResolutionResultFlags; + + // TODO: any better idea? + aliasResolutionIsTemplate?: boolean; + + /** + * The symbol for the constraint of a type parameter. Will not be present when + * the name resolver could not determine a symbol for the constraint, e.g. + * when it is a computed type. + */ + constraintSymbol?: Sym; + + /** + * The result of resolving the type parameter constraint. When resolved, + * constraintSymbol will contain the resolved symbol. Otherwise, + * constraintSymbol may be present if the constraint is a type literal with a + * symbol, otherwise it will be undefined. + */ + constraintResolutionResult?: ResolutionResultFlags; +} + +export interface ResolutionResult { + resolutionResult: ResolutionResultFlags; + isTemplateInstantiation?: boolean; + resolvedSymbol: Sym | undefined; + finalSymbol: Sym | undefined; + ambiguousSymbols?: Sym[]; +} + +export interface NodeLinks { + /** the result of type checking this node */ + resolvedType?: Type; + + /**The syntax symbol resolved by this node. */ + resolvedSymbol?: Sym; + + /** If the resolvedSymbol is an alias point to the symbol the alias reference(recursively), otherwise is the same as resolvedSymbol */ + finalSymbol?: Sym | undefined; + + /** + * If the link involve template argument. + * Note that this only catch if template arguments are used. If referencing the default instance(e.g Foo for Foo) this will not be set to true. + * This is by design as the symbol reference has different meaning depending on the context: + * - For augment decorator it would reference the template declaration + * - For type references it would reference the default instance. + */ + isTemplateInstantiation?: boolean; + + /** + * The result of resolution of this reference node. + * + * When the the result is `Resolved`, `resolvedSymbol` contains the result. + **/ + resolutionResult?: ResolutionResultFlags; + + /** If the resolution result is Ambiguous list of symbols that are */ + ambiguousSymbols?: Sym[]; +} + +export enum ResolutionResultFlags { + None = 0, + Resolved = 1 << 1, + Unknown = 1 << 2, + Ambiguous = 1 << 3, + NotFound = 1 << 4, + + ResolutionFailed = Unknown | Ambiguous | NotFound, } /** @@ -828,47 +951,62 @@ export interface SymbolTable extends ReadonlyMap { readonly duplicates: ReadonlyMap>; } +export interface MutableSymbolTable extends SymbolTable { + set(key: string, value: Sym): void; + + /** + * Put the symbols in the source table into this table. + * @param source table to copy + * @param parentSym Parent symbol that the source symbol should update to. + */ + include(source: SymbolTable, parentSym?: Sym): void; +} + // prettier-ignore export const enum SymbolFlags { None = 0, Model = 1 << 1, - ModelProperty = 1 << 2, - Scalar = 1 << 3, - Operation = 1 << 4, - Enum = 1 << 5, - EnumMember = 1 << 6, - Interface = 1 << 7, - InterfaceMember = 1 << 8, - Union = 1 << 9, - UnionVariant = 1 << 10, - Alias = 1 << 11, - Namespace = 1 << 12, - Projection = 1 << 13, - Decorator = 1 << 14, - TemplateParameter = 1 << 15, - ProjectionParameter = 1 << 16, - Function = 1 << 17, - FunctionParameter = 1 << 18, - Using = 1 << 19, - DuplicateUsing = 1 << 20, - SourceFile = 1 << 21, - Declaration = 1 << 22, - Implementation = 1 << 23, - Const = 1 << 24, - ScalarMember = 1 << 25, + Scalar = 1 << 2, + Operation = 1 << 3, + Enum = 1 << 4, + Interface = 1 << 5, + Union = 1 << 6, + Alias = 1 << 7, + Namespace = 1 << 8, + Projection = 1 << 9, + Decorator = 1 << 10, + TemplateParameter = 1 << 11, + ProjectionParameter = 1 << 12, + Function = 1 << 13, + FunctionParameter = 1 << 14, + Using = 1 << 15, + DuplicateUsing = 1 << 16, + SourceFile = 1 << 17, + Member = 1 << 18, + Const = 1 << 19, + + + /** + * A symbol which represents a declaration. Such symbols will have at least + * one entry in the `declarations[]` array referring to a node with an `id`. + * + * Symbols which do not represent declarations + */ + Declaration = 1 << 20, + + Implementation = 1 << 21, /** * A symbol which was late-bound, in which case, the type referred to * by this symbol is stored directly in the symbol. */ - LateBound = 1 << 26, + LateBound = 1 << 22, ExportContainer = Namespace | SourceFile, /** * Symbols whose members will be late bound (and stored on the type) */ MemberContainer = Model | Enum | Union | Interface | Scalar, - Member = ModelProperty | EnumMember | UnionVariant | InterfaceMember | ScalarMember, } /** @@ -1046,6 +1184,8 @@ export interface BaseNode extends TextRange { * you will likely only access symbol in cases where you know the node has a symbol. */ readonly symbol: Sym; + /** Unique id across the process used to look up NodeLinks */ + _id?: number; } export interface TemplateDeclarationNode { @@ -1159,6 +1299,7 @@ export type MemberContainerNode = | InterfaceStatementNode | EnumStatementNode | UnionStatementNode + | IntersectionExpressionNode | ScalarStatementNode; export type MemberNode = diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index fa29ea5322..c30358350f 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -1,27 +1,37 @@ -export { ResolveCompilerOptionsOptions, resolveCompilerOptions } from "./config/index.js"; +export { resolveCompilerOptions, ResolveCompilerOptionsOptions } from "./config/index.js"; export * from "./core/index.js"; export * from "./lib/decorators.js"; export * from "./server/index.js"; +export type { PackageJson } from "./types/package-json.js"; import * as formatter from "./formatter/index.js"; export const TypeSpecPrettierPlugin = formatter; // DO NOT ADD ANYMORE EXPORTS HERE, this is for backcompat. Utils should be exported from the utils folder. export { + /** @deprecated use import from @typespec/compiler/utils */ + createRekeyableMap, /** @deprecated use import from @typespec/compiler/utils */ DuplicateTracker, /** @deprecated use import from @typespec/compiler/utils */ Queue, /** @deprecated use import from @typespec/compiler/utils */ TwoLevelMap, - /** @deprecated use import from @typespec/compiler/utils */ - createRekeyableMap, } from "./utils/index.js"; /** @deprecated Use TypeSpecPrettierPlugin */ export const CadlPrettierPlugin = TypeSpecPrettierPlugin; +import { $decorators as intrinsicDecorators } from "./lib/intrinsic/tsp-index.js"; +import { $decorators as stdDecorators } from "./lib/tsp-index.js"; /** @internal for Typespec compiler */ -export { $decorators } from "./lib/tsp-index.js"; -export { PackageJson } from "./types/package-json.js"; +export const $decorators = { + TypeSpec: { + ...stdDecorators.TypeSpec, + }, + "TypeSpec.Prototypes": { + ...intrinsicDecorators["TypeSpec.Prototypes"], + }, +}; + /** @deprecated use `PackageJson` */ export { PackageJson as NodePackage } from "./types/package-json.js"; diff --git a/packages/compiler/src/lib/intrinsic-decorators.ts b/packages/compiler/src/lib/intrinsic-decorators.ts deleted file mode 100644 index d6bfb06deb..0000000000 --- a/packages/compiler/src/lib/intrinsic-decorators.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DocTarget, setDocData } from "../core/intrinsic-type-state.js"; -import type { Program } from "../core/program.js"; -import type { DecoratorContext, ModelIndexer, Scalar, Type } from "../core/types.js"; - -export const namespace = "TypeSpec"; - -const indexTypeKey = Symbol.for(`TypeSpec.index`); -export const $indexer = (context: DecoratorContext, target: Type, key: Scalar, value: Type) => { - const indexer: ModelIndexer = { key, value }; - context.program.stateMap(indexTypeKey).set(target, indexer); -}; - -export function getIndexer(program: Program, target: Type): ModelIndexer | undefined { - return program.stateMap(indexTypeKey).get(target); -} - -/** - * @internal to be used to set the `@doc` from doc comment. - */ -export const $docFromComment = ( - context: DecoratorContext, - target: Type, - key: DocTarget, - text: string, -) => { - setDocData(context.program, target, key, { value: text, source: "comment" }); -}; diff --git a/packages/compiler/src/lib/intrinsic/decorators.ts b/packages/compiler/src/lib/intrinsic/decorators.ts new file mode 100644 index 0000000000..da86fd3c69 --- /dev/null +++ b/packages/compiler/src/lib/intrinsic/decorators.ts @@ -0,0 +1,41 @@ +import { DocTarget, setDocData } from "../../core/intrinsic-type-state.js"; +import type { Program } from "../../core/program.js"; +import type { DecoratorContext, ModelIndexer, Scalar, Type } from "../../core/types.js"; + +const indexTypeKey = Symbol.for(`TypeSpec.index`); +export const indexerDecorator = ( + context: DecoratorContext, + target: Type, + key: Scalar, + value: Type, +) => { + const indexer: ModelIndexer = { key, value }; + context.program.stateMap(indexTypeKey).set(target, indexer); +}; + +export function getIndexer(program: Program, target: Type): ModelIndexer | undefined { + return program.stateMap(indexTypeKey).get(target); +} + +/** + * @internal to be used to set the `@doc` from doc comment. + */ +export const docFromCommentDecorator = ( + context: DecoratorContext, + target: Type, + key: DocTarget, + text: string, +) => { + setDocData(context.program, target, key, { value: text, source: "comment" }); +}; + +const prototypeGetterKey = Symbol.for(`TypeSpec.Prototypes.getter`); +/** @internal */ +export function getterDecorator(context: DecoratorContext, target: Type) { + context.program.stateMap(prototypeGetterKey).set(target, true); +} + +/** @internal */ +export function isPrototypeGetter(program: Program, target: Type): ModelIndexer | undefined { + return program.stateMap(prototypeGetterKey).get(target) ?? false; +} diff --git a/packages/compiler/src/lib/intrinsic/tsp-index.ts b/packages/compiler/src/lib/intrinsic/tsp-index.ts new file mode 100644 index 0000000000..ffbdcc0df6 --- /dev/null +++ b/packages/compiler/src/lib/intrinsic/tsp-index.ts @@ -0,0 +1,12 @@ +import type { TypeSpecPrototypesDecorators } from "../../../generated-defs/TypeSpec.Prototypes.js"; +import { docFromCommentDecorator, getterDecorator, indexerDecorator } from "./decorators.js"; + +export const $decorators = { + TypeSpec: { + indexer: indexerDecorator, + docFromComment: docFromCommentDecorator, + }, + "TypeSpec.Prototypes": { + getter: getterDecorator, + } as TypeSpecPrototypesDecorators, +}; diff --git a/packages/compiler/src/server/completion.ts b/packages/compiler/src/server/completion.ts index 353f0dbe56..edf287e765 100644 --- a/packages/compiler/src/server/completion.ts +++ b/packages/compiler/src/server/completion.ts @@ -6,6 +6,7 @@ import { MarkupKind, TextEdit, } from "vscode-languageserver"; +import { getSymNode } from "../core/binder.js"; import { getDeprecationDetails } from "../core/deprecation.js"; import { CompilerHost, @@ -410,17 +411,15 @@ function addIdentifierCompletion( for (const [key, { sym, label, suffix }] of result) { let kind: CompletionItemKind; let deprecated = false; - const type = sym.type ?? program.checker.getTypeForNode(sym.declarations[0]); + const node = getSymNode(sym); + const type = sym.type ?? program.checker.getTypeForNode(node); if (sym.flags & (SymbolFlags.Function | SymbolFlags.Decorator)) { kind = CompletionItemKind.Function; - } else if ( - sym.flags & SymbolFlags.Namespace && - sym.declarations[0].kind !== SyntaxKind.NamespaceStatement - ) { + } else if (sym.flags & SymbolFlags.Namespace && node.kind !== SyntaxKind.NamespaceStatement) { kind = CompletionItemKind.Module; - } else if (sym.declarations[0]?.kind === SyntaxKind.AliasStatement) { + } else if (node?.kind === SyntaxKind.AliasStatement) { kind = CompletionItemKind.Variable; - deprecated = getDeprecationDetails(program, sym.declarations[0]) !== undefined; + deprecated = getDeprecationDetails(program, node) !== undefined; } else { kind = getCompletionItemKind(program, type); deprecated = getDeprecationDetails(program, type) !== undefined; diff --git a/packages/compiler/src/server/type-details.ts b/packages/compiler/src/server/type-details.ts index 24bb3a7924..6df2a35942 100644 --- a/packages/compiler/src/server/type-details.ts +++ b/packages/compiler/src/server/type-details.ts @@ -1,3 +1,4 @@ +import { getSymNode } from "../core/binder.js"; import { compilerAssert, DocContent, @@ -55,7 +56,7 @@ export function getSymbolDetails( function getSymbolDocumentation(program: Program, symbol: Sym) { const docs: string[] = []; - for (const node of symbol.declarations) { + for (const node of [...symbol.declarations, ...(symbol.node ? [symbol.node] : [])]) { // Add /** ... */ developer docs for (const d of node.docs ?? []) { docs.push(getDocContent(d.content)); @@ -65,7 +66,7 @@ function getSymbolDocumentation(program: Program, symbol: Sym) { // Add @doc(...) API docs let type = symbol.type; if (!type) { - const entity = program.checker.getTypeOrValueForNode(symbol.declarations[0]); + const entity = program.checker.getTypeOrValueForNode(getSymNode(symbol)); if (entity && isType(entity)) { type = entity; } diff --git a/packages/compiler/src/server/type-signature.ts b/packages/compiler/src/server/type-signature.ts index c6e42055d2..527d6f2e00 100644 --- a/packages/compiler/src/server/type-signature.ts +++ b/packages/compiler/src/server/type-signature.ts @@ -1,3 +1,4 @@ +import { getSymNode } from "../core/binder.js"; import { compilerAssert } from "../core/diagnostics.js"; import { printIdentifier } from "../core/helpers/syntax-utils.js"; import { getEntityName, getTypeName, isStdNamespace } from "../core/helpers/type-name-utils.js"; @@ -21,7 +22,7 @@ import { /** @internal */ export function getSymbolSignature(program: Program, sym: Sym): string { - const decl = sym.declarations[0]; + const decl = getSymNode(sym); switch (decl?.kind) { case SyntaxKind.AliasStatement: return fence(`alias ${getAliasSignature(decl)}`); diff --git a/packages/compiler/src/testing/test-host.ts b/packages/compiler/src/testing/test-host.ts index 9bf3808224..9b38cf0867 100644 --- a/packages/compiler/src/testing/test-host.ts +++ b/packages/compiler/src/testing/test-host.ts @@ -236,7 +236,7 @@ export const StandardTestLibrary: TypeSpecTestLibrary = { name: "@typespec/compiler", packageRoot: await findTestPackageRoot(import.meta.url), files: [ - { virtualPath: "./.tsp/dist/src/lib", realDir: "./dist/src/lib", pattern: "*" }, + { virtualPath: "./.tsp/dist/src/lib", realDir: "./dist/src/lib", pattern: "**" }, { virtualPath: "./.tsp/lib", realDir: "./lib", pattern: "**" }, ], }; diff --git a/packages/compiler/src/testing/test-utils.ts b/packages/compiler/src/testing/test-utils.ts index a8ed306373..f03062c855 100644 --- a/packages/compiler/src/testing/test-utils.ts +++ b/packages/compiler/src/testing/test-utils.ts @@ -1,5 +1,6 @@ +import { fail, ok } from "assert"; import { fileURLToPath } from "url"; -import { NodeHost, resolvePath } from "../core/index.js"; +import { getTypeName, NodeHost, resolvePath, Type } from "../core/index.js"; import { CompilerOptions } from "../core/options.js"; import { findProjectRoot } from "../utils/misc.js"; import { @@ -134,3 +135,22 @@ export function trimBlankLines(code: string) { return code.slice(start, end); } + +/** + * Compare 2 TypeSpec type and make sure they are the exact same(a === b). + * Show a better diff than just having ok(a===b) while not crashing like strictEqual/expect.toEqual + */ +export function expectTypeEquals(actual: Type | undefined, expected: Type) { + if (actual === expected) return; + + ok(actual, "Expected value to be defined"); + + const message = [`Expected type ${getTypeName(actual)} to be ${getTypeName(expected)}:`]; + if (actual.kind !== expected.kind) { + message.push(`kind: ${actual.kind} !== ${expected.kind}`); + } + if ("symbol" in actual && "symbol" in expected) { + message.push(`symbol: ${expected && actual.symbol === expected.symbol}`); + } + fail(message.join("\n")); +} diff --git a/packages/compiler/src/utils/misc.ts b/packages/compiler/src/utils/misc.ts index 8101d84e77..d7b2fbdf31 100644 --- a/packages/compiler/src/utils/misc.ts +++ b/packages/compiler/src/utils/misc.ts @@ -13,10 +13,10 @@ import { CompilerHost, Diagnostic, DiagnosticTarget, + MutableSymbolTable, NoTarget, RekeyableMap, SourceFile, - Sym, SymbolTable, } from "../core/types.js"; @@ -455,7 +455,7 @@ export class Queue { */ //prettier-ignore export type Mutable = - T extends SymbolTable ? T & { set(key: string, value: Sym): void } : + T extends SymbolTable ? T & MutableSymbolTable : T extends ReadonlyMap ? Map : T extends ReadonlySet ? Set : T extends readonly (infer V)[] ? V[] : diff --git a/packages/compiler/test/binder.test.ts b/packages/compiler/test/binder.test.ts index 273e9ea9cd..8b2f1e7728 100644 --- a/packages/compiler/test/binder.test.ts +++ b/packages/compiler/test/binder.test.ts @@ -42,17 +42,17 @@ describe("compiler: binder", () => { assertBindings("root", script.symbol.exports!, { A: { declarations: [SyntaxKind.NamespaceStatement], - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: { B: { - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: { C: { - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: {}, }, D: { - flags: SymbolFlags.Model, + flags: SymbolFlags.Model | SymbolFlags.Declaration, }, }, }, @@ -73,20 +73,20 @@ describe("compiler: binder", () => { assertBindings("root", script.symbol.exports!, { A: { declarations: [SyntaxKind.NamespaceStatement], - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: { B: { - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: { A: { declarations: [SyntaxKind.NamespaceStatement], - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: { B: { - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: { D: { - flags: SymbolFlags.Model, + flags: SymbolFlags.Model | SymbolFlags.Declaration, }, }, }, @@ -111,10 +111,10 @@ describe("compiler: binder", () => { assertBindings("root", script.symbol.exports!, { A: { declarations: [SyntaxKind.NamespaceStatement], - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: { B: { - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: {}, }, }, @@ -146,22 +146,22 @@ describe("compiler: binder", () => { assertBindings("root", script.symbol.exports!, { test: { declarations: [SyntaxKind.NamespaceStatement], - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: { A: { declarations: [SyntaxKind.NamespaceStatement, SyntaxKind.NamespaceStatement], - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: { B: { declarations: [SyntaxKind.NamespaceStatement, SyntaxKind.NamespaceStatement], - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: {}, }, get1: { - flags: SymbolFlags.Operation, + flags: SymbolFlags.Operation | SymbolFlags.Declaration, }, get2: { - flags: SymbolFlags.Operation, + flags: SymbolFlags.Operation | SymbolFlags.Declaration, }, }, }, @@ -183,23 +183,23 @@ describe("compiler: binder", () => { assertBindings("root", script.symbol.exports!, { A: { declarations: [SyntaxKind.NamespaceStatement], - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: { A: { - flags: SymbolFlags.Model, + flags: SymbolFlags.Model | SymbolFlags.Declaration, }, }, }, B: { declarations: [SyntaxKind.ModelStatement], - flags: SymbolFlags.Model, + flags: SymbolFlags.Model | SymbolFlags.Declaration, }, }); const BNode = script.statements[1] as ModelStatementNode; assertBindings("B", BNode.locals!, { - Foo: { flags: SymbolFlags.TemplateParameter }, - Bar: { flags: SymbolFlags.TemplateParameter }, + Foo: { flags: SymbolFlags.TemplateParameter | SymbolFlags.Declaration }, + Bar: { flags: SymbolFlags.TemplateParameter | SymbolFlags.Declaration }, }); }); it("binds enums", () => { @@ -215,16 +215,16 @@ describe("compiler: binder", () => { assertBindings("root", script.symbol.exports!, { A: { declarations: [SyntaxKind.NamespaceStatement], - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: { A: { - flags: SymbolFlags.Enum, + flags: SymbolFlags.Enum | SymbolFlags.Declaration, }, }, }, B: { declarations: [SyntaxKind.EnumStatement], - flags: SymbolFlags.Enum, + flags: SymbolFlags.Enum | SymbolFlags.Declaration, }, }); }); @@ -242,17 +242,17 @@ describe("compiler: binder", () => { assertBindings("root", script.symbol.exports!, { A: { declarations: [SyntaxKind.NamespaceStatement], - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: { Foo: { declarations: [SyntaxKind.OperationStatement], - flags: SymbolFlags.Operation, + flags: SymbolFlags.Operation | SymbolFlags.Declaration, }, }, }, Foo: { declarations: [SyntaxKind.OperationStatement], - flags: SymbolFlags.Operation, + flags: SymbolFlags.Operation | SymbolFlags.Declaration, }, }); }); @@ -270,24 +270,24 @@ describe("compiler: binder", () => { assertBindings("root", script.symbol.exports!, { A: { declarations: [SyntaxKind.NamespaceStatement], - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: { Foo: { declarations: [SyntaxKind.InterfaceStatement], - flags: SymbolFlags.Interface, + flags: SymbolFlags.Interface | SymbolFlags.Declaration, }, }, }, Bar: { declarations: [SyntaxKind.InterfaceStatement], - flags: SymbolFlags.Interface, + flags: SymbolFlags.Interface | SymbolFlags.Declaration, }, }); const INode = script.statements[1] as InterfaceStatementNode; assertBindings("Bar", INode.locals!, { - T: { flags: SymbolFlags.TemplateParameter }, - U: { flags: SymbolFlags.TemplateParameter }, + T: { flags: SymbolFlags.TemplateParameter | SymbolFlags.Declaration }, + U: { flags: SymbolFlags.TemplateParameter | SymbolFlags.Declaration }, }); }); @@ -304,24 +304,24 @@ describe("compiler: binder", () => { assertBindings("root", script.symbol.exports!, { A: { declarations: [SyntaxKind.NamespaceStatement], - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: { Foo: { declarations: [SyntaxKind.UnionStatement], - flags: SymbolFlags.Union, + flags: SymbolFlags.Union | SymbolFlags.Declaration, }, }, }, Bar: { declarations: [SyntaxKind.UnionStatement], - flags: SymbolFlags.Union, + flags: SymbolFlags.Union | SymbolFlags.Declaration, }, }); const UNode = script.statements[1] as UnionStatementNode; assertBindings("Bar", UNode.locals!, { - T: { flags: SymbolFlags.TemplateParameter }, - U: { flags: SymbolFlags.TemplateParameter }, + T: { flags: SymbolFlags.TemplateParameter | SymbolFlags.Declaration }, + U: { flags: SymbolFlags.TemplateParameter | SymbolFlags.Declaration }, }); }); @@ -338,24 +338,24 @@ describe("compiler: binder", () => { assertBindings("root", script.symbol.exports!, { A: { declarations: [SyntaxKind.NamespaceStatement], - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, exports: { Foo: { declarations: [SyntaxKind.AliasStatement], - flags: SymbolFlags.Alias, + flags: SymbolFlags.Alias | SymbolFlags.Declaration, }, }, }, Bar: { declarations: [SyntaxKind.AliasStatement], - flags: SymbolFlags.Alias, + flags: SymbolFlags.Alias | SymbolFlags.Declaration, }, }); const ANode = script.statements[1] as AliasStatementNode; assertBindings("Bar", ANode.locals!, { - T: { flags: SymbolFlags.TemplateParameter }, - U: { flags: SymbolFlags.TemplateParameter }, + T: { flags: SymbolFlags.TemplateParameter | SymbolFlags.Declaration }, + U: { flags: SymbolFlags.TemplateParameter | SymbolFlags.Declaration }, }); }); @@ -381,12 +381,12 @@ describe("compiler: binder", () => { SyntaxKind.ProjectionStatement, SyntaxKind.ProjectionStatement, ], - flags: SymbolFlags.Projection, + flags: SymbolFlags.Projection | SymbolFlags.Declaration, }, }); const toNode = (script.statements[0] as ProjectionStatementNode).to!; assertBindings("Foo#proj to", toNode.locals!, { - a: { flags: SymbolFlags.ProjectionParameter }, + a: { flags: SymbolFlags.ProjectionParameter | SymbolFlags.Declaration }, }); }); @@ -404,7 +404,7 @@ describe("compiler: binder", () => { .body[0] as ProjectionExpressionStatementNode ).expr as ProjectionLambdaExpressionNode; assertBindings("lambda", lambdaNode.locals!, { - a: { flags: SymbolFlags.FunctionParameter }, + a: { flags: SymbolFlags.FunctionParameter | SymbolFlags.Declaration }, }); }); @@ -428,29 +428,29 @@ describe("compiler: binder", () => { const sourceFile = bindJs(exports); assertBindings("jsFile", sourceFile.symbol.exports!, { Foo: { - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, declarations: [SyntaxKind.JsNamespaceDeclaration], exports: { Bar: { - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, declarations: [SyntaxKind.JsNamespaceDeclaration], exports: { "@myDec2": { - flags: SymbolFlags.Decorator | SymbolFlags.Implementation, + flags: SymbolFlags.Decorator | SymbolFlags.Declaration | SymbolFlags.Implementation, declarations: [SyntaxKind.JsSourceFile], }, fn2: { - flags: SymbolFlags.Function | SymbolFlags.Implementation, + flags: SymbolFlags.Function | SymbolFlags.Declaration | SymbolFlags.Implementation, declarations: [SyntaxKind.JsSourceFile], }, }, }, "@myDec": { - flags: SymbolFlags.Decorator | SymbolFlags.Implementation, + flags: SymbolFlags.Decorator | SymbolFlags.Declaration | SymbolFlags.Implementation, declarations: [SyntaxKind.JsSourceFile], }, fn: { - flags: SymbolFlags.Function | SymbolFlags.Implementation, + flags: SymbolFlags.Function | SymbolFlags.Declaration | SymbolFlags.Implementation, declarations: [SyntaxKind.JsSourceFile], }, }, @@ -469,21 +469,21 @@ describe("compiler: binder", () => { const sourceFile = bindJs(exports); assertBindings("jsFile", sourceFile.symbol.exports!, { Foo: { - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, declarations: [SyntaxKind.JsNamespaceDeclaration], exports: { Bar: { - flags: SymbolFlags.Namespace, + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, declarations: [SyntaxKind.JsNamespaceDeclaration], exports: { "@myDec2": { - flags: SymbolFlags.Decorator | SymbolFlags.Implementation, + flags: SymbolFlags.Decorator | SymbolFlags.Declaration | SymbolFlags.Implementation, declarations: [SyntaxKind.JsSourceFile], }, }, }, "@myDec": { - flags: SymbolFlags.Decorator | SymbolFlags.Implementation, + flags: SymbolFlags.Decorator | SymbolFlags.Declaration | SymbolFlags.Implementation, declarations: [SyntaxKind.JsSourceFile], }, }, diff --git a/packages/compiler/test/checker/alias.test.ts b/packages/compiler/test/checker/alias.test.ts index 5bbbb02321..cef3c4df52 100644 --- a/packages/compiler/test/checker/alias.test.ts +++ b/packages/compiler/test/checker/alias.test.ts @@ -76,7 +76,7 @@ describe("compiler: aliases", () => { testHost.addTypeSpecFile( "main.tsp", ` - alias Foo = int32 | T; + alias Foo = int32 | TEST; @test model A { prop: Foo<"hi"> @@ -298,7 +298,7 @@ describe("compiler: aliases", () => { message: `Cannot resolve 'prop' in node AliasStatement since it has no members. Did you mean to use "::" instead of "."?`, }); }); - it("trying to access member of aliased model expression shouldn't crash", async () => { + it("trying to access unknown member of aliased model expression shouldn't crash", async () => { testHost.addTypeSpecFile( "main.tsp", ` @@ -311,7 +311,7 @@ describe("compiler: aliases", () => { const diagnostics = await testHost.diagnose("main.tsp"); expectDiagnostics(diagnostics, { code: "invalid-ref", - message: `Cannot resolve 'prop' in node AliasStatement since it has no members. Did you mean to use "::" instead of "."?`, + message: `Model doesn't have member prop`, }); }); }); diff --git a/packages/compiler/test/checker/augment-decorators.test.ts b/packages/compiler/test/checker/augment-decorators.test.ts index 055f7d29c0..19ff624a17 100644 --- a/packages/compiler/test/checker/augment-decorators.test.ts +++ b/packages/compiler/test/checker/augment-decorators.test.ts @@ -1,95 +1,99 @@ -import { deepEqual, strictEqual } from "assert"; +import { ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { Model, Operation, StringLiteral, Type } from "../../src/core/types.js"; -import { TestHost, createTestHost, expectDiagnosticEmpty } from "../../src/testing/index.js"; +import { StringLiteral, Type } from "../../src/core/types.js"; +import { + TestHost, + createTestHost, + expectDiagnosticEmpty, + expectDiagnostics, +} from "../../src/testing/index.js"; + +let testHost: TestHost; + +beforeEach(async () => { + testHost = await createTestHost(); +}); -describe("compiler: checker: augment decorators", () => { - let testHost: TestHost; +it("run decorator without arguments", async () => { + let blueThing: Type | undefined; - beforeEach(async () => { - testHost = await createTestHost(); + testHost.addJsFile("test.js", { + $blue(_: any, t: Type) { + blueThing = t; + }, }); - it("run decorator without arguments", async () => { - let blueThing: Type | undefined; - - testHost.addJsFile("test.js", { - $blue(_: any, t: Type) { - blueThing = t; - }, - }); - - testHost.addTypeSpecFile( - "test.tsp", - ` + testHost.addTypeSpecFile( + "test.tsp", + ` import "./test.js"; @test model Foo { }; @@blue(Foo); `, - ); + ); - const { Foo } = await testHost.compile("test.tsp"); - strictEqual(Foo, blueThing); - }); + const { Foo } = await testHost.compile("test.tsp"); + strictEqual(Foo, blueThing); +}); - it("run decorator with arguments", async () => { - let customName: string | undefined; +it("run decorator with arguments", async () => { + let customName: string | undefined; - testHost.addJsFile("test.js", { - $customName(_: any, t: Type, n: StringLiteral) { - customName = n.value; - }, - }); + testHost.addJsFile("test.js", { + $customName(_: any, t: Type, n: StringLiteral) { + customName = n.value; + }, + }); - testHost.addTypeSpecFile( - "test.tsp", - ` + testHost.addTypeSpecFile( + "test.tsp", + ` import "./test.js"; model Foo { }; @@customName(Foo, "FooCustom"); `, - ); + ); - await testHost.compile("test.tsp"); - strictEqual(customName, "FooCustom"); - }); + await testHost.compile("test.tsp"); + strictEqual(customName, "FooCustom"); +}); - describe("declaration scope", () => { - let blueThing: Type | undefined; +describe("declaration scope", () => { + let blueThing: Type | undefined; - beforeEach(() => { - blueThing = undefined; - testHost.addJsFile("test.js", { - $blue(_: any, t: Type) { - blueThing = t; - }, - }); + beforeEach(() => { + blueThing = undefined; + testHost.addJsFile("test.js", { + $blue(_: any, t: Type) { + blueThing = t; + }, }); + }); - it("can be defined at the root of document", async () => { - testHost.addTypeSpecFile( - "test.tsp", - ` + it("can be defined at the root of document", async () => { + testHost.addTypeSpecFile( + "test.tsp", + ` import "./test.js"; @test model Foo { }; @@blue(Foo); `, - ); + ); - const { Foo } = await testHost.compile("test.tsp"); - strictEqual(Foo, blueThing); - }); + const { Foo } = await testHost.compile("test.tsp"); + strictEqual(Foo, blueThing); + }); - it("can be defined in blockless namespace", async () => { - testHost.addTypeSpecFile( - "test.tsp", - ` + it("can be defined in blockless namespace", async () => { + testHost.addTypeSpecFile( + "test.tsp", + ` import "./test.js"; namespace MyLibrary; @@ -98,16 +102,16 @@ describe("compiler: checker: augment decorators", () => { @@blue(Foo); `, - ); + ); - const { Foo } = await testHost.compile("test.tsp"); - strictEqual(Foo, blueThing); - }); + const { Foo } = await testHost.compile("test.tsp"); + strictEqual(Foo, blueThing); + }); - it("can be defined in namespace", async () => { - testHost.addTypeSpecFile( - "test.tsp", - ` + it("can be defined in namespace", async () => { + testHost.addTypeSpecFile( + "test.tsp", + ` import "./test.js"; namespace MyLibrary { @@ -116,21 +120,21 @@ describe("compiler: checker: augment decorators", () => { @@blue(Foo); } `, - ); + ); - const { Foo } = await testHost.compile("test.tsp"); - strictEqual(Foo, blueThing); - }); + const { Foo } = await testHost.compile("test.tsp"); + strictEqual(Foo, blueThing); + }); - // Regression for https://github.com/microsoft/typespec/issues/2600 - it("alias of instantiated template doesn't interfere with augment decorators", async () => { - // Here we could have add an issue where `Foo` would have been checked before the `@@blue` augment decorator could be run - // As we resolve the member symbols and meta types early, - // alias `FactoryString` would have checked the template instance `Factory` - // which would then have checked `Foo` and then `@@blue` wouldn't have been run - testHost.addTypeSpecFile( - "test.tsp", - ` + // Regression for https://github.com/microsoft/typespec/issues/2600 + it("alias of instantiated template doesn't interfere with augment decorators", async () => { + // Here we could have add an issue where `Foo` would have been checked before the `@@blue` augment decorator could be run + // As we resolve the member symbols and meta types early, + // alias `FactoryString` would have checked the template instance `Factory` + // which would then have checked `Foo` and then `@@blue` wouldn't have been run + testHost.addTypeSpecFile( + "test.tsp", + ` import "./test.js"; @test model Foo {}; @@ -146,177 +150,314 @@ describe("compiler: checker: augment decorators", () => { @@doc(Foo, "This doc"); @@blue(Foo); `, - ); + ); - const { Foo } = await testHost.compile("test.tsp"); - strictEqual(Foo, blueThing); - }); + const { Foo } = await testHost.compile("test.tsp"); + strictEqual(Foo, blueThing); }); +}); - describe("augment types", () => { - async function expectTarget(code: string, reference: string) { - let customName: string | undefined; - let runOnTarget: Type | undefined; +describe("augment types", () => { + async function expectTarget(code: string, reference: string) { + let customName: string | undefined; + let runOnTarget: Type | undefined; - testHost.addJsFile("test.js", { - $customName(_: any, t: Type, n: StringLiteral) { - runOnTarget = t; - customName = n.value; - }, - }); + testHost.addJsFile("test.js", { + $customName(_: any, t: Type, n: StringLiteral) { + runOnTarget = t; + customName = n.value; + }, + }); - testHost.addTypeSpecFile( - "test.tsp", - ` + testHost.addTypeSpecFile( + "test.tsp", + ` import "./test.js"; ${code} @@customName(${reference}, "FooCustom"); `, - ); + ); - const [result, diagnostics] = await testHost.compileAndDiagnose("test.tsp"); - expectDiagnosticEmpty(diagnostics); - strictEqual(runOnTarget?.kind, result.target.kind); - strictEqual(runOnTarget, result.target); - strictEqual(customName, "FooCustom"); - } + const [result, diagnostics] = await testHost.compileAndDiagnose("test.tsp"); + expectDiagnosticEmpty(diagnostics); + ok(result.target, `Missing element decorated with '@test("target")'`); + strictEqual(runOnTarget?.kind, result.target.kind); + strictEqual(runOnTarget, result.target); + strictEqual(customName, "FooCustom"); + } - it("namespace", () => expectTarget(`@test("target") namespace Foo {}`, "Foo")); + it("namespace", () => expectTarget(`@test("target") namespace Foo {}`, "Foo")); - it("global namespace", () => expectTarget(`@@test(global, "target");`, "global")); + it("global namespace", () => expectTarget(`@@test(global, "target");`, "global")); - it("model", () => expectTarget(`@test("target") model Foo {}`, "Foo")); - it("model property", () => - expectTarget( - `model Foo { + it("model", () => expectTarget(`@test("target") model Foo {}`, "Foo")); + it("model property", () => + expectTarget( + `model Foo { @test("target") name: string }`, - "Foo.name", - )); - it("enum", () => expectTarget(`@test("target") enum Foo { a, b }`, "Foo")); - it("enum member", () => expectTarget(`enum Foo { @test("target") a, b }`, "Foo.a")); - it("union", () => expectTarget(`@test("target") union Foo { }`, "Foo")); - it("union variant", () => expectTarget(`union Foo { @test("target") a: {}, b: {} }`, "Foo.a")); - it("operation", () => expectTarget(`@test("target") op foo(): string;`, "foo")); - it("interface", () => expectTarget(`@test("target") interface Foo { }`, "Foo")); - it("operation in interface", () => - expectTarget(`interface Foo { @test("target") list(): void }`, "Foo.list")); - it("uninstantiated template", async () => { - testHost.addJsFile("test.js", { - $customName(_: any, t: Type, n: string) { - const runOnTarget: Type | undefined = t; - const customName: string | undefined = n; - if (runOnTarget) { - } - if (customName) { + "Foo.name", + )); + it("property in alias of model expression", () => + expectTarget( + `alias Foo = { + @test("target") name: string + };`, + "Foo.name", + )); + it("property from spread of alias", () => + expectTarget( + `alias Spread = { + @test("target") name: string + }; + model Foo { + ...Spread, + }`, + "Foo.name", + )); + it("property from spread of alias in alias expression", () => + expectTarget( + `alias Spread = { + @test("target") name: string + }; + alias Foo = { + ...Spread, + };`, + "Foo.name", + )); + + it("property from model is", () => + expectTarget( + `model Base { + @test("target") name: string + }; + model Foo is Base;`, + "Foo.name", + )); + + it("property of nested model expression", () => + expectTarget( + `model Foo { + nested: { + @test("target") name: string + } + }`, + "Foo.nested::type.name", + )); + it("property of multiple nested model expression", () => + expectTarget( + `model Foo { + nested: { + nestedAgain: { + @test("target") name: string } - }, - }); + } + }`, + "Foo.nested::type.nestedAgain::type.name", + )); + + it("enum", () => expectTarget(`@test("target") enum Foo { a, b }`, "Foo")); + it("enum member", () => expectTarget(`enum Foo { @test("target") a, b }`, "Foo.a")); + it("union", () => expectTarget(`@test("target") union Foo { }`, "Foo")); + it("union variant", () => expectTarget(`union Foo { @test("target") a: {}, b: {} }`, "Foo.a")); + it("operation", () => expectTarget(`@test("target") op foo(): string;`, "foo")); + it("interface", () => expectTarget(`@test("target") interface Foo { }`, "Foo")); + it("operation in interface", () => + expectTarget(`interface Foo { @test("target") list(): void }`, "Foo.list")); + it("operation parameter", () => + expectTarget(`op foo(@test("target") bar: string): string;`, "foo::parameters.bar")); + it("operation parameter nested model expression", () => + expectTarget( + `op foo(nested: { @test("target") bar: string }): string;`, + "foo::parameters.nested::type.bar", + )); + + describe("uninstantiated template", () => { + it("model", () => + expectTarget( + ` + @test("target") model Foo { testProp: T } + + model Insantiate { foo: Foo } + `, + "Foo", + )); - testHost.addTypeSpecFile( - "test.tsp", + it("model property", () => + expectTarget( ` - import "./test.js"; - - model Foo { - testProp: T; - }; + model Foo { @test("target") testProp: T } + + model Insantiate { foo: Foo } + `, + "Foo.testProp", + )); + + it("via alias", () => + expectTarget( + ` + @test("target") model Foo { testProp: T } - @test - op stringTest(): Foo; + alias FooAlias = Foo; - @@customName(Foo, "Some foo thing"); - @@customName(Foo.testProp, "Some test prop"); + model Insantiate { foo: FooAlias } `, - ); - const [results, diagnostics] = await testHost.compileAndDiagnose("test.tsp"); - expectDiagnosticEmpty(diagnostics); - const stringTest = results.stringTest as Operation; - strictEqual(stringTest.kind, "Operation"); - deepEqual((stringTest.returnType as Model).decorators[0].args[0].value, { - entityKind: "Type", - kind: "String", - value: "Some foo thing", - isFinished: false, - }); - for (const prop of (stringTest.returnType as Model).properties) { - deepEqual(prop[1].decorators[0].args[0].value, { - entityKind: "Type", - kind: "String", - value: "Some test prop", - isFinished: false, - }); - } + "FooAlias", + )); + }); +}); + +describe("emit diagnostic", () => { + function diagnose(code: string) { + testHost.addJsFile("test.js", { + $customName(_: any, t: Type, n: string) {}, }); - it("emit diagnostic if target is instantiated template", async () => { - testHost.addJsFile("test.js", { - $customName(_: any, t: Type, n: string) { - const runOnTarget: Type | undefined = t; - const customName: string | undefined = n; - if (runOnTarget) { - } - if (customName) { - } - }, - }); + testHost.addTypeSpecFile( + "test.tsp", + ` + import "./test.js"; + ${code} + `, + ); + return testHost.diagnose("test.tsp"); + } - testHost.addTypeSpecFile( - "test.tsp", - ` + it("if using unknown decorator", async () => { + testHost.addTypeSpecFile( + "test.tsp", + ` import "./test.js"; + + `, + ); + const diagnostics = await diagnose(` + model Foo {} + @@notDefined(Foo, "A string Foo"); + `); + expectDiagnostics(diagnostics, { + code: "invalid-ref", + message: "Unknown decorator @notDefined", + }); + }); - model Foo { - testProp: T; - }; + it("if target is invalid identifier", async () => { + const diagnostics = await diagnose(`@@customName(Foo, "A string Foo");`); + expectDiagnostics(diagnostics, { + code: "invalid-ref", + message: "Unknown identifier Foo", + }); + }); - alias StringFoo = Foo; + it("if target is invalid member expression", async () => { + const diagnostics = await diagnose(`@@customName(Foo.prop, "A string Foo");`); + expectDiagnostics(diagnostics, { + code: "invalid-ref", + message: "Unknown identifier Foo", + }); + }); - @test - op stringTest(): Foo; + it("if target is missing member", async () => { + const diagnostics = await diagnose(`model Foo{} @@customName(Foo.prop, "A string Foo");`); + expectDiagnostics(diagnostics, { + code: "invalid-ref", + message: "Model doesn't have member prop", + }); + }); - @@customName(Foo, "A string Foo"); - @@customName(StringFoo, "A string Foo"); - `, - ); - const diagnostics = await testHost.diagnose("test.tsp"); - strictEqual(diagnostics.length, 2); - for (const diagnostic of diagnostics) { - strictEqual(diagnostic.message, "Cannot reference template instances"); - } + it("emit diagnostic if target is instantiated template", async () => { + const diagnostics = await diagnose(` + model Foo { prop: T } + @@customName(Foo, "A string Foo"); + `); + expectDiagnostics(diagnostics, { + code: "augment-decorator-target", + message: "Cannot reference template instances", }); + }); + + it("emit diagnostic if target is instantiated template via alias", async () => { + const diagnostics = await diagnose(` + model Foo { prop: T } + alias StringFoo = Foo; - describe("augment location", () => { - async function expectAugmentTarget(code: string) { - let customName: string | undefined; - let runOnTarget: Type | undefined; - - testHost.addJsFile("test.js", { - $customName(_: any, t: Type, n: StringLiteral) { - runOnTarget = t; - customName = n.value; - }, - }); - - testHost.addTypeSpecFile( - "test.tsp", - ` + @@customName(StringFoo, "A string Foo"); + `); + expectDiagnostics(diagnostics, { + code: "augment-decorator-target", + message: "Cannot reference template instances", + }); + }); + + it("emit diagnostic if target is instantiated template member", async () => { + const diagnostics = await diagnose(` + interface Foo { test(): T } + + @@customName(Foo.test, "A string Foo"); + `); + expectDiagnostics(diagnostics, { + code: "augment-decorator-target", + message: "Cannot reference template instances", + }); + }); + + it("emit diagnostic if target is instantiated template member container", async () => { + const diagnostics = await diagnose(` + interface Foo { test(): T } + alias FooString = Foo; + @@customName(FooString.test, "A string Foo"); + `); + expectDiagnostics(diagnostics, { + code: "augment-decorator-target", + message: "Cannot reference template instances", + }); + }); + + it("emit diagnostic if target is instantiated template via metatype", async () => { + const diagnostics = await diagnose(` + model Foo { prop: T } + op test(): Foo; + + @@customName(test::returnType, "A string Foo"); + `); + expectDiagnostics(diagnostics, { + code: "augment-decorator-target", + message: "Cannot reference template instances", + }); + }); +}); + +describe("augment location", () => { + async function expectAugmentTarget(code: string) { + let customName: string | undefined; + let runOnTarget: Type | undefined; + + testHost.addJsFile("test.js", { + $customName(_: any, t: Type, n: StringLiteral) { + runOnTarget = t; + customName = n.value; + }, + }); + + testHost.addTypeSpecFile( + "test.tsp", + ` import "./test.js"; ${code} `, - ); + ); - const { target } = await testHost.compile("test.tsp"); - strictEqual(runOnTarget?.kind, target.kind); - strictEqual(runOnTarget, target); - strictEqual(customName, "FooCustom"); - } + const { target } = await testHost.compile("test.tsp"); + strictEqual(runOnTarget?.kind, target.kind); + strictEqual(runOnTarget, target); + strictEqual(customName, "FooCustom"); + } - it("augment type in another namespace", async () => { - await expectAugmentTarget(` + it("augment type in another namespace", async () => { + await expectAugmentTarget(` namespace Lib { @test("target") model Foo {} } @@ -325,73 +466,71 @@ describe("compiler: checker: augment decorators", () => { @@customName(Lib.Foo, "FooCustom"); } `); - }); + }); - it("augment type in another file checked before", async () => { - testHost.addTypeSpecFile("lib.tsp", `@test("target") model Foo {} `); + it("augment type in another file checked before", async () => { + testHost.addTypeSpecFile("lib.tsp", `@test("target") model Foo {} `); - await expectAugmentTarget(` + await expectAugmentTarget(` import "./lib.tsp"; @@customName(Foo, "FooCustom"); `); - }); + }); - it("augment type in another file checked after", async () => { - testHost.addTypeSpecFile("lib.tsp", `@@customName(Foo, "FooCustom"); `); + it("augment type in another file checked after", async () => { + testHost.addTypeSpecFile("lib.tsp", `@@customName(Foo, "FooCustom"); `); - await expectAugmentTarget(` + await expectAugmentTarget(` import "./lib.tsp"; @test("target") model Foo {} `); - }); + }); +}); + +describe("augment order", () => { + async function expectAugmentTarget(code: string) { + let customName: string | undefined; + let runOnTarget: Type | undefined; + + testHost.addJsFile("test.js", { + $customName(_: any, t: Type, n: StringLiteral) { + runOnTarget = t; + customName = n.value; + }, }); - describe("augment order", () => { - async function expectAugmentTarget(code: string) { - let customName: string | undefined; - let runOnTarget: Type | undefined; - - testHost.addJsFile("test.js", { - $customName(_: any, t: Type, n: StringLiteral) { - runOnTarget = t; - customName = n.value; - }, - }); - - testHost.addTypeSpecFile( - "test.tsp", - ` + testHost.addTypeSpecFile( + "test.tsp", + ` import "./test.js"; ${code} `, - ); + ); - const { target } = await testHost.compile("test.tsp"); - strictEqual(runOnTarget?.kind, target.kind); - strictEqual(runOnTarget, target); - strictEqual(customName, "FooCustom"); - } + const { target } = await testHost.compile("test.tsp"); + strictEqual(runOnTarget?.kind, target.kind); + strictEqual(runOnTarget, target); + strictEqual(customName, "FooCustom"); + } - it("augment decorator should be applied at last", async () => { - await expectAugmentTarget(` + it("augment decorator should be applied at last", async () => { + await expectAugmentTarget(` @test("target") @customName("Foo") model Foo {} @@customName(Foo, "FooCustom"); `); - }); + }); - it("augment decorator - last win", async () => { - await expectAugmentTarget(` + it("augment decorator - last win", async () => { + await expectAugmentTarget(` @test("target") @customName("Foo") model Foo {} @@customName(Foo, "NonCustom"); @@customName(Foo, "FooCustom"); `); - }); - }); }); }); diff --git a/packages/compiler/test/checker/model.test.ts b/packages/compiler/test/checker/model.test.ts index 87ca5ccbd2..020d97f078 100644 --- a/packages/compiler/test/checker/model.test.ts +++ b/packages/compiler/test/checker/model.test.ts @@ -103,7 +103,7 @@ describe("compiler: models", () => { const diagnostics = await testHost.diagnose("main.tsp"); expectDiagnostics(diagnostics, [ { - code: "unknown-identifier", + code: "invalid-ref", message: "Unknown identifier notValidType", }, ]); @@ -288,9 +288,7 @@ describe("compiler: models", () => { `, ); const diagnostics = await testHost.diagnose("main.tsp"); - expectDiagnostics(diagnostics, [ - { code: "unknown-identifier", message: "Unknown identifier bool" }, - ]); + expectDiagnostics(diagnostics, [{ code: "invalid-ref", message: "Unknown identifier bool" }]); }); describe("link model with its properties", () => { @@ -715,6 +713,67 @@ describe("compiler: models", () => { strictEqual(diagnostics[0].message, "Type 'A' recursively references itself as a base type."); }); + it("emit error when extends circular reference with alias - case 1", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model A extends B {} + model C extends A {} + alias B = C; + `, + ); + const diagnostics = await testHost.diagnose("main.tsp"); + expectDiagnostics(diagnostics, { + code: "circular-base-type", + message: "Type 'A' recursively references itself as a base type.", + }); + }); + + it("emit error when extends circular reference with alias - case 2", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model A extends B {} + alias B = A; + `, + ); + const diagnostics = await testHost.diagnose("main.tsp"); + expectDiagnostics(diagnostics, { + code: "circular-base-type", + message: "Type 'A' recursively references itself as a base type.", + }); + }); + + it("emit error when model is circular reference with alias", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model A is B; + model C is A; + alias B = C; + `, + ); + const diagnostics = await testHost.diagnose("main.tsp"); + expectDiagnostics(diagnostics, { + code: "circular-base-type", + message: "Type 'A' recursively references itself as a base type.", + }); + }); + it("emit error when model is circular reference with alias - case 2", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model A is B; + alias B = A; + `, + ); + const diagnostics = await testHost.diagnose("main.tsp"); + expectDiagnostics(diagnostics, { + code: "circular-base-type", + message: "Type 'A' recursively references itself as a base type.", + }); + }); + it("emit no error when extends has property to base model", async () => { testHost.addTypeSpecFile( "main.tsp", diff --git a/packages/compiler/test/checker/operations.test.ts b/packages/compiler/test/checker/operations.test.ts index 78773e11b5..60cea11379 100644 --- a/packages/compiler/test/checker/operations.test.ts +++ b/packages/compiler/test/checker/operations.test.ts @@ -319,6 +319,22 @@ describe("compiler: operations", () => { ]); }); + it("emit error when extends circular reference with alias", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + op a is b; + op c is a; + alias b = c; + `, + ); + const diagnostics = await testHost.diagnose("main.tsp"); + expectDiagnostics(diagnostics, { + code: "circular-op-signature", + message: `Operation 'a' recursively references itself.`, + }); + }); + it("emit diagnostic when operation(in interface) is referencing itself as signature", async () => { testHost.addTypeSpecFile( "main.tsp", @@ -329,12 +345,10 @@ describe("compiler: operations", () => { `, ); const diagnostics = await testHost.diagnose("main.tsp"); - expectDiagnostics(diagnostics, [ - { - code: "circular-op-signature", - message: "Operation 'foo' recursively references itself.", - }, - ]); + expectDiagnostics(diagnostics, { + code: "circular-op-signature", + message: "Operation 'foo' recursively references itself.", + }); }); it("emit diagnostic when operations reference each other using signature", async () => { diff --git a/packages/compiler/test/checker/references.test.ts b/packages/compiler/test/checker/references.test.ts index 3c7de6b9e9..1d6a58b795 100644 --- a/packages/compiler/test/checker/references.test.ts +++ b/packages/compiler/test/checker/references.test.ts @@ -2,7 +2,12 @@ import { ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { Enum, Interface, Model, Operation, Type } from "../../src/core/types.js"; -import { TestHost, createTestHost, expectDiagnostics } from "../../src/testing/index.js"; +import { + TestHost, + createTestHost, + expectDiagnostics, + expectTypeEquals, +} from "../../src/testing/index.js"; describe("compiler: references", () => { let testHost: TestHost; @@ -26,7 +31,7 @@ describe("compiler: references", () => { target: any; }; const expectedTarget = resolveTarget ? resolveTarget(target) : target; - strictEqual(RefContainer.properties.get("y")!.type, expectedTarget); + expectTypeEquals(RefContainer.properties.get("y")!.type, expectedTarget); } const refCode = ` @test model RefContainer { y: ${ref} } @@ -225,17 +230,17 @@ describe("compiler: references", () => { testHost.addTypeSpecFile( "main.tsp", ` - @test model Foo { - a: string; - b: Foo.a; - } - `, + @test model Foo { + a: string; + b: Foo.a; + } + `, ); const { Foo } = (await testHost.compile("./main.tsp")) as { Foo: Model; }; - strictEqual(Foo.properties.get("b")!.type, Foo.properties.get("a")); + expectTypeEquals(Foo.properties.get("b")!.type, Foo.properties.get("a")!); }); it("can reference sibling property defined after", async () => { @@ -573,7 +578,7 @@ describe("compiler: references", () => { expectDiagnostics(diagnostics, [ { - code: "unknown-identifier", + code: "invalid-ref", message: `Unknown identifier NotDefined`, }, ]); @@ -592,6 +597,37 @@ describe("compiler: references", () => { ref: "Person.address::type.city", })); + describe("ModelProperty::type that is an anonymous model", () => + itCanReference({ + code: ` + model A { a: string } + model B { b: string } + model Person { + @test("target") address: { ...A, ...B }; + } + `, + ref: "Person.address::type.a", + resolveTarget: (prop) => { + // Abc + return prop.type.properties.get("a"); + }, + })); + + describe("ModelProperty::type that is an intersection", () => + itCanReference({ + code: ` + model A { a: string } + model B { b: string } + model Person { + @test("target") address: A & B; + } + `, + ref: "Person.address::type.a", + resolveTarget: (prop) => { + // Abc + return prop.type.properties.get("a"); + }, + })); describe("ModelProperty::type that is a type reference", () => itCanReference({ code: ` @@ -646,8 +682,7 @@ describe("compiler: references", () => { ]); }); - // Error should be removed when this is fixed https://github.com/microsoft/typespec/issues/2213 - it("(TEMP) emits a diagnostic when referencing a non-resolved meta type property", async () => { + it("allows spreading meta type property", async () => { testHost.addTypeSpecFile( "main.tsp", ` @@ -659,20 +694,39 @@ describe("compiler: references", () => { a: A; } - model Spread { + @test model Spread { ... B.a::type; } `, ); - const diagnostics = await testHost.diagnose("./main.tsp"); + const { Spread } = (await testHost.compile("./main.tsp")) as { Spread: Model }; + strictEqual(Spread.properties.size, 1); + ok(Spread.properties.get("name")); + }); - expectDiagnostics(diagnostics, [ - { - code: "invalid-ref", - message: `ModelProperty doesn't have meta property type`, - }, - ]); + it("the Johan test case", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model Completion { + choices: string[]; + } + + op baseVersion("model": string, top_n: int32): Completion; + + @test op azureVersion(... baseVersion::parameters, dataSources: string[]): baseVersion::returnType & { rai: string[] }; + `, + ); + + const { azureVersion } = (await testHost.compile("./main.tsp")) as { + azureVersion: Operation; + }; + const existingNames = [...azureVersion.parameters.properties.values()].map((v) => v.name); + strictEqual(existingNames.length, 3); + ok(existingNames.includes("model")); + ok(existingNames.includes("top_n")); + ok(existingNames.includes("dataSources")); }); }); }); diff --git a/packages/compiler/test/checker/resolve-type-reference.test.ts b/packages/compiler/test/checker/resolve-type-reference.test.ts index 84edace38d..eac9e00200 100644 --- a/packages/compiler/test/checker/resolve-type-reference.test.ts +++ b/packages/compiler/test/checker/resolve-type-reference.test.ts @@ -1,4 +1,4 @@ -import { strictEqual } from "assert"; +import { ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { BasicTestRunner, @@ -21,7 +21,7 @@ describe("compiler: resolveTypeReference", () => { } const [resolved, diagnostics] = runner.program.resolveTypeReference(reference); expectDiagnosticEmpty(diagnostics); - strictEqual(resolved, target); + ok(resolved === target, `Exected to resolve ${reference} to same type as ${code}`); } async function diagnoseResolution(reference: string, code: string) { @@ -91,6 +91,16 @@ describe("compiler: resolveTypeReference", () => { ); }); + it("resolve model property from base class", async () => { + await expectResolve( + "Pet.name", + ` + model Animal { @test("target") name: string} + model Pet extends Animal { } + `, + ); + }); + it("resolve metatype", async () => { await expectResolve( "Pet.home::type.street", @@ -114,6 +124,16 @@ describe("compiler: resolveTypeReference", () => { ); }); + it("resolve enum member with spread", async () => { + await expectResolve( + "Direction.up", + ` + enum Foo { @test("target") up } + enum Direction { ... Foo } + `, + ); + }); + it("resolve via alias", async () => { await expectResolve( "PetName", @@ -128,7 +148,7 @@ describe("compiler: resolveTypeReference", () => { it("emit diagnostic if not found", async () => { const diagnostics = await diagnoseResolution("Direction.up", ""); expectDiagnostics(diagnostics, { - code: "unknown-identifier", + code: "invalid-ref", message: "Unknown identifier Direction", }); }); diff --git a/packages/compiler/test/checker/scalar.test.ts b/packages/compiler/test/checker/scalar.test.ts index 10caf9e1e0..b97b15c335 100644 --- a/packages/compiler/test/checker/scalar.test.ts +++ b/packages/compiler/test/checker/scalar.test.ts @@ -139,4 +139,36 @@ describe("compiler: scalars", () => { expectDiagnostics(diagnostics, [{ code: "unassignable", message: /42.*S/ }]); }); }); + + describe("circular references", () => { + describe("emit diagnostic when circular reference in extends", () => { + it("reference itself", async () => { + const diagnostics = await runner.diagnose(`scalar a extends a;`); + expectDiagnostics(diagnostics, { + code: "circular-base-type", + message: "Type 'a' recursively references itself as a base type.", + }); + }); + it("reference itself via another scalar", async () => { + const diagnostics = await runner.diagnose(` + scalar a extends b; + scalar b extends a; + `); + expectDiagnostics(diagnostics, { + code: "circular-base-type", + message: "Type 'a' recursively references itself as a base type.", + }); + }); + it("reference itself via an alias", async () => { + const diagnostics = await runner.diagnose(` + scalar a extends b; + alias b = a; + `); + expectDiagnostics(diagnostics, { + code: "circular-base-type", + message: "Type 'a' recursively references itself as a base type.", + }); + }); + }); + }); }); diff --git a/packages/compiler/test/checker/spread.test.ts b/packages/compiler/test/checker/spread.test.ts index 1f79437a4b..bb3c1ee0df 100644 --- a/packages/compiler/test/checker/spread.test.ts +++ b/packages/compiler/test/checker/spread.test.ts @@ -39,7 +39,7 @@ describe("compiler: spread", () => { } }); - it("doesn't emit additional diagnostic if spread reference is unknown-identifier", async () => { + it("doesn't emit additional diagnostic if spread reference is invalid-ref", async () => { const diagnostics = await runner.diagnose(` model Foo { ...NotDefined @@ -47,7 +47,7 @@ describe("compiler: spread", () => { `); expectDiagnostics(diagnostics, { - code: "unknown-identifier", + code: "invalid-ref", message: "Unknown identifier NotDefined", }); }); diff --git a/packages/compiler/test/checker/templates.test.ts b/packages/compiler/test/checker/templates.test.ts index a892a87b72..59ccf98f43 100644 --- a/packages/compiler/test/checker/templates.test.ts +++ b/packages/compiler/test/checker/templates.test.ts @@ -296,7 +296,7 @@ describe("compiler: templates", () => { const [{ prop }, diagnostics] = await testHost.compileAndDiagnose("main.tsp"); // Only one error expectDiagnostics(diagnostics, { - code: "unknown-identifier", + code: "invalid-ref", message: "Unknown identifier notExists", }); diff --git a/packages/compiler/test/checker/using.test.ts b/packages/compiler/test/checker/using.test.ts index c35867d4d1..736e4b5052 100644 --- a/packages/compiler/test/checker/using.test.ts +++ b/packages/compiler/test/checker/using.test.ts @@ -106,6 +106,29 @@ describe("compiler: using statements", () => { strictEqual(Y.properties.size, 1); }); + // This is checking a case where when using a namespace it would start linking its content + // before the using of the file were resolved themself causing invalid refs. + it("using a namespace won't start linking it", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + import "./a.tsp"; + using A; + `, + ); + testHost.addTypeSpecFile( + "a.tsp", + ` + import "./b.tsp"; + using B; + namespace A { @test model AModel { b: BModel } } + `, + ); + testHost.addTypeSpecFile("b.tsp", `namespace B { model BModel {} }`); + + expectDiagnosticEmpty(await testHost.diagnose("./")); + }); + it("TypeSpec.Xyz namespace doesn't need TypeSpec prefix in using", async () => { testHost.addTypeSpecFile( "main.tsp", @@ -169,34 +192,58 @@ describe("compiler: using statements", () => { expectDiagnosticEmpty(diagnostics); }); - it("throws errors for duplicate imported usings", async () => { - testHost.addTypeSpecFile( - "main.tsp", - ` + describe("duplicate usings", () => { + it("doesn't consider using scoped in namespace as duplicate", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + import "./a.tsp"; + using A; + + namespace B { + using A; + } + namespace C { + using A; + } + `, + ); + testHost.addTypeSpecFile("a.tsp", `namespace A { model AModel {} }`); + + const diagnostics = await testHost.diagnose("./"); + expectDiagnosticEmpty(diagnostics); + }); + + it("throws errors for duplicate imported usings", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` import "./a.tsp"; import "./b.tsp"; `, - ); - testHost.addTypeSpecFile( - "a.tsp", - ` + ); + testHost.addTypeSpecFile( + "a.tsp", + ` namespace N.M; model X { x: int32 } `, - ); + ); - testHost.addTypeSpecFile( - "b.tsp", - ` + testHost.addTypeSpecFile( + "b.tsp", + ` using N.M; using N.M; `, - ); + ); - const diagnostics = await testHost.diagnose("./"); - strictEqual(diagnostics.length, 1); - strictEqual(diagnostics[0].code, "duplicate-using"); - strictEqual(diagnostics[0].message, 'duplicate using of "N.M" namespace'); + const diagnostics = await testHost.diagnose("./"); + expectDiagnostics(diagnostics, [ + { code: "duplicate-using", message: 'duplicate using of "N.M" namespace' }, + { code: "duplicate-using", message: 'duplicate using of "N.M" namespace' }, + ]); + }); }); it("does not throws errors for different usings with the same bindings if not used", async () => { @@ -338,7 +385,6 @@ describe("compiler: using statements", () => { code: "ambiguous-symbol", message: `"doc" is an ambiguous name between TypeSpec.doc, Test.A.doc. Try using fully qualified name instead: TypeSpec.doc, Test.A.doc`, }, - { code: "unknown-decorator" }, ]); }); @@ -365,7 +411,6 @@ describe("compiler: using statements", () => { code: "ambiguous-symbol", message: `"doc" is an ambiguous name between TypeSpec.doc, Test.A.doc. Try using fully qualified name instead: TypeSpec.doc, Test.A.doc`, }, - { code: "unknown-decorator" }, { code: "missing-implementation" }, ]); }); @@ -521,8 +566,32 @@ describe("compiler: using statements", () => { ); await testHost.compile("./"); }); +}); + +describe("emit diagnostics", () => { + async function diagnose(code: string) { + const testHost = await createTestHost(); + testHost.addTypeSpecFile( + "main.tsp", + ` + ${code} + `, + ); + + return testHost.diagnose("main.tsp"); + } + + it("unknown identifier", async () => { + const diagnostics = await diagnose(` + using NotDefined; + `); + expectDiagnostics(diagnostics, { + code: "invalid-ref", + message: "Unknown identifier NotDefined", + }); + }); - describe("emit diagnostic when using non-namespace types", () => { + describe("when using non-namespace types", () => { [ ["model", "model Target {}"], ["enum", "enum Target {}"], @@ -532,15 +601,10 @@ describe("compiler: using statements", () => { ["operation", "op Target(): void;"], ].forEach(([name, code]) => { it(name, async () => { - testHost.addTypeSpecFile( - "main.tsp", - ` + const diagnostics = await diagnose(` using Target; ${code} - `, - ); - - const diagnostics = await testHost.diagnose("./"); + `); expectDiagnostics(diagnostics, { code: "using-invalid-ref", message: "Using must refer to a namespace", diff --git a/packages/compiler/test/name-resolver.test.ts b/packages/compiler/test/name-resolver.test.ts new file mode 100644 index 0000000000..e336595fb0 --- /dev/null +++ b/packages/compiler/test/name-resolver.test.ts @@ -0,0 +1,1488 @@ +import { ok, strictEqual } from "assert"; +import { beforeEach, describe, expect, it } from "vitest"; +import { Binder, createBinder } from "../src/core/binder.js"; +import { typeReferenceToString } from "../src/core/helpers/syntax-utils.js"; +import { inspectSymbolFlags } from "../src/core/inspector/symbol.js"; +import { createLogger } from "../src/core/logger/logger.js"; +import { createTracer } from "../src/core/logger/tracer.js"; +import { createResolver, NameResolver } from "../src/core/name-resolver.js"; +import { getNodeAtPosition, parse } from "../src/core/parser.js"; +import { + IdentifierNode, + JsSourceFileNode, + MemberExpressionNode, + Node, + NodeFlags, + ResolutionResult, + ResolutionResultFlags, + SymbolFlags, + SymbolLinks, + SyntaxKind, + TypeReferenceNode, +} from "../src/core/types.js"; +import { createSourceFile, Program, Sym } from "../src/index.js"; + +let binder: Binder; +let resolver: ReturnType; +let program: Program; + +beforeEach(() => { + program = createProgramShim(); + binder = createBinder(program); + resolver = createResolver(program); +}); + +describe("model statements", () => { + describe("binding", () => { + it("binds is members", () => { + const sym = getGlobalSymbol([ + ` + model M1 { + x: "x"; + } + + model M2 is M1 { + y: "y"; + } + `, + ]); + + assertSymbol(sym, { + exports: { + M1: { + members: { + x: { + flags: SymbolFlags.Member, + }, + }, + }, + M2: { + members: { + x: { + flags: SymbolFlags.Member, + }, + y: { + flags: SymbolFlags.Member, + }, + }, + }, + }, + }); + }); + + it("binds spread members", () => { + const sym = getGlobalSymbol([ + ` + model M1 { + x: "x"; + } + + model M2 { + ... M1, + y: "y"; + } + `, + ]); + + assertSymbol(sym, { + exports: { + M1: { + members: { + x: { + flags: SymbolFlags.Member, + }, + }, + }, + M2: { + members: { + x: { + flags: SymbolFlags.Member, + }, + y: { + flags: SymbolFlags.Member, + }, + }, + }, + }, + }); + }); + + it("binds spread members of templates", () => { + const sym = getGlobalSymbol([ + ` + model M1 { + x: "x"; + ... T; + } + + model M2 { + ... M1<{}>, + y: "y"; + } + `, + ]); + + assertSymbol(sym, { + exports: { + M2: { + members: { + x: { + flags: SymbolFlags.Member, + }, + y: { + flags: SymbolFlags.Member, + }, + }, + }, + }, + }); + }); + + it("binds spread members of templates with constraints", () => { + const sym = getGlobalSymbol([ + ` + model M1 { + x: "x"; + ... T; + } + + model M2 { + ... M1<{}>, + y: "y"; + } + `, + ]); + assertSymbol(sym, { + exports: { + M2: { + members: { + x: { flags: SymbolFlags.Member }, + y: { flags: SymbolFlags.Member }, + z: { flags: SymbolFlags.Member }, + }, + }, + }, + }); + }); + + it("sets containsUnknownMembers flag with spread/extends of instantiations", () => { + const sym = getGlobalSymbol([ + ` + model Template { ... T }; + + model M1 extends Template<{}> {} + + model M2 { + ... Template<{}>; + } + + model M3 extends M1 {} + model M4 { + ... M1; + } + `, + ]); + + const hasUnknownMembers = { links: { hasUnknownMembers: true } }; + assertSymbol(sym, { + exports: { + M1: hasUnknownMembers, + M2: hasUnknownMembers, + M3: hasUnknownMembers, + M4: hasUnknownMembers, + }, + }); + }); + + it("binds members of templates", () => { + const sym = getGlobalSymbol([ + ` + model Template { + x: "x"; + } + `, + ]); + + assertSymbol(sym, { + exports: { + Template: { + members: { + x: { + flags: SymbolFlags.Member, + }, + }, + }, + }, + }); + }); + }); + + describe("resolution", () => { + it("resolves model members", () => { + const { "Foo.prop": prop } = getResolutions( + [ + ` + model Foo { + prop: "prop"; + } + `, + ], + "Foo.prop", + ); + assertSymbol(prop, { name: "prop", flags: SymbolFlags.Member }); + }); + + it("resolves model members from spread", () => { + const { "Foo.prop": prop } = getResolutions( + [ + ` + model Bar { + prop: "prop"; + } + + model Foo { + ... Bar; + } + `, + ], + "Foo.prop", + ); + assertSymbol(prop, { name: "prop", flags: SymbolFlags.Member }); + }); + + it("resolves model members from extends", () => { + const { "Foo.prop": prop, Bar } = getResolutions( + [ + ` + model Bar { + prop: "prop"; + } + + model Foo extends Bar {} + `, + ], + "Foo.prop", + "Bar", + ); + assertSymbol(prop, { name: "prop", flags: SymbolFlags.Member }); + ok(prop.finalSymbol!.parent === Bar.finalSymbol); + }); + + it("resolves model members from extends with unknown spreads to unknown not inherited member", () => { + const { "Foo.prop": prop } = getResolutions( + [ + ` + model Bar { + prop: "prop"; + } + + model Foo extends Bar { + ... Baz<{}>; + } + + model Baz { + ... T; + } + `, + ], + "Foo.prop", + ); + ok(prop.resolutionResult & ResolutionResultFlags.Unknown); + }); + + it("model members should be unknown with an unknown spread", () => { + const { "Foo.prop": prop } = getResolutions( + [ + ` + model Foo { + ... Baz<{}>; + } + + model Baz { + ... T; + } + + `, + ], + "Foo.prop", + ); + ok(prop.resolutionResult & ResolutionResultFlags.Unknown); + }); + + it("model members should be unknown with an unknown base class", () => { + const { "Foo.prop": prop } = getResolutions( + [ + ` + model Foo extends Baz<{}> { + } + + model Baz { + ... T; + } + + `, + ], + "Foo.prop", + ); + ok(prop.resolutionResult & ResolutionResultFlags.Unknown); + }); + + it("resolves model circular reference", () => { + const { Foo: model } = getResolutions( + [ + ` + model Foo { + prop: Foo; + } + `, + ], + "Foo", + ); + assertSymbol(model, { name: "Foo", flags: SymbolFlags.Declaration }); + }); + }); +}); + +describe("model expressions", () => { + describe("binding", () => { + it("binds members", () => { + const sym = getAliasedSymbol("M1", [ + ` + alias M1 = { + x: "x"; + } + `, + ]); + + assertSymbol(sym, { + members: { + x: { + flags: SymbolFlags.Member, + }, + }, + }); + }); + + it("binds spread members", () => { + const sym = getAliasedSymbol("M2", [ + ` + alias M1 { + x: "x"; + } + + alias M2 { + ... M1, + y: "y"; + } + `, + ]); + + assertSymbol(sym, { + members: { + x: { + flags: SymbolFlags.Member, + }, + y: { + flags: SymbolFlags.Member, + }, + }, + }); + }); + + it("binds spread members of templates", () => { + const sym = getAliasedSymbol("M2", [ + ` + alias M1 = { + x: "x"; + ... T; + } + + alias M2 = { + ... M1<{}>, + y: "y"; + } + `, + ]); + + assertSymbol(sym, { + members: { + x: { + flags: SymbolFlags.Member, + }, + y: { + flags: SymbolFlags.Member, + }, + }, + }); + }); + + it("binds spread members of templates with constraints", () => { + const sym = getAliasedSymbol("M2", [ + ` + alias M1 = { + x: "x"; + ... T; + } + + alias M2 { + ... M1<{}>, + y: "y"; + } + `, + ]); + + assertSymbol(sym, { + members: { + x: { flags: SymbolFlags.Member }, + y: { flags: SymbolFlags.Member }, + z: { flags: SymbolFlags.Member }, + }, + }); + }); + + it("binds members of templates", () => { + const sym = getAliasedSymbol("Template", [ + ` + alias Template = { + x: "x"; + } + `, + ]); + + assertSymbol(sym, { + members: { + x: { + flags: SymbolFlags.Member, + }, + }, + }); + }); + }); + + describe("resolution", () => { + it("resolves model members", () => { + const { "Foo.prop": prop } = getResolutions( + [ + ` + alias Foo = { + prop: "prop"; + } + `, + ], + "Foo.prop", + ); + assertSymbol(prop, { name: "prop", flags: SymbolFlags.Member }); + }); + + it("resolves model members from spread", () => { + const { "Foo.prop": prop } = getResolutions( + [ + ` + alias Bar = { + prop: "prop"; + } + + alias Foo = { + ... Bar; + } + `, + ], + "Foo.prop", + ); + assertSymbol(prop, { name: "prop", flags: SymbolFlags.Member }); + }); + + it("model members should be unknown with an unknown spread", () => { + const { "Foo.prop": prop } = getResolutions( + [ + ` + alias Foo = { + ... Baz<{}>; + } + + alias Baz = { + ... T; + } + + `, + ], + "Foo.prop", + ); + ok(prop.resolutionResult & ResolutionResultFlags.Unknown); + }); + + it("resolves model expression circular reference with alias", () => { + const { Foo: model } = getResolutions( + [ + ` + alias Foo = { + prop: Foo; + } + `, + ], + "Foo", + ); + assertSymbol(model, { name: "-" }); + }); + }); +}); + +describe("model properties", () => { + it("resolves meta properties", () => { + const { "Foo.prop::type": propType, Bar } = getResolutions( + [ + ` + model Foo { + prop: Bar; + } + model Bar { } + `, + ], + "Foo.prop::type", + "Bar", + ); + ok(propType.finalSymbol === Bar.finalSymbol, "should resolve to Bar"); + }); + + it("resolves meta properties of nested model types", () => { + const { "Foo.prop::type.nestedProp::type": propType, Bar } = getResolutions( + [ + ` + model Foo { + prop: { + nestedProp: Bar; + }; + } + model Bar { } + `, + ], + "Foo.prop::type.nestedProp::type", + "Bar", + ); + ok(propType.finalSymbol === Bar.finalSymbol, "should resolve to Bar"); + }); + + it("resolves meta properties of aliased model properties", () => { + const { "FooProp::type": propType, Bar } = getResolutions( + [ + ` + model Foo { + prop: Bar; + } + model Bar { } + alias FooProp = Foo.prop; + `, + ], + "FooProp::type", + "Bar", + ); + ok(propType.finalSymbol === Bar.finalSymbol, "should resolve to Bar"); + }); +}); +describe("interfaces", () => { + describe("binding", () => { + it("binds interface members from extends", () => { + const sym = getGlobalSymbol([ + ` + interface Bar { + x(): void; + } + + interface Baz { + y(): void; + } + interface Foo extends Bar, Baz {} + `, + ]); + + assertSymbol(sym, { + exports: { + Foo: { + members: { + x: { + flags: SymbolFlags.Member | SymbolFlags.Operation, + }, + y: { + flags: SymbolFlags.Member | SymbolFlags.Operation, + }, + }, + }, + }, + }); + }); + + it("binds interface members from extends templates", () => { + const sym = getGlobalSymbol([ + ` + interface Template { + x(): void; + } + + interface Foo extends Template<{}> { + + } + `, + ]); + + assertSymbol(sym, { + exports: { + Foo: { + members: { + x: { + flags: SymbolFlags.Member | SymbolFlags.Operation, + }, + }, + }, + }, + }); + }); + + it("binds interface members from multiple extends templates", () => { + const sym = getGlobalSymbol([ + ` + interface Template1 { + x(): void; + } + + interface Template2 extends Template3 { + y(): void; + } + + interface Template3 { + z(): void; + } + + interface Foo extends Template1<{}>, Template2<{}> { + + } + `, + ]); + + assertSymbol(sym, { + exports: { + Foo: { + members: { + x: { + flags: SymbolFlags.Member | SymbolFlags.Operation, + }, + y: { + flags: SymbolFlags.Member | SymbolFlags.Operation, + }, + z: { + flags: SymbolFlags.Member | SymbolFlags.Operation, + }, + }, + }, + }, + }); + }); + }); + describe("resolution", () => { + it("resolves interface members", () => { + const { "Foo.x": x } = getResolutions( + [ + ` + interface Foo { + x(): void; + } + `, + ], + "Foo.x", + ); + assertSymbol(x, { name: "x", flags: SymbolFlags.Member | SymbolFlags.Operation }); + }); + + it("resolves interface members from templates", () => { + const { "Foo.x": x, "Template.x": tx } = getResolutions( + [ + ` + interface Template { + x(): void; + } + interface Foo extends Template<{}> {} + `, + ], + "Foo.x", + "Template.x", + ); + + assertSymbol(x, { name: "x", flags: SymbolFlags.Member | SymbolFlags.Operation }); + assertSymbol(tx, { name: "x", flags: SymbolFlags.Member | SymbolFlags.Operation }); + }); + }); +}); + +describe("operations", () => { + describe("resolution", () => { + it("resolves parameters meta property", () => { + const { "Foo::parameters.x::type": x, Bar: Bar } = getResolutions( + [ + ` + model Bar { } + op Foo(x: Bar): void; + `, + ], + "Foo::parameters.x::type", + "Bar", + ); + + ok(x.finalSymbol === Bar.finalSymbol, "Should resolve to Bar"); + }); + + it("resolves parameters meta property with is ops", () => { + const { "Baz::parameters.x::type": x, Bar: Bar } = getResolutions( + [ + ` + model Bar { } + op Foo(x: Bar): void; + op Baz is Foo; + `, + ], + "Baz::parameters.x::type", + "Bar", + ); + + ok(x.finalSymbol === Bar.finalSymbol, "Should resolve to Bar"); + }); + }); +}); + +describe("accessing non members resolve to NotFound", () => { + it("accessing property on ModelProperty", () => { + const { "Foo.bar.doesNotExists": x } = getResolutions( + [ + ` + model Foo { bar: string } + `, + ], + "Foo.bar.doesNotExists", + ); + + ok(x.resolutionResult & ResolutionResultFlags.NotFound); + }); + + it("accessing property on ModelProperty of operation parameters", () => { + const { "test::parameters.param.doesNotExists": x } = getResolutions( + [ + ` + op test(param: string): void; + `, + ], + "test::parameters.param.doesNotExists", + ); + + ok(x.resolutionResult & ResolutionResultFlags.NotFound); + }); + + it("accessing property on ModelProperty of operation parameters template", () => { + const { "test::parameters.param.doesNotExists": x } = getResolutions( + [ + ` + op template(param: T): void; + op test is template; + `, + ], + "test::parameters.param.doesNotExists", + ); + + ok(x.resolutionResult & ResolutionResultFlags.NotFound); + }); +}); +describe("enums", () => { + describe("binding", () => { + it("binds enum members from spread", () => { + const sym = getGlobalSymbol([ + ` + enum A { + x; + ... B; + } + + enum B{ + y: 2; + } + `, + ]); + + assertSymbol(sym, { + exports: { + A: { + members: { + x: { + flags: SymbolFlags.Member, + }, + y: { + flags: SymbolFlags.Member, + }, + }, + }, + B: { + members: { + y: { + flags: SymbolFlags.Member, + }, + }, + }, + }, + }); + }); + }); + + describe("resolution", () => { + it("resolves enum members", () => { + const { "Foo.x": x } = getResolutions( + [ + ` + enum Foo { + x; + } + `, + ], + "Foo.x", + ); + assertSymbol(x, { name: "x", flags: SymbolFlags.Member }); + }); + + it("resolves enum members from spread", () => { + const { "Foo.x": x, "Bar.y": y } = getResolutions( + [ + ` + enum Foo { + x; + } + enum Bar { + ... Foo; + y; + } + `, + ], + "Foo.x", + "Bar.y", + ); + assertSymbol(x, { name: "x", flags: SymbolFlags.Member }); + assertSymbol(y, { name: "y", flags: SymbolFlags.Member }); + }); + }); +}); + +describe("unions", () => { + describe("binding", () => { + it("binds union members", () => { + const sym = getGlobalSymbol([ + ` + union Foo { + x: "x"; + } + `, + ]); + + assertSymbol(sym, { + exports: { + Foo: { + members: { + x: { + flags: SymbolFlags.Member, + }, + }, + }, + }, + }); + }); + }); + describe("resolution", () => { + it("resolves named union variants", () => { + const { "Foo.x": x } = getResolutions( + [ + ` + union Foo { + x: "x"; + } + `, + ], + "Foo.x", + ); + assertSymbol(x, { name: "x", flags: SymbolFlags.Member }); + }); + }); +}); + +describe("namespaces", () => { + describe("binding", () => { + it("merges across the same file", () => { + const sym = getGlobalSymbol([ + `namespace Foo { + model M { } + } + namespace Foo { + model N { } + }`, + ]); + + assertSymbol(sym, { + exports: { + Foo: { + flags: SymbolFlags.Namespace, + exports: { + M: { + flags: SymbolFlags.Model, + }, + N: { + flags: SymbolFlags.Model, + }, + }, + }, + }, + }); + }); + + it("merges across files", () => { + const sym = getGlobalSymbol([ + `namespace Foo { + model M { } + }`, + `namespace Foo { + model N { } + }`, + ]); + + assertSymbol(sym, { + exports: { + Foo: { + flags: SymbolFlags.Namespace, + exports: { + M: { + flags: SymbolFlags.Model, + }, + N: { + flags: SymbolFlags.Model, + }, + }, + }, + }, + }); + }); + }); + + describe("resolution", () => { + it("resolves namespace members", () => { + const { "Foo.Bar.M": M, "Foo.N": N } = getResolutions( + [ + `namespace Foo { + namespace Bar { + model M {} + } + model N { } + } + `, + ], + "Foo.Bar.M", + "Foo.N", + ); + assertSymbol(M, { name: "M" }); + assertSymbol(N, { name: "N" }); + }); + }); +}); + +describe("js namespaces", () => { + describe("binding", () => { + it("merges across files", () => { + const sym = getGlobalSymbol([ + { + $decorators: { Foo: { testDec1: () => null } }, + }, + { + $decorators: { "Foo.Bar": { testDec2: () => null } }, + }, + { + $decorators: { "Foo.Bar": { testDec3: () => null } }, + }, + ]); + + assertSymbol(sym, { + exports: { + Foo: { + flags: SymbolFlags.Namespace, + exports: { + "@testDec1": { + flags: SymbolFlags.Decorator, + }, + Bar: { + flags: SymbolFlags.Namespace, + exports: { + "@testDec2": { + flags: SymbolFlags.Decorator, + }, + "@testDec3": { + flags: SymbolFlags.Decorator, + }, + }, + }, + }, + }, + }, + }); + }); + it("merges with tsp namespace", () => { + const sym = getGlobalSymbol([ + ` + namespace Foo { + model FooModel {} + + namespace Bar { + model BarModel {} + } + } + `, + { + $decorators: { Foo: { testDec1: () => null } }, + }, + { + $decorators: { "Foo.Bar": { testDec2: () => null } }, + }, + ]); + + assertSymbol(sym, { + exports: { + Foo: { + flags: SymbolFlags.Namespace, + exports: { + "@testDec1": { + flags: SymbolFlags.Decorator, + }, + FooModel: { + flags: SymbolFlags.Model, + }, + Bar: { + flags: SymbolFlags.Namespace, + exports: { + "@testDec2": { + flags: SymbolFlags.Decorator, + }, + BarModel: { + flags: SymbolFlags.Model, + }, + }, + }, + }, + }, + }, + }); + }); + }); +}); + +describe("aliases", () => { + describe("binding", () => { + it("binds aliases to symbols", () => { + // this is just handled by the binder, but verifying here. + const sym = getGlobalSymbol([ + `namespace Foo { + model M { } + } + namespace Bar { + alias M = Foo.M; + }`, + ]); + + assertSymbol(sym, { + exports: { + Foo: { + flags: SymbolFlags.Namespace, + exports: { + M: { + flags: SymbolFlags.Model, + }, + }, + }, + Bar: { + flags: SymbolFlags.Namespace, + exports: { + M: { + flags: SymbolFlags.Alias, + }, + }, + }, + }, + }); + }); + it("resolves aliases to their aliased symbol", () => { + const sym = getGlobalSymbol([ + ` + model M1 { + x: "x"; + } + + alias M2 = M1; + alias M3 = M2; + `, + ]); + + const m1Sym = sym.exports?.get("M1"); + assertSymbol(sym, { + exports: { + M1: { + members: { + x: { + flags: SymbolFlags.Member, + }, + }, + }, + M2: { + flags: SymbolFlags.Alias, + links: { + aliasedSymbol: m1Sym, + }, + }, + M3: { + flags: SymbolFlags.Alias, + links: { + aliasedSymbol: m1Sym, + }, + }, + }, + }); + }); + }); + + describe("resolution", () => { + it("resolves aliases", () => { + const { + "Foo.Bar.M": M, + "Baz.AliasM": AliasM, + "Baz.AliasAliasM": AliasAliasM, + } = getResolutions( + [ + `namespace Foo { + namespace Bar { + model M {} + } + } + namespace Baz { + alias AliasM = Foo.Bar.M; + alias AliasAliasM = AliasM; + } + `, + ], + "Foo.Bar.M", + "Baz.AliasM", + "Baz.AliasAliasM", + ); + assertSymbol(M, { name: "M", flags: SymbolFlags.Model }); + assertSymbol(AliasM, { name: "M", flags: SymbolFlags.Model }); + assertSymbol(AliasAliasM, { name: "M", flags: SymbolFlags.Model }); + }); + + it("resolves known members of aliased things with members", () => { + const { + "Foo.x": x, + "Bar.x": aliasX, + Baz: aliasAliasX, + } = getResolutions( + [ + ` + model Foo { x: "hi" } + alias Bar = Foo; + alias Baz = Bar.x; + `, + ], + "Foo.x", + "Bar.x", + "Baz", + ); + const xDescriptor = { name: "x", flags: SymbolFlags.Member }; + assertSymbol(x, xDescriptor); + assertSymbol(aliasX, xDescriptor); + assertSymbol(aliasAliasX, xDescriptor); + }); + + it("resolves unknown members of aliased things with members", () => { + const { Baz: yResult } = getResolutions( + [ + ` + model Template { ... T }; + model Foo { x: "hi", ... Template<{}> } + alias Bar = Foo; + alias Baz = Bar.y; + `, + ], + "Foo.x", + "Bar.x", + "Baz", + ); + ok(yResult.resolutionResult & ResolutionResultFlags.Unknown, "Baz alias should be unknown"); + }); + }); +}); + +describe("usings", () => { + describe("binding", () => { + it("binds usings to locals", () => { + const sym = getGlobalSymbol(["namespace Foo { model M { }} namespace Bar { using Foo; }"]); + assertSymbol(sym, { + exports: { + Foo: { + flags: SymbolFlags.Namespace, + }, + Bar: { + flags: SymbolFlags.Namespace, + locals: { + M: { + flags: SymbolFlags.Model | SymbolFlags.Using, + }, + }, + }, + }, + }); + }); + }); + + describe("resolution", () => { + it("resolves usings", () => { + const sources = [ + ` + namespace Foo { + model M { } + } + + namespace Bar { + using Foo; + ┆ + } + `, + ]; + + const { M } = getResolutions(sources, "M"); + assertSymbol(M.finalSymbol, { name: "M", flags: SymbolFlags.Using }); + }); + }); +}); + +type StringTuplesToSymbolRecord = { + [K in T[number]]: ResolutionResult; +}; + +function getResolutions( + sources: string[], + ...names: T +): StringTuplesToSymbolRecord { + let index = 0; + const symbols = {} as any; + const referenceNodes: TypeReferenceNode[] = []; + + for (let source of sources) { + const explicitCursorPos = source.indexOf("┆"); + const cursorPos = explicitCursorPos >= 0 ? explicitCursorPos : source.length - 1; + const aliasCodes = names.map((name) => `alias test${name.replace(/\.|(::)/g, "")} = ${name};`); + const aliasOffsets: number[] = []; + let prevOffset = 0; + for (let i = 0; i < names.length; i++) { + aliasOffsets.push(prevOffset + aliasCodes[i].length - 1); + prevOffset += aliasCodes[i].length; + } + source = source.slice(0, cursorPos) + aliasCodes.join("") + source.slice(cursorPos + 1); + const sf = parse(source); + program.sourceFiles.set(String(index++), sf); + binder.bindSourceFile(sf); + + for (let i = 0; i < names.length; i++) { + const node = getNodeAtPosition(sf, cursorPos + aliasOffsets[i]); + referenceNodes.push(getParentTypeRef(node)); + } + } + + resolver.resolveProgram(); + for (let i = 0; i < names.length; i++) { + const nodeLinks = resolver.getNodeLinks(referenceNodes[i]); + validateReferenceNodes(referenceNodes[0]); + + symbols[names[i]] = nodeLinks; + } + return symbols; +} + +function validateReferenceNodes(node: TypeReferenceNode) { + const base = resolver.getNodeLinks(node); + if (!base.resolutionResult) { + throw new Error(`Reference ${typeReferenceToString(node)} hasn't been resolved`); + } + validate(node.target); + + function validate(sub: IdentifierNode | MemberExpressionNode) { + const subLinks = resolver.getNodeLinks(node); + expect(subLinks.resolutionResult).toBe(base.resolutionResult); + if (sub.kind === SyntaxKind.MemberExpression) { + validate(sub.id); + } + } +} + +function getParentTypeRef(node: Node | undefined) { + if (!node) { + throw new Error("Can't find parent of undefined node."); + } + if (node.kind !== SyntaxKind.MemberExpression && node.kind !== SyntaxKind.Identifier) { + throw new Error(`Can't find parent of non-reference node. ${SyntaxKind[node.kind]}`); + } + + if (!node.parent) { + throw new Error("can't find parent."); + } + + if (node.parent.kind === SyntaxKind.TypeReference) { + return node.parent; + } + + return getParentTypeRef(node.parent); +} + +function resolve(sources: (string | Record)[]): NameResolver { + let index = 0; + for (const source of sources) { + if (typeof source === "string") { + const sf = parse(source); + program.sourceFiles.set(String(index++), sf); + binder.bindSourceFile(sf); + } else { + const sf: JsSourceFileNode = createJsSourceFile(source); + program.jsSourceFiles.set(String(index++), sf); + binder.bindJsSourceFile(sf); + } + } + + resolver.resolveProgram(); + return resolver; +} + +function getGlobalSymbol(sources: (string | Record)[]): Sym { + const resolver = resolve(sources); + return resolver.symbols.global; +} +function getAliasedSymbol(name: string, sources: (string | Record)[]): Sym { + const global = getGlobalSymbol(sources); + const aliasSym = global.exports?.get(name); + ok(aliasSym, `Expected ${name} to be available in global symbol exports`); + const aliasedSym = resolver.getSymbolLinks(aliasSym).aliasedSymbol; + ok(aliasedSym, "Expected alias sym to have resolved"); + return aliasedSym; +} + +function assertSymbol( + sym: ResolutionResult | Sym | undefined, + record: SymbolDescriptor = {}, +): asserts sym is Required | Sym { + if (sym && "resolutionResult" in sym) { + sym = sym.finalSymbol; + } + if (!sym) { + throw new Error(`Symbol not found.`); + } + if (record.flags) { + ok( + sym.flags & record.flags, + `Expected symbol ${sym.name} to have flags ${inspectSymbolFlags( + record.flags, + )} but got ${inspectSymbolFlags(sym.flags)}`, + ); + } + + if (record.nodeFlags) { + ok( + sym.declarations[0].flags & record.nodeFlags, + `Expected symbol ${sym.name} to have node flags ${record.nodeFlags} but got ${sym.declarations[0].flags}`, + ); + } + + if (record.name) { + strictEqual(sym.name, record.name); + } + + if (record.exports) { + ok(sym.exports, `Expected symbol ${sym.name} to have exports`); + const exports = resolver.getAugmentedSymbolTable(sym.exports); + + for (const [name, descriptor] of Object.entries(record.exports)) { + const exportSym = exports.get(name); + ok( + exportSym, + `Expected symbol ${sym.name} to have export ${name} but only has ${[...exports.keys()].join(", ")}`, + ); + assertSymbol(exportSym, descriptor); + } + } + + if (record.locals) { + const node = sym.declarations[0] as any; + ok(node.locals, `Expected symbol ${sym.name} to have locals`); + const locals = resolver.getAugmentedSymbolTable(node.locals); + + for (const [name, descriptor] of Object.entries(record.locals)) { + const localSym = locals.get(name); + ok(localSym, `Expected symbol ${sym.name} to have local ${name}`); + assertSymbol(localSym, descriptor); + } + } + + if (record.members) { + ok(sym.members, `Expected symbol ${sym.name} to have exports`); + const members = resolver.getAugmentedSymbolTable(sym.members); + + for (const [name, descriptor] of Object.entries(record.members)) { + const exportSym = members.get(name); + ok(exportSym, `Expected symbol ${sym.name} to have member ${name}`); + assertSymbol(exportSym, descriptor); + } + } + + if (record.links) { + const links = resolver.getSymbolLinks(sym); + for (const [key, value] of Object.entries(record.links) as [keyof SymbolLinks, any][]) { + if (value) { + ok(links[key], `Expected symbol ${sym.name} to have link ${key}`); + strictEqual(links[key], value); + } + } + } +} + +interface SymbolDescriptor { + name?: string; + flags?: SymbolFlags; + nodeFlags?: NodeFlags; + locals?: Record; + exports?: Record; + members?: Record; + links?: SymbolLinks; +} + +function createProgramShim(): Program { + return { + tracer: createTracer(createLogger({ sink: { log: () => {} } })), + reportDuplicateSymbols() {}, + onValidate() {}, + sourceFiles: new Map(), + jsSourceFiles: new Map(), + } as any; +} + +function createJsSourceFile(exports: any): JsSourceFileNode { + const file = createSourceFile("", "path"); + return { + kind: SyntaxKind.JsSourceFile, + id: { + kind: SyntaxKind.Identifier, + sv: "", + pos: 0, + end: 0, + symbol: undefined!, + flags: NodeFlags.Synthetic, + }, + esmExports: exports, + file, + namespaceSymbols: [], + symbol: undefined!, + pos: 0, + end: 0, + flags: NodeFlags.None, + }; +} diff --git a/packages/openapi3/test/tsp-openapi3/parameters.test.ts b/packages/openapi3/test/tsp-openapi3/parameters.test.ts index a1a6d38d36..c8f54871a5 100644 --- a/packages/openapi3/test/tsp-openapi3/parameters.test.ts +++ b/packages/openapi3/test/tsp-openapi3/parameters.test.ts @@ -196,6 +196,7 @@ describe("converts top-level parameters", () => { assert(Foo, "Foo model not found"); expect(Foo.properties.size).toBe(1); const foo = Foo.properties.get("foo"); + ok(foo); expect(foo).toMatchObject({ optional: true, type: { kind: "Scalar", name: "string" }, diff --git a/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts b/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts index 6ae44525cb..7be8562478 100644 --- a/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts +++ b/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts @@ -2,8 +2,10 @@ import { CompilerHost, Decorator, Diagnostic, + Namespace, type PackageJson, Program, + SemanticNodeListener, type SourceLocation, compile, createDiagnosticCollector, @@ -12,6 +14,7 @@ import { getTypeName, joinPaths, navigateProgram, + navigateTypesInNamespace, resolvePath, } from "@typespec/compiler"; import prettier from "prettier"; @@ -80,7 +83,9 @@ export async function generateExternSignatureForExports( } catch (e) {} await host.mkdirp(outDir); - const files = await generateExternDecorators(program, pkgJson.name, prettierConfig ?? undefined); + const files = await generateExternDecorators(program, pkgJson.name, { + prettierConfig: prettierConfig ?? undefined, + }); for (const [name, content] of Object.entries(files)) { await host.writeFile(resolvePath(outDir, name), content); } @@ -92,14 +97,19 @@ async function readPackageJson(host: CompilerHost, libraryPath: string): Promise return JSON.parse(file.text); } +export interface GenerateExternDecoratorOptions { + /** Render those namespaces only(exclude sub namespaces as well). By default it will include all namespaces. */ + readonly namespaces?: Namespace[]; + readonly prettierConfig?: prettier.Options; +} export async function generateExternDecorators( program: Program, packageName: string, - prettierConfig?: prettier.Options, + options?: GenerateExternDecoratorOptions, ): Promise> { const decorators = new Map(); - navigateProgram(program, { + const listener: SemanticNodeListener = { decorator(dec) { if ( packageName !== "@typespec/compiler" && @@ -115,12 +125,19 @@ export async function generateExternDecorators( } decoratorForNamespace.push(resolveDecoratorSignature(dec)); }, - }); + }; + if (options?.namespaces) { + for (const namespace of options.namespaces) { + navigateTypesInNamespace(namespace, listener, { skipSubNamespaces: true }); + } + } else { + navigateProgram(program, listener); + } function format(value: string) { try { const formatted = prettier.format(value, { - ...prettierConfig, + ...options?.prettierConfig, parser: "typescript", }); return formatted; diff --git a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts index bb8c3cde85..7a64e7a5ae 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -24,8 +24,10 @@ async function generateDecoratorSignatures(code: string) { ); const result = await generateExternDecorators(host.program, "test-lib", { - printWidth: 160, // So there is no inconsistency in the .each test with different parameter length - plugins: [], + prettierConfig: { + printWidth: 160, // So there is no inconsistency in the .each test with different parameter length + plugins: [], + }, }); return result["__global__.ts"]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47764429c3..3969f3596b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -362,9 +362,6 @@ importers: tmlanguage-generator: specifier: workspace:~ version: link:../tmlanguage-generator - ts-node: - specifier: ~10.9.2 - version: 10.9.2(@swc/core@1.7.35)(@types/node@22.7.5)(typescript@5.6.3) typescript: specifier: ~5.6.3 version: 5.6.3 @@ -5351,87 +5348,12 @@ packages: peerDependencies: storybook: ^8.3.5 - '@swc/core-darwin-arm64@1.7.35': - resolution: {integrity: sha512-BQSSozVxjxS+SVQz6e3GC/+OBWGIK3jfe52pWdANmycdjF3ch7lrCKTHTU7eHwyoJ96mofszPf5AsiVJF34Fwg==} - engines: {node: '>=10'} - cpu: [arm64] - os: [darwin] - - '@swc/core-darwin-x64@1.7.35': - resolution: {integrity: sha512-44TYdKN/EWtkU88foXR7IGki9JzhEJzaFOoPevfi9Xe7hjAD/x2+AJOWWqQNzDPMz9+QewLdUVLyR6s5okRgtg==} - engines: {node: '>=10'} - cpu: [x64] - os: [darwin] - - '@swc/core-linux-arm-gnueabihf@1.7.35': - resolution: {integrity: sha512-ccfA5h3zxwioD+/z/AmYtkwtKz9m4rWTV7RoHq6Jfsb0cXHrd6tbcvgqRWXra1kASlE+cDWsMtEZygs9dJRtUQ==} - engines: {node: '>=10'} - cpu: [arm] - os: [linux] - - '@swc/core-linux-arm64-gnu@1.7.35': - resolution: {integrity: sha512-hx65Qz+G4iG/IVtxJKewC5SJdki8PAPFGl6gC/57Jb0+jA4BIoGLD/J3Q3rCPeoHfdqpkCYpahtyUq8CKx41Jg==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - - '@swc/core-linux-arm64-musl@1.7.35': - resolution: {integrity: sha512-kL6tQL9No7UEoEvDRuPxzPTpxrvbwYteNRbdChSSP74j13/55G2/2hLmult5yFFaWuyoyU/2lvzjRL/i8OLZxg==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - - '@swc/core-linux-x64-gnu@1.7.35': - resolution: {integrity: sha512-Ke4rcLQSwCQ2LHdJX1FtnqmYNQ3IX6BddKlUtS7mcK13IHkQzZWp0Dcu6MgNA3twzb/dBpKX5GLy07XdGgfmyw==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - - '@swc/core-linux-x64-musl@1.7.35': - resolution: {integrity: sha512-T30tlLnz0kYyDFyO5RQF5EQ4ENjW9+b56hEGgFUYmfhFhGA4E4V67iEx7KIG4u0whdPG7oy3qjyyIeTb7nElEw==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - - '@swc/core-win32-arm64-msvc@1.7.35': - resolution: {integrity: sha512-CfM/k8mvtuMyX+okRhemfLt784PLS0KF7Q9djA8/Dtavk0L5Ghnq+XsGltO3d8B8+XZ7YOITsB14CrjehzeHsg==} - engines: {node: '>=10'} - cpu: [arm64] - os: [win32] - - '@swc/core-win32-ia32-msvc@1.7.35': - resolution: {integrity: sha512-ATB3uuH8j/RmS64EXQZJSbo2WXfRNpTnQszHME/sGaexsuxeijrp3DTYSFAA3R2Bu6HbIIX6jempe1Au8I3j+A==} - engines: {node: '>=10'} - cpu: [ia32] - os: [win32] - - '@swc/core-win32-x64-msvc@1.7.35': - resolution: {integrity: sha512-iDGfQO1571NqWUXtLYDhwIELA/wadH42ioGn+J9R336nWx40YICzy9UQyslWRhqzhQ5kT+QXAW/MoCWc058N6Q==} - engines: {node: '>=10'} - cpu: [x64] - os: [win32] - - '@swc/core@1.7.35': - resolution: {integrity: sha512-3cUteCTbr2r5jqfgx0r091sfq5Mgh6F1SQh8XAOnSvtKzwv2bC31mvBHVAieD1uPa2kHJhLav20DQgXOhpEitw==} - engines: {node: '>=10'} - peerDependencies: - '@swc/helpers': '*' - peerDependenciesMeta: - '@swc/helpers': - optional: true - - '@swc/counter@0.1.3': - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.13': resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} '@swc/helpers@0.5.8': resolution: {integrity: sha512-lruDGw3pnfM3wmZHeW7JuhkGQaJjPyiKjxeGhdmfoOT53Ic9qb5JLDNaK2HUdl1zLDeX28H221UvKjfdvSLVMg==} - '@swc/types@0.1.13': - resolution: {integrity: sha512-JL7eeCk6zWCbiYQg2xQSdLXQJl8Qoc9rXmG2cEKvHe3CKwMHwHGpfOb8frzNLmbycOo6I51qxnLnn9ESf4I20Q==} - '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -6027,11 +5949,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.11.3: - resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.12.1: resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} engines: {node: '>=0.4.0'} @@ -13560,6 +13477,7 @@ snapshots: '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 + optional: true '@ctrl/tinycolor@4.1.0': {} @@ -15281,6 +15199,7 @@ snapshots: dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + optional: true '@jsdevtools/ono@7.1.3': {} @@ -16457,56 +16376,6 @@ snapshots: dependencies: storybook: 8.3.5 - '@swc/core-darwin-arm64@1.7.35': - optional: true - - '@swc/core-darwin-x64@1.7.35': - optional: true - - '@swc/core-linux-arm-gnueabihf@1.7.35': - optional: true - - '@swc/core-linux-arm64-gnu@1.7.35': - optional: true - - '@swc/core-linux-arm64-musl@1.7.35': - optional: true - - '@swc/core-linux-x64-gnu@1.7.35': - optional: true - - '@swc/core-linux-x64-musl@1.7.35': - optional: true - - '@swc/core-win32-arm64-msvc@1.7.35': - optional: true - - '@swc/core-win32-ia32-msvc@1.7.35': - optional: true - - '@swc/core-win32-x64-msvc@1.7.35': - optional: true - - '@swc/core@1.7.35': - dependencies: - '@swc/counter': 0.1.3 - '@swc/types': 0.1.13 - optionalDependencies: - '@swc/core-darwin-arm64': 1.7.35 - '@swc/core-darwin-x64': 1.7.35 - '@swc/core-linux-arm-gnueabihf': 1.7.35 - '@swc/core-linux-arm64-gnu': 1.7.35 - '@swc/core-linux-arm64-musl': 1.7.35 - '@swc/core-linux-x64-gnu': 1.7.35 - '@swc/core-linux-x64-musl': 1.7.35 - '@swc/core-win32-arm64-msvc': 1.7.35 - '@swc/core-win32-ia32-msvc': 1.7.35 - '@swc/core-win32-x64-msvc': 1.7.35 - optional: true - - '@swc/counter@0.1.3': - optional: true - '@swc/helpers@0.5.13': dependencies: tslib: 2.7.0 @@ -16515,11 +16384,6 @@ snapshots: dependencies: tslib: 2.6.2 - '@swc/types@0.1.13': - dependencies: - '@swc/counter': 0.1.3 - optional: true - '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.25.7 @@ -16557,13 +16421,17 @@ snapshots: '@tootallnate/once@2.0.0': {} - '@tsconfig/node10@1.0.11': {} + '@tsconfig/node10@1.0.11': + optional: true - '@tsconfig/node12@1.0.11': {} + '@tsconfig/node12@1.0.11': + optional: true - '@tsconfig/node14@1.0.3': {} + '@tsconfig/node14@1.0.3': + optional: true - '@tsconfig/node16@1.0.4': {} + '@tsconfig/node16@1.0.4': + optional: true '@tufjs/canonical-json@2.0.0': {} @@ -17288,12 +17156,11 @@ snapshots: acorn-walk@7.2.0: {} - acorn-walk@8.3.2: {} + acorn-walk@8.3.2: + optional: true acorn@7.4.1: {} - acorn@8.11.3: {} - acorn@8.12.1: {} acorn@8.14.0: {} @@ -17429,7 +17296,8 @@ snapshots: archy@1.0.0: {} - arg@4.1.3: {} + arg@4.1.3: + optional: true arg@5.0.2: {} @@ -18256,7 +18124,8 @@ snapshots: optionalDependencies: typescript: 5.6.3 - create-require@1.1.1: {} + create-require@1.1.1: + optional: true create-storybook@8.3.5: dependencies: @@ -18752,7 +18621,8 @@ snapshots: didyoumean@1.2.2: {} - diff@4.0.2: {} + diff@4.0.2: + optional: true diff@5.2.0: {} @@ -20870,7 +20740,8 @@ snapshots: dependencies: semver: 7.6.3 - make-error@1.3.6: {} + make-error@1.3.6: + optional: true make-fetch-happen@13.0.0: dependencies: @@ -22169,7 +22040,7 @@ snapshots: yaml: 2.5.1 optionalDependencies: postcss: 8.4.47 - ts-node: 10.9.2(@swc/core@1.7.35)(@types/node@22.7.5)(typescript@5.6.3) + ts-node: 10.9.2(@types/node@22.7.5)(typescript@5.6.3) postcss-nested@6.2.0(postcss@8.4.47): dependencies: @@ -23514,7 +23385,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@swc/core@1.7.35)(@types/node@22.7.5)(typescript@5.6.3): + ts-node@10.9.2(@types/node@22.7.5)(typescript@5.6.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -23522,7 +23393,7 @@ snapshots: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 '@types/node': 22.7.5 - acorn: 8.11.3 + acorn: 8.14.0 acorn-walk: 8.3.2 arg: 4.1.3 create-require: 1.1.1 @@ -23531,8 +23402,7 @@ snapshots: typescript: 5.6.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - optionalDependencies: - '@swc/core': 1.7.35 + optional: true ts-toolbelt@9.6.0: {} @@ -23851,7 +23721,8 @@ snapshots: uuid@9.0.1: {} - v8-compile-cache-lib@3.0.1: {} + v8-compile-cache-lib@3.0.1: + optional: true v8-to-istanbul@9.2.0: dependencies: @@ -24408,7 +24279,8 @@ snapshots: ylru@1.4.0: {} - yn@3.1.1: {} + yn@3.1.1: + optional: true yocto-queue@0.1.0: {}