From 00a32d7d20a6b68800e185404cad435fe3710e57 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Tue, 3 Feb 2026 13:18:51 -0500 Subject: [PATCH 1/8] Refactored mapper into checker context. --- packages/compiler/src/core/checker.ts | 847 ++++++++++++++------------ 1 file changed, 454 insertions(+), 393 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index fd8563ff39c..aeda9b19eb3 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -165,6 +165,90 @@ import { export type CreateTypeProps = Omit; +/** + * Context passed through type checking operations. + * + * Check contexts are immutable. + */ +interface CheckContext< + Options = {}, + Mapper extends TypeMapper | undefined = TypeMapper | undefined, +> { + /** The type mapper, if any. */ + mapper: Mapper; + + /** The set of active checking flags. */ + flags: CheckFlags; + + /** + * Options for the checker operation. + */ + options: Options; + + /** Derives a new CheckContext with the given flags. */ + withFlags(flags: CheckFlags): CheckContext; + + /** Derives a new CheckContext with the given mapper. */ + withMapper( + mapper: NewMapper, + ): CheckContext; + + /** Derives a new CheckContext with the given options. */ + withOptions( + options: NewOptions, + ): CheckContext; +} + +enum CheckFlags { + /** No flags set. */ + None = 0, + /** Currently checking within an uninstantiated template declaration. */ + InTemplateDeclaration = 1 << 0, +} + +const DEFAULT_CHECK_CONTEXT: CheckContext<{}, undefined> = Object.freeze({ + withFlags(flags: CheckFlags) { + return Object.freeze({ + withFlags: this.withFlags, + withMapper: this.withMapper, + withOptions: this.withOptions, + mapper: this.mapper, + flags, + options: this.options, + }); + }, + + withMapper(mapper: NewMapper) { + return Object.freeze({ + withFlags: this.withFlags, + withMapper: this.withMapper, + withOptions: this.withOptions, + mapper, + flags: this.flags, + options: this.options, + } as CheckContext<{}, NewMapper>); + }, + + withOptions(options: NewOptions) { + return Object.freeze({ + withFlags: this.withFlags, + withMapper: this.withMapper, + withOptions: this.withOptions, + mapper: this.mapper, + flags: this.flags, + options, + } as CheckContext); + }, + + mapper: undefined, + flags: CheckFlags.None, + options: {}, +}); + +const CheckContext = Object.freeze({ + default: DEFAULT_CHECK_CONTEXT, +} as const); + export interface Checker { /** @internal */ typePrototype: TypePrototype; @@ -471,9 +555,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const sym = typespecNamespaceBinding?.exports?.get(name); compilerAssert(sym, `Unexpected missing symbol to std type "${name}"`); if (sym.flags & SymbolFlags.Model) { - checkModelStatement(sym!.declarations[0] as any, undefined); + checkModelStatement(CheckContext.default, sym!.declarations[0] as any); } else { - checkScalar(sym.declarations[0] as any, undefined); + checkScalar(CheckContext.default, sym.declarations[0] as any); } const loadedType = stdTypes[name]; @@ -492,17 +576,17 @@ export function createChecker(program: Program, resolver: NameResolver): Checker * @param type Type * @param mapper Type mapper if in an template instantiation */ - function linkType(links: SymbolLinks, type: Type, mapper: TypeMapper | undefined) { - if (mapper === undefined) { + function linkType(ctx: CheckContext, links: SymbolLinks, type: Type) { + if (ctx.mapper === undefined) { links.declaredType = type; links.instantiations = new TypeInstantiationMap(); } else if (links.instantiations) { - links.instantiations.set(mapper.args, type); + links.instantiations.set(ctx.mapper.args, type); } } - function linkMemberType(links: SymbolLinks, type: Type, mapper: TypeMapper | undefined) { - if (mapper === undefined) { + function linkMemberType(ctx: CheckContext, links: SymbolLinks, type: Type) { + if (ctx.mapper === undefined) { links.declaredType = type; } } @@ -513,17 +597,17 @@ export function createChecker(program: Program, resolver: NameResolver): Checker * @param mapper Type mapper. * @returns Checked type for the given member symbol. */ - function checkMemberSym(sym: Sym, mapper: TypeMapper | undefined): Type { + function checkMemberSym(ctx: CheckContext, sym: Sym): Type { const symbolLinks = getSymbolLinks(sym); - const memberContainer = getTypeForNode(getSymNode(sym.parent!), mapper); + const memberContainer = getTypeForNode(getSymNode(sym.parent!), ctx.mapper); const type = symbolLinks.declaredType ?? symbolLinks.type; if (type) { return type; } else { return checkMember( + ctx, getSymNode(sym) as MemberNode, - mapper, memberContainer as MemberContainerType, )!; } @@ -537,21 +621,21 @@ export function createChecker(program: Program, resolver: NameResolver): Checker * @returns Checked member */ function checkMember( + ctx: CheckContext, node: MemberNode, - mapper: TypeMapper | undefined, containerType: MemberContainerType, ): Type { switch (node.kind) { case SyntaxKind.ModelProperty: - return checkModelProperty(node, mapper); + return checkModelProperty(ctx, node); case SyntaxKind.EnumMember: - return checkEnumMember(node, mapper, containerType as Enum); + return checkEnumMember(ctx, node, containerType as Enum); case SyntaxKind.OperationStatement: - return checkOperation(node, mapper, containerType as Interface); + return checkOperation(ctx, node, containerType as Interface); case SyntaxKind.UnionVariant: - return checkUnionVariant(node, mapper); + return checkUnionVariant(ctx, node); case SyntaxKind.ScalarConstructor: - return checkScalarConstructor(node, mapper, containerType as Scalar); + return checkScalarConstructor(ctx, node, containerType as Scalar); } } @@ -563,7 +647,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function getTypeForNode(node: Node, mapper?: TypeMapper): Type { - const entity = checkNode(node, mapper); + const ctx = CheckContext.default.withMapper(mapper); + const entity = checkNode(ctx, node); if (entity === null) { return errorType; } @@ -599,7 +684,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker mapper?: TypeMapper, constraint?: CheckValueConstraint, ): Value | null { - const initial = checkNode(node, mapper, constraint); + const ctx = CheckContext.default.withMapper(mapper); + const initial = checkNode(ctx, node, constraint); if (initial === null) { return null; } @@ -772,8 +858,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker mapper?: TypeMapper, constraint?: CheckConstraint | undefined, ): Type | Value | null { + const ctx = CheckContext.default.withMapper(mapper); const valueConstraint = extractValueOfConstraints(constraint); - const entity = checkNode(node, mapper, valueConstraint); + const entity = checkNode(ctx, node, valueConstraint); if (entity === null) { return entity; } else if (isType(entity)) { @@ -820,36 +907,36 @@ export function createChecker(program: Program, resolver: NameResolver): Checker * It is the job of of the consumer to decide if it should be a type or a value depending on the context. */ function checkNode( + ctx: CheckContext, node: Node, - mapper?: TypeMapper, valueConstraint?: CheckValueConstraint | undefined, ): Type | Value | IndeterminateEntity | null { switch (node.kind) { case SyntaxKind.ModelExpression: - return checkModel(node, mapper); + return checkModel(ctx, node); case SyntaxKind.ModelStatement: - return checkModel(node, mapper); + return checkModel(ctx, node); case SyntaxKind.ModelProperty: - return checkModelProperty(node, mapper); + return checkModelProperty(ctx, node); case SyntaxKind.ScalarStatement: - return checkScalar(node, mapper); + return checkScalar(ctx, node); case SyntaxKind.AliasStatement: - return checkAlias(node, mapper); + return checkAlias(ctx, node); case SyntaxKind.EnumStatement: - return checkEnum(node, mapper); + return checkEnum(ctx, node); case SyntaxKind.EnumMember: - return checkEnumMember(node, mapper); + return checkEnumMember(ctx, node); case SyntaxKind.InterfaceStatement: - return checkInterface(node, mapper); + return checkInterface(ctx, node); case SyntaxKind.UnionStatement: - return checkUnion(node, mapper); + return checkUnion(ctx, node); case SyntaxKind.UnionVariant: - return checkUnionVariant(node, mapper); + return checkUnionVariant(ctx, node); case SyntaxKind.NamespaceStatement: case SyntaxKind.JsNamespaceDeclaration: - return checkNamespace(node); + return checkNamespace(ctx, node); case SyntaxKind.OperationStatement: - return checkOperation(node, mapper); + return checkOperation(ctx, node); case SyntaxKind.NumericLiteral: return checkNumericLiteral(node); case SyntaxKind.BooleanLiteral: @@ -857,25 +944,25 @@ export function createChecker(program: Program, resolver: NameResolver): Checker case SyntaxKind.StringLiteral: return checkStringLiteral(node); case SyntaxKind.TupleExpression: - return checkTupleExpression(node, mapper); + return checkTupleExpression(ctx, node); case SyntaxKind.StringTemplateExpression: - return checkStringTemplateExpresion(node, mapper); + return checkStringTemplateExpresion(ctx, node); case SyntaxKind.ArrayExpression: - return checkArrayExpression(node, mapper); + return checkArrayExpression(ctx, node); case SyntaxKind.UnionExpression: - return checkUnionExpression(node, mapper); + return checkUnionExpression(ctx, node); case SyntaxKind.IntersectionExpression: - return checkIntersectionExpression(node, mapper); + return checkIntersectionExpression(ctx, node); case SyntaxKind.DecoratorDeclarationStatement: - return checkDecoratorDeclaration(node, mapper); + return checkDecoratorDeclaration(ctx, node); case SyntaxKind.FunctionDeclarationStatement: - return checkFunctionDeclaration(node, mapper); + return checkFunctionDeclaration(ctx, node); case SyntaxKind.TypeReference: - return checkTypeOrValueReference(node, mapper); + return checkTypeOrValueReference(ctx, node); case SyntaxKind.TemplateArgument: - return checkTemplateArgument(node, mapper); + return checkTemplateArgument(ctx, node); case SyntaxKind.TemplateParameterDeclaration: - return checkTemplateParameterDeclaration(node, mapper); + return checkTemplateParameterDeclaration(ctx, node); case SyntaxKind.VoidKeyword: return voidType; case SyntaxKind.NeverKeyword: @@ -883,19 +970,19 @@ export function createChecker(program: Program, resolver: NameResolver): Checker case SyntaxKind.UnknownKeyword: return unknownType; case SyntaxKind.ObjectLiteral: - return checkObjectValue(node, mapper, valueConstraint); + return checkObjectValue(ctx, node, valueConstraint); case SyntaxKind.ArrayLiteral: - return checkArrayValue(node, mapper, valueConstraint); + return checkArrayValue(ctx, node, valueConstraint); case SyntaxKind.ConstStatement: return checkConst(node); case SyntaxKind.CallExpression: - return checkCallExpression(node, mapper); + return checkCallExpression(ctx, node); case SyntaxKind.TypeOfExpression: - return checkTypeOfExpression(node, mapper); + return checkTypeOfExpression(ctx, node); case SyntaxKind.AugmentDecoratorStatement: - return checkAugmentDecorator(node); + return checkAugmentDecorator(ctx, node); case SyntaxKind.UsingStatement: - return checkUsings(node); + return checkUsings(ctx, node); default: return errorType; } @@ -941,20 +1028,16 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkTemplateParameterDeclaration( + ctx: CheckContext<{}, undefined>, node: TemplateParameterDeclarationNode, - mapper: undefined, ): TemplateParameter; function checkTemplateParameterDeclaration( + ctx: CheckContext, node: TemplateParameterDeclarationNode, - mapper: TypeMapper, ): Type | Value | IndeterminateEntity; function checkTemplateParameterDeclaration( + ctx: CheckContext, node: TemplateParameterDeclarationNode, - mapper: TypeMapper | undefined, - ): Type | Value | IndeterminateEntity; - function checkTemplateParameterDeclaration( - node: TemplateParameterDeclarationNode, - mapper: TypeMapper | undefined, ): Type | Value | IndeterminateEntity { const parentNode = node.parent!; const grandParentNode = parentNode.parent; @@ -965,7 +1048,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } if (pendingResolutions.has(getNodeSym(node), ResolutionKind.Constraint)) { - if (mapper === undefined) { + if (ctx.mapper === undefined) { reportCheckerDiagnostic( createDiagnostic({ code: "circular-constraint", @@ -998,13 +1081,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker if (node.constraint) { pendingResolutions.start(getNodeSym(node), ResolutionKind.Constraint); - type.constraint = getParamConstraintEntityForNode(node.constraint); + type.constraint = getParamConstraintEntityForNode(ctx, node.constraint); pendingResolutions.finish(getNodeSym(node), ResolutionKind.Constraint); } if (node.default) { // Set this to unknownType in case the default points back to the template itself causing failures type.default = unknownType; type.default = checkTemplateParameterDefault( + ctx, node.default, parentNode.templateParameters, index, @@ -1012,13 +1096,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ); } } - return mapper ? mapper.getMappedType(type) : type; + return ctx.mapper ? ctx.mapper.getMappedType(type) : type; } function getResolvedTypeParameterDefault( + ctx: CheckContext<{}, TypeMapper>, declaredType: TemplateParameter, node: TemplateParameterDeclarationNode, - mapper: TypeMapper, ): Type | Value | IndeterminateEntity | null | undefined { if (declaredType.default === undefined) { return undefined; @@ -1030,17 +1114,18 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return declaredType.default; } - return checkNode(node.default!, mapper); + return checkNode(ctx, node.default!); } function checkTemplateParameterDefault( + ctx: CheckContext, nodeDefault: Expression, templateParameters: readonly TemplateParameterDeclarationNode[], index: number, constraint: Entity | undefined, ): Type | Value | IndeterminateEntity { function visit(node: Node) { - const entity = checkNode(node); + const entity = checkNode(ctx, node); let hasError = false; if (entity !== null && "kind" in entity && entity.kind === "TemplateParameter") { for (let i = index; i < templateParameters.length; i++) { @@ -1079,16 +1164,16 @@ export function createChecker(program: Program, resolver: NameResolver): Checker * @returns Resolved type. */ function checkTypeReference( + ctx: CheckContext, node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, - mapper: TypeMapper | undefined, instantiateTemplate = true, ): Type { - const sym = resolveTypeReferenceSym(node, mapper); + const sym = resolveTypeReferenceSym(ctx, node); if (!sym) { return errorType; } - const type = checkTypeReferenceSymbol(sym, node, mapper, instantiateTemplate); + const type = checkTypeReferenceSymbol(ctx, sym, node, instantiateTemplate); return type; } @@ -1100,23 +1185,23 @@ export function createChecker(program: Program, resolver: NameResolver): Checker * @returns Resolved type. */ function checkTypeOrValueReference( + ctx: CheckContext, node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, - mapper: TypeMapper | undefined, instantiateTemplate = true, ): Type | Value | IndeterminateEntity { - const sym = resolveTypeReferenceSym(node, mapper); + const sym = resolveTypeReferenceSym(ctx, node); if (!sym) { return errorType; } - return checkTypeOrValueReferenceSymbol(sym, node, mapper, instantiateTemplate) ?? errorType; + return checkTypeOrValueReferenceSymbol(ctx, sym, node, instantiateTemplate) ?? errorType; } function checkTemplateArgument( + ctx: CheckContext, node: TemplateArgumentNode, - mapper: TypeMapper | undefined, ): Type | Value | IndeterminateEntity | null { - return checkNode(node.argument, mapper); + return checkNode(ctx, node.argument); } function resolveTypeOrValueReference( @@ -1125,7 +1210,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const oldDiagnosticHook = onCheckerDiagnostic; const diagnostics: Diagnostic[] = []; onCheckerDiagnostic = (x: Diagnostic) => diagnostics.push(x); - const entity = checkTypeOrValueReference(node, undefined, false); + const entity = checkTypeOrValueReference(CheckContext.default, node, false); onCheckerDiagnostic = oldDiagnosticHook; return [entity === errorType ? undefined : entity, diagnostics]; } @@ -1136,7 +1221,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const oldDiagnosticHook = onCheckerDiagnostic; const diagnostics: Diagnostic[] = []; onCheckerDiagnostic = (x: Diagnostic) => diagnostics.push(x); - const type = checkTypeReference(node, undefined, false); + const type = checkTypeReference(CheckContext.default, node, false); onCheckerDiagnostic = oldDiagnosticHook; return [type === errorType ? undefined : type, diagnostics]; } @@ -1206,10 +1291,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkTemplateInstantiationArgs( + ctx: CheckContext, node: Node, args: readonly TemplateArgumentNode[], decls: readonly TemplateParameterDeclarationNode[], - mapper: TypeMapper | undefined, parentMapper?: TypeMapper, ): Map { const params = new Map(); @@ -1221,7 +1306,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } const initMap = new Map( decls.map((decl) => { - const declaredType = checkTemplateParameterDeclaration(decl, undefined); + const declaredType = checkTemplateParameterDeclaration(CheckContext.default, decl); positional.push(declaredType); params.set(decl.id.sv, declaredType); @@ -1240,7 +1325,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker for (const [arg, idx] of args.map((v, i) => [v, i] as const)) { function deferredCheck(): [Node, Type | Value | IndeterminateEntity | null] { - return [arg, checkNode(arg.argument, mapper)]; + return [arg, checkNode(ctx, arg.argument)]; } if (arg.name) { @@ -1323,10 +1408,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const argumentMapper = createTypeMapper( mapperParams, mapperArgs, - { node, mapper }, + { node, mapper: ctx.mapper }, parentMapper, ); - const defaultValue = getResolvedTypeParameterDefault(param, decl, argumentMapper); + const defaultValue = getResolvedTypeParameterDefault( + ctx.withMapper(argumentMapper), + param, + decl, + ); if (defaultValue) { commit(param, defaultValue); } else { @@ -1407,12 +1496,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker * @returns resolved type. */ function checkTypeReferenceSymbol( + ctx: CheckContext, sym: Sym, node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, - mapper: TypeMapper | undefined, instantiateTemplates = true, ): Type { - const result = checkTypeOrValueReferenceSymbol(sym, node, mapper, instantiateTemplates); + const result = checkTypeOrValueReferenceSymbol(ctx, sym, node, instantiateTemplates); if (result === null || isValue(result)) { reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node })); return errorType; @@ -1424,12 +1513,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkTypeOrValueReferenceSymbol( + ctx: CheckContext, sym: Sym, node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, - mapper: TypeMapper | undefined, instantiateTemplates = true, ): Type | Value | IndeterminateEntity | null { - const entity = checkTypeOrValueReferenceSymbolWorker(sym, node, mapper, instantiateTemplates); + const entity = checkTypeOrValueReferenceSymbolWorker(ctx, sym, node, instantiateTemplates); if (entity !== null && isType(entity) && entity.kind === "TemplateParameter") { templateParameterUsageMap.set(entity.node!, true); @@ -1438,13 +1527,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkTypeOrValueReferenceSymbolWorker( + ctx: CheckContext, sym: Sym, node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, - mapper: TypeMapper | undefined, instantiateTemplates = true, ): Type | Value | IndeterminateEntity | null { if (sym.flags & SymbolFlags.Const) { - return getValueForNode(sym.declarations[0], mapper); + return getValueForNode(sym.declarations[0], ctx.mapper); } if (sym.flags & SymbolFlags.Decorator) { @@ -1494,19 +1583,19 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } else if (symbolLinks.declaredType) { baseType = symbolLinks.declaredType; } else if (sym.flags & SymbolFlags.Member) { - baseType = checkMemberSym(sym, mapper); + baseType = checkMemberSym(ctx, sym); } else { - baseType = checkDeclaredTypeOrIndeterminate(sym, decl, mapper); + baseType = checkDeclaredTypeOrIndeterminate(ctx, sym, decl); } } else { - const declaredType = getOrCheckDeclaredType(sym, decl, mapper); + const declaredType = getOrCheckDeclaredType(ctx, sym, decl); const templateParameters = decl.templateParameters; const instantiation = checkTemplateInstantiationArgs( + ctx, node, argumentNodes, templateParameters, - mapper, declaredType.templateMapper, ); @@ -1514,7 +1603,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker decl, [...instantiation.keys()], [...instantiation.values()], - { node, mapper }, + { node, mapper: ctx.mapper }, declaredType.templateMapper, instantiateTemplates, ); @@ -1537,8 +1626,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return sym.type; } else if (sym.flags & SymbolFlags.TemplateParameter) { const mapped = checkTemplateParameterDeclaration( + ctx, symNode as TemplateParameterDeclarationNode, - mapper, ); baseType = mapped as any; } else if (symbolLinks.type) { @@ -1548,10 +1637,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker baseType = symbolLinks.declaredType; } else { if (sym.flags & SymbolFlags.Member) { - baseType = checkMemberSym(sym, mapper); + baseType = checkMemberSym(ctx, sym); } else { // don't have a cached type for this symbol, so go grab it and cache it - baseType = getTypeForNode(symNode, mapper); + baseType = getTypeForNode(symNode, ctx.mapper); symbolLinks.type = baseType; } } @@ -1561,7 +1650,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker // don't raise deprecation when the usage site is also a deprecated // declaration. const declarationNode = getSymNode(sym); - if (declarationNode && mapper === undefined && isType(baseType)) { + if (declarationNode && ctx.mapper === undefined && isType(baseType)) { if (!isTypeReferenceContextDeprecated(node.parent!)) { checkDeprecated(baseType, declarationNode, node); } @@ -1586,9 +1675,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker * @returns The declared type for the given node. */ function getOrCheckDeclaredType( + ctx: CheckContext, sym: Sym, decl: TemplateableNode, - mapper: TypeMapper | undefined, ): TemplatedType { const symbolLinks = getSymbolLinks(sym); if (symbolLinks.declaredType) { @@ -1601,9 +1690,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } if (sym.flags & SymbolFlags.Member) { - return checkMemberSym(sym, mapper) as TemplatedType; + return checkMemberSym(ctx, sym) as TemplatedType; } else { - return checkDeclaredType(sym, decl, mapper) as TemplatedType; + return checkDeclaredType(ctx, sym, decl) as TemplatedType; } } @@ -1615,32 +1704,28 @@ export function createChecker(program: Program, resolver: NameResolver): Checker * @returns The declared type for the given node. */ function checkDeclaredTypeOrIndeterminate( + ctx: CheckContext, sym: Sym, node: TemplateableNode, - mapper: TypeMapper | undefined, ): Type | IndeterminateEntity { const type = sym.flags & SymbolFlags.Model - ? checkModelStatement(node as ModelStatementNode, mapper) + ? checkModelStatement(ctx, node as ModelStatementNode) : sym.flags & SymbolFlags.Scalar - ? checkScalar(node as ScalarStatementNode, mapper) + ? checkScalar(ctx, node as ScalarStatementNode) : sym.flags & SymbolFlags.Alias - ? checkAlias(node as AliasStatementNode, mapper) + ? checkAlias(ctx, node as AliasStatementNode) : sym.flags & SymbolFlags.Interface - ? checkInterface(node as InterfaceStatementNode, mapper) + ? checkInterface(ctx, node as InterfaceStatementNode) : sym.flags & SymbolFlags.Operation - ? checkOperation(node as OperationStatementNode, mapper) - : checkUnion(node as UnionStatementNode, mapper); + ? checkOperation(ctx, node as OperationStatementNode) + : checkUnion(ctx, node as UnionStatementNode); return type; } - function checkDeclaredType( - sym: Sym, - node: TemplateableNode, - mapper: TypeMapper | undefined, - ): Type { - return getTypeForTypeOrIndeterminate(checkDeclaredTypeOrIndeterminate(sym, node, mapper)); + function checkDeclaredType(ctx: CheckContext, sym: Sym, node: TemplateableNode): Type { + return getTypeForTypeOrIndeterminate(checkDeclaredTypeOrIndeterminate(ctx, sym, node)); } function getOrInstantiateTemplate( @@ -1713,13 +1798,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker /** Check a union expresion used in a parameter constraint, those allow the use of `valueof` as a variant. */ function checkMixedParameterConstraintUnion( + ctx: CheckContext, node: UnionExpressionNode, - mapper: TypeMapper | undefined, ): MixedParameterConstraint { const values: Type[] = []; const types: Type[] = []; for (const option of node.options) { - const [kind, type] = getTypeOrValueOfTypeForNode(option, mapper); + const [kind, type] = getTypeOrValueOfTypeForNode(option, ctx.mapper); if (kind === "value") { values.push(type); } else { @@ -1772,7 +1857,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return union; } - function checkUnionExpression(node: UnionExpressionNode, mapper: TypeMapper | undefined): Union { + function checkUnionExpression(ctx: CheckContext, node: UnionExpressionNode): Union { const unionType: Union = createAndFinishType({ kind: "Union", node, @@ -1786,7 +1871,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker }); for (const o of node.options) { - const type = getTypeForNode(o, mapper); + const type = getTypeForNode(o, ctx.mapper); // The type `A | never` is just `A` if (type === neverType) { @@ -1810,7 +1895,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } } - linkMapper(unionType, mapper); + linkMapper(unionType, ctx.mapper); return unionType; } @@ -1820,26 +1905,23 @@ export function createChecker(program: Program, resolver: NameResolver): Checker * So this doesn't work if we don't have a known set of properties (e.g. * with unions). The resulting model is anonymous. */ - function checkIntersectionExpression( - node: IntersectionExpressionNode, - mapper: TypeMapper | undefined, - ) { + function checkIntersectionExpression(ctx: CheckContext, node: IntersectionExpressionNode) { const links = getSymbolLinks(node.symbol); - if (links.declaredType && mapper === undefined) { + if (links.declaredType && ctx.mapper === undefined) { // we're not instantiating this model and we've already checked it return links.declaredType as any; } const intersection: Model = initModel(node); - const options = node.options.map((o): [Expression, Type] => [o, getTypeForNode(o, mapper)]); + const options = node.options.map((o): [Expression, Type] => [o, getTypeForNode(o, ctx.mapper)]); ensureResolved( options.map(([, type]) => type), intersection, () => { - const type = mergeModelTypes(node.symbol, node, options, mapper, intersection); - linkType(links, type, mapper); + const type = mergeModelTypes(ctx, node.symbol, node, options, intersection); + linkType(ctx, links, type); finishType(intersection); }, ); @@ -1881,12 +1963,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkDecoratorDeclaration( + ctx: CheckContext, node: DecoratorDeclarationStatementNode, - mapper: TypeMapper | undefined, ): Decorator { const symbol = getMergedSymbol(node.symbol); const links = getSymbolLinks(symbol); - if (links.declaredType && mapper === undefined) { + if (links.declaredType && ctx.mapper === undefined) { // we're not instantiating this operation and we've already checked it return links.declaredType as Decorator; } @@ -1911,39 +1993,36 @@ export function createChecker(program: Program, resolver: NameResolver): Checker name: `@${name}`, namespace, node, - target: checkFunctionParameter(node.target, mapper, true), - parameters: node.parameters.map((x) => checkFunctionParameter(x, mapper, true)), + target: checkFunctionParameter(ctx, node.target, true), + parameters: node.parameters.map((param) => checkFunctionParameter(ctx, param, true)), implementation: implementation ?? (() => {}), }); namespace.decoratorDeclarations.set(name, decoratorType); - linkType(links, decoratorType, mapper); + linkType(ctx, links, decoratorType); return decoratorType; } - function checkFunctionDeclaration( - node: FunctionDeclarationStatementNode, - mapper: TypeMapper | undefined, - ) { + function checkFunctionDeclaration(ctx: CheckContext, node: FunctionDeclarationStatementNode) { reportCheckerDiagnostic(createDiagnostic({ code: "function-unsupported", target: node })); return errorType; } function checkFunctionParameter( + ctx: CheckContext, node: FunctionParameterNode, - mapper: TypeMapper | undefined, mixed: true, ): MixedFunctionParameter; function checkFunctionParameter( + ctx: CheckContext, node: FunctionParameterNode, - mapper: TypeMapper | undefined, mixed: false, ): SignatureFunctionParameter; function checkFunctionParameter( + ctx: CheckContext, node: FunctionParameterNode, - mapper: TypeMapper | undefined, mixed: boolean, ): FunctionParameter { const links = getSymbolLinks(node.symbol); @@ -1977,7 +2056,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker if (mixed) { const type = node.type - ? getParamConstraintEntityForNode(node.type) + ? getParamConstraintEntityForNode(ctx, node.type) : ({ entityKind: "MixedParameterConstraint", type: unknownType, @@ -1997,7 +2076,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker }); } - linkType(links, parameterType, mapper); + linkType(ctx, links, parameterType); return parameterType; } @@ -2013,14 +2092,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function getParamConstraintEntityForNode( + ctx: CheckContext, node: Expression, - mapper?: TypeMapper, ): MixedParameterConstraint { switch (node.kind) { case SyntaxKind.UnionExpression: - return checkMixedParameterConstraintUnion(node, mapper); + return checkMixedParameterConstraintUnion(ctx, node); default: - const [kind, entity] = getTypeOrValueOfTypeForNode(node, mapper); + const [kind, entity] = getTypeOrValueOfTypeForNode(node, ctx.mapper); return { entityKind: "MixedParameterConstraint", node: node, @@ -2031,10 +2110,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function mergeModelTypes( + ctx: CheckContext, parentModelSym: Sym | undefined, node: ModelStatementNode | ModelExpressionNode | IntersectionExpressionNode, options: [Node, Type][], - mapper: TypeMapper | undefined, intersection: Model, ) { const properties = intersection.properties; @@ -2092,7 +2171,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ? cloneTypeForSymbol(memberSym, prop, overrides) : cloneType(prop, overrides); properties.set(prop.name, newPropType); - linkIndirectMember(node, newPropType, mapper); + linkIndirectMember(ctx, node, newPropType); for (const indexer of indexers.filter((x) => x !== option.indexer)) { checkPropertyCompatibleWithIndexer(indexer, prop, node); @@ -2106,20 +2185,20 @@ export function createChecker(program: Program, resolver: NameResolver): Checker intersection.indexer = { key: indexers[0].key, value: mergeModelTypes( + ctx, undefined, node, indexers.map((x) => [x.value.node!, x.value]), - mapper, initModel(node), ), }; } - linkMapper(intersection, mapper); + linkMapper(intersection, ctx.mapper); return finishType(intersection); } - function checkArrayExpression(node: ArrayExpressionNode, mapper: TypeMapper | undefined): Model { - const elementType = getTypeForNode(node.elementType, mapper); + function checkArrayExpression(ctx: CheckContext, node: ArrayExpressionNode): Model { + const elementType = getTypeForNode(node.elementType, ctx.mapper); const arrayType = getStdType("Array"); const arrayNode: ModelStatementNode = arrayType.node as any; const param: TemplateParameter = getTypeForNode(arrayNode.templateParameters[0]) as any; @@ -2127,12 +2206,15 @@ export function createChecker(program: Program, resolver: NameResolver): Checker arrayNode, [param], [elementType], - { node, mapper }, + { node, mapper: ctx.mapper }, undefined, ) as Model; } - function checkNamespace(node: NamespaceStatementNode | JsNamespaceDeclarationNode) { + function checkNamespace( + ctx: CheckContext, + node: NamespaceStatementNode | JsNamespaceDeclarationNode, + ) { const links = getSymbolLinks(getMergedSymbol(node.symbol)); let type = links.type as Namespace; if (!type) { @@ -2141,9 +2223,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker if (node.kind === SyntaxKind.NamespaceStatement) { if (isArray(node.statements)) { - node.statements.forEach((x) => checkNode(x)); + node.statements.forEach((x) => checkNode(ctx, x)); } else if (node.statements) { - const subNs = checkNamespace(node.statements); + const subNs = checkNamespace(ctx, node.statements); type.namespaces.set(subNs.name, subNs); } } @@ -2179,7 +2261,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker for (const sourceNode of mergedSymbol.declarations) { // namespaces created from TypeSpec scripts don't have decorators if (sourceNode.kind !== SyntaxKind.NamespaceStatement) continue; - type.decorators = type.decorators.concat(checkDecorators(type, sourceNode, undefined)); + type.decorators = type.decorators.concat( + checkDecorators(CheckContext.default, type, sourceNode), + ); } finishType(type); @@ -2273,32 +2357,32 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkOperation( + ctx: CheckContext, node: OperationStatementNode, - mapper: TypeMapper | undefined, parentInterface?: Interface, ): Operation { const inInterface = node.parent?.kind === SyntaxKind.InterfaceStatement; const symbol = inInterface ? getSymbolForMember(node) : node.symbol; const links = symbol && getSymbolLinks(symbol); if (links) { - if (links.declaredType && mapper === undefined) { + if (links.declaredType && ctx.mapper === undefined) { // we're not instantiating this operation and we've already checked it return links.declaredType as Operation; } } - if (mapper === undefined && inInterface) { + if (ctx.mapper === undefined && inInterface) { compilerAssert( parentInterface, "Operation in interface should already have been checked.", node.parent, ); } - checkTemplateDeclaration(node, mapper); + checkTemplateDeclaration(ctx, node); // If we are instantating operation inside of interface - if (isTemplatedNode(node) && mapper !== undefined && parentInterface) { - mapper = { ...mapper, partial: true }; + if (isTemplatedNode(node) && ctx.mapper !== undefined && parentInterface) { + ctx = ctx.withMapper({ ...ctx.mapper, partial: true }); } const namespace = getParentNamespaceType(node); @@ -2331,7 +2415,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker interface: parentInterface, }); if (links) { - linkType(links, operationType, mapper); + linkType(ctx, links, operationType); } const parent = node.parent!; @@ -2339,18 +2423,18 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function finishOperation() { operationType.parameters.namespace = namespace; - operationType.decorators.push(...checkDecorators(operationType, node, mapper)); + operationType.decorators.push(...checkDecorators(ctx, operationType, node)); const runDecorators = parent.kind === SyntaxKind.InterfaceStatement - ? shouldRunDecorators(parent, mapper) && shouldRunDecorators(node, mapper) - : shouldRunDecorators(node, mapper); + ? shouldRunDecorators(ctx, parent) && shouldRunDecorators(ctx, node) + : shouldRunDecorators(ctx, node); return finishType(operationType, { skipDecorators: !runDecorators }); } // Is this a definition or reference? if (node.signature.kind === SyntaxKind.OperationSignatureReference) { // Attempt to resolve the operation - const baseOperation = checkOperationIs(node, node.signature.baseOperation, mapper); + const baseOperation = checkOperationIs(ctx, node, node.signature.baseOperation); if (baseOperation) { ensureResolved([baseOperation], operationType, () => { operationType.sourceOperation = baseOperation; @@ -2382,15 +2466,15 @@ export function createChecker(program: Program, resolver: NameResolver): Checker finishOperation(); } } else { - operationType.parameters = getTypeForNode(node.signature.parameters, mapper) as Model; - operationType.returnType = getTypeForNode(node.signature.returnType, mapper); + operationType.parameters = getTypeForNode(node.signature.parameters, ctx.mapper) as Model; + operationType.returnType = getTypeForNode(node.signature.returnType, ctx.mapper); ensureResolved([operationType.parameters], operationType, () => { finishOperation(); }); } - linkMapper(operationType, mapper); - if (parent.kind !== SyntaxKind.InterfaceStatement && mapper === undefined) { + linkMapper(operationType, ctx.mapper); + if (parent.kind !== SyntaxKind.InterfaceStatement && ctx.mapper === undefined) { namespace?.operations.set(name, operationType); } @@ -2398,9 +2482,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkOperationIs( + ctx: CheckContext, operation: OperationStatementNode, opReference: TypeReferenceNode | undefined, - mapper: TypeMapper | undefined, ): Operation | undefined { if (!opReference) return undefined; // Ensure that we don't end up with a circular reference to the same operation @@ -2413,7 +2497,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker // Did we encounter a circular operation reference? if (target && pendingResolutions.has(target, ResolutionKind.BaseType)) { - if (mapper === undefined) { + if (ctx.mapper === undefined) { reportCheckerDiagnostic( createDiagnostic({ code: "circular-op-signature", @@ -2427,7 +2511,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } // Resolve the base operation type - const baseOperation = getTypeForNode(opReference, mapper); + const baseOperation = getTypeForNode(opReference, ctx.mapper); if (opSymId) { pendingResolutions.finish(opSymId, ResolutionKind.BaseType); } @@ -2453,11 +2537,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return resolver.symbols.global.declarations[0] as any; } - function checkTupleExpression(node: TupleExpressionNode, mapper: TypeMapper | undefined): Tuple { + function checkTupleExpression(ctx: CheckContext, node: TupleExpressionNode): Tuple { return createAndFinishType({ kind: "Tuple", node: node, - values: node.values.map((v) => getTypeForNode(v, mapper)), + values: node.values.map((v) => getTypeForNode(v, ctx.mapper)), }); } @@ -2466,6 +2550,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function resolveRelatedSymbols(id: IdentifierNode, mapper?: TypeMapper): Sym[] | undefined { + const ctx = CheckContext.default.withMapper(mapper); let sym: Sym | undefined; const { node, kind } = getIdentifierContext(id); @@ -2499,10 +2584,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker resolveDecorator = false; } } - sym = resolveTypeReferenceSym(ref, mapper, resolveDecorator); + sym = resolveTypeReferenceSym(ctx, ref, resolveDecorator); break; case IdentifierKind.TemplateArgument: - const templates = getTemplateDeclarationsForArgument(node as TemplateArgumentNode, mapper); + const templates = getTemplateDeclarationsForArgument(ctx, node as TemplateArgumentNode); const firstMatchingParameter = templates .flatMap((t) => t.templateParameters) @@ -2528,17 +2613,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return undefined; //sym?.symbolSource ?? sym; } - function getTemplateDeclarationsForArgument( - node: TemplateArgumentNode, - mapper: TypeMapper | undefined, - ) { + function getTemplateDeclarationsForArgument(ctx: CheckContext, node: TemplateArgumentNode) { const ref = node.parent as TypeReferenceNode; - let resolved = resolveTypeReferenceSym(ref, mapper, false); + let resolved = resolveTypeReferenceSym(ctx, ref, false); // if the reference type can't be resolved and has parse error, // it likely means the reference type hasn't been completed yet. i.e. Foo isTemplatedNode(n)) ?? []) as TemplateableNode[]; } @@ -2688,9 +2770,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const argNode = node.parent; const refNode = node.parent.parent; const decl = getTemplateDeclarationsForArgument( - argNode, // We should be giving the argument so the mapper here should be undefined - undefined /* mapper */, + CheckContext.default, + argNode, ); const index = refNode.arguments.findIndex((n) => n === argNode); @@ -2734,7 +2816,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return undefined; } - const ctorType = checkCallExpressionTarget(callExpNode, undefined); + const ctorType = checkCallExpressionTarget(CheckContext.default, callExpNode); if (ctorType?.kind !== "ScalarConstructor") { return undefined; @@ -2831,8 +2913,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return completions; // cannot complete, name can be chosen arbitrarily case IdentifierKind.TemplateArgument: { const templates = getTemplateDeclarationsForArgument( + CheckContext.default, ancestor as TemplateArgumentNode, - undefined, ); for (const template of templates) { @@ -2896,7 +2978,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker if (base) { if (base.flags & SymbolFlags.Alias) { - base = getAliasedSymbol(base, undefined); + base = getAliasedSymbol(CheckContext.default, base); } if (base) { @@ -2929,8 +3011,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ancestor.parent.name === undefined ) { const templates = getTemplateDeclarationsForArgument( + CheckContext.default, ancestor.parent as TemplateArgumentNode, - undefined, ); for (const template of templates) { @@ -3038,8 +3120,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function resolveTypeReferenceSym( + ctx: CheckContext, node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, - mapper: TypeMapper | undefined, options?: Partial | boolean, ): Sym | undefined { const resolvedOptions: SymbolResolutionOptions = @@ -3047,13 +3129,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ? { ...defaultSymbolResolutionOptions, resolveDecorators: options } : { ...defaultSymbolResolutionOptions, ...(options ?? {}) }; if ( - mapper === undefined && + ctx.mapper === undefined && !resolvedOptions.resolveDeclarationOfTemplate && referenceSymCache.has(node) ) { return referenceSymCache.get(node); } - const sym = resolveTypeReferenceSymInternal(node, mapper, resolvedOptions); + const sym = resolveTypeReferenceSymInternal(ctx, node, resolvedOptions); if (!resolvedOptions.resolveDeclarationOfTemplate) { referenceSymCache.set(node, sym); } @@ -3061,8 +3143,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function resolveTypeReferenceSymInternal( + ctx: CheckContext, node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, - mapper: TypeMapper | undefined, options: SymbolResolutionOptions, ): Sym | undefined { if (hasParseError(node)) { @@ -3071,12 +3153,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return undefined; } if (node.kind === SyntaxKind.TypeReference) { - return resolveTypeReferenceSym(node.target, mapper, options); + return resolveTypeReferenceSym(ctx, node.target, options); } else if (node.kind === SyntaxKind.Identifier) { const links = resolver.getNodeLinks(node); - if (mapper === undefined && links.resolutionResult) { + if (ctx.mapper === undefined && links.resolutionResult) { if ( - mapper === undefined && // do not report error when instantiating + ctx.mapper === undefined && // do not report error when instantiating links.resolutionResult & (ResolutionResultFlags.NotFound | ResolutionResultFlags.Unknown) ) { reportCheckerDiagnostic( @@ -3096,7 +3178,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const sym = links.resolvedSymbol; return sym?.symbolSource ?? sym; } else if (node.kind === SyntaxKind.MemberExpression) { - let base = resolveTypeReferenceSym(node.base, mapper, { + let base = resolveTypeReferenceSym(ctx, node.base, { ...options, resolveDecorators: false, // when resolving decorator the base cannot also be one }); @@ -3107,7 +3189,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker // when resolving a type reference based on an alias, unwrap the alias. if (base.flags & SymbolFlags.Alias) { - const aliasedSym = getAliasedSymbol(base, mapper); + const aliasedSym = getAliasedSymbol(ctx, base); if (!aliasedSym) { reportCheckerDiagnostic( createDiagnostic({ @@ -3126,7 +3208,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } base = aliasedSym; } else if (!options.resolveDeclarationOfTemplate && isTemplatedNode(getSymNode(base))) { - const baseSym = getContainerTemplateSymbol(base, node.base, mapper); + const baseSym = getContainerTemplateSymbol(ctx, base, node.base); if (!baseSym) { return undefined; } @@ -3246,7 +3328,7 @@ export function createChecker(program: Program, resolver: NameResolver): 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): Sym | undefined { + function getAliasedSymbol(ctx: CheckContext, aliasSymbol: Sym): Sym | undefined { const node = getSymNode(aliasSymbol); const links = resolver.getSymbolLinks(aliasSymbol); if (!links.aliasResolutionIsTemplate) { @@ -3254,7 +3336,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } // Otherwise for templates we need to get the type and retrieve the late bound symbol. - const aliasType = getTypeForNode(node as AliasStatementNode, mapper); + const aliasType = getTypeForNode(node as AliasStatementNode, ctx.mapper); return lateBindContainer(aliasType, aliasSymbol); } @@ -3265,12 +3347,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker * ``` */ function getContainerTemplateSymbol( + ctx: CheckContext, sym: Sym, node: MemberExpressionNode | IdentifierNode, - mapper: TypeMapper | undefined, ): Sym | undefined { if (pendingResolutions.has(sym, ResolutionKind.Type)) { - if (mapper === undefined) { + if (ctx.mapper === undefined) { reportCheckerDiagnostic( createDiagnostic({ code: "circular-alias-type", @@ -3283,7 +3365,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } pendingResolutions.start(sym, ResolutionKind.Type); - const type = checkTypeReferenceSymbol(sym, node, mapper); + const type = checkTypeReferenceSymbol(ctx, sym, node); pendingResolutions.finish(sym, ResolutionKind.Type); return lateBindContainer(type, sym); @@ -3311,13 +3393,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkStringTemplateExpresion( + ctx: CheckContext, node: StringTemplateExpressionNode, - mapper: TypeMapper | undefined, ): IndeterminateEntity | StringValue | null { let hasType = false; let hasValue = false; const spanTypeOrValues = node.spans.map( - (span) => [span, checkNode(span.expression, mapper)] as const, + (span) => [span, checkNode(ctx, span.expression)] as const, ); for (const [_, typeOrValue] of spanTypeOrValues) { if (typeOrValue !== null) { @@ -3564,7 +3646,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function checkSourceFile(file: TypeSpecScriptNode) { for (const statement of file.statements) { - checkNode(statement, undefined); + checkNode(CheckContext.default, statement, undefined); } } @@ -3573,34 +3655,31 @@ export function createChecker(program: Program, resolver: NameResolver): Checker * @param node Node with template parameters * @param mapper Type mapper, set if instantiating the template, undefined otherwise. */ - function checkTemplateDeclaration(node: TemplateableNode, mapper: TypeMapper | undefined) { + function checkTemplateDeclaration(ctx: CheckContext, node: TemplateableNode) { // If mapper is undefined it means we are checking the declaration of the template. - if (mapper === undefined) { + if (ctx.mapper === undefined) { for (const templateParameter of node.templateParameters) { - checkTemplateParameterDeclaration(templateParameter, undefined); + checkTemplateParameterDeclaration(ctx, templateParameter); } } } - function checkModel( - node: ModelExpressionNode | ModelStatementNode, - mapper: TypeMapper | undefined, - ): Model { + function checkModel(ctx: CheckContext, node: ModelExpressionNode | ModelStatementNode): Model { if (node.kind === SyntaxKind.ModelStatement) { - return checkModelStatement(node, mapper); + return checkModelStatement(ctx, node); } else { - return checkModelExpression(node, mapper); + return checkModelExpression(ctx, node); } } - function checkModelStatement(node: ModelStatementNode, mapper: TypeMapper | undefined): Model { + function checkModelStatement(ctx: CheckContext, node: ModelStatementNode): Model { const links = getSymbolLinks(node.symbol); - if (links.declaredType && mapper === undefined) { + if (links.declaredType && ctx.mapper === undefined) { // we're not instantiating this model and we've already checked it return links.declaredType as any; } - checkTemplateDeclaration(node, mapper); + checkTemplateDeclaration(ctx, node); const decorators: DecoratorApplication[] = []; const type: Model = createType({ @@ -3613,7 +3692,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker sourceModels: [], derivedModels: [], }); - linkType(links, type, mapper); + linkType(ctx, links, type); if (node.symbol.members) { const members = resolver.getAugmentedSymbolTable(node.symbol.members); @@ -3626,13 +3705,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } } - const isBase = checkModelIs(node, node.is, mapper); + const isBase = checkModelIs(ctx, node, node.is); ensureResolved( [ isBase, ...node.properties .filter((x) => x.kind === SyntaxKind.ModelSpreadProperty) - .map((x) => checkSpreadTarget(node, x.target, mapper)), + .map((x) => checkSpreadTarget(ctx, node, x.target)), ], type, () => { @@ -3650,7 +3729,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker sourceProperty: prop, model: type, }); - linkIndirectMember(node, newProp, mapper); + linkIndirectMember(ctx, node, newProp); type.properties.set(prop.name, newProp); } } @@ -3658,7 +3737,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker if (isBase) { type.baseModel = isBase.baseModel; } else if (node.extends) { - type.baseModel = checkClassHeritage(node, node.extends, mapper); + type.baseModel = checkClassHeritage(ctx, node, node.extends); if (type.baseModel) { copyDeprecation(type.baseModel, type); } @@ -3670,17 +3749,17 @@ export function createChecker(program: Program, resolver: NameResolver): Checker // Hold on to the model type that's being defined so that it // can be referenced - if (mapper === undefined) { + if (ctx.mapper === undefined) { type.namespace?.models.set(type.name, type); } // Evaluate the properties after - checkModelProperties(node, type.properties, type, mapper); + checkModelProperties(ctx, node, type.properties, type); - decorators.push(...checkDecorators(type, node, mapper)); + decorators.push(...checkDecorators(ctx, type, node)); - linkMapper(type, mapper); - finishType(type, { skipDecorators: !shouldRunDecorators(node, mapper) }); + linkMapper(type, ctx.mapper); + finishType(type, { skipDecorators: !shouldRunDecorators(ctx, node) }); lateBindMemberContainer(type); lateBindMembers(type); @@ -3700,45 +3779,45 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return type; } - function shouldRunDecorators(node: TemplateDeclarationNode, mapper: TypeMapper | undefined) { + function shouldRunDecorators(ctx: CheckContext, node: TemplateDeclarationNode) { // Node is not a template we should create the type. if (node.templateParameters.length === 0) { return true; } // There is no mapper so we shouldn't be instantiating the template. - if (mapper === undefined) { + if (ctx.mapper === undefined) { return false; } // Some of the mapper args are still template parameter so we shouldn't create the type. return ( - !mapper.partial && - mapper.args.every( + !ctx.mapper.partial && + ctx.mapper.args.every( (t) => isValue(t) || t.entityKind === "Indeterminate" || t.kind !== "TemplateParameter", ) ); } - function checkModelExpression(node: ModelExpressionNode, mapper: TypeMapper | undefined) { + function checkModelExpression(ctx: CheckContext, node: ModelExpressionNode) { const links = getSymbolLinks(node.symbol); - if (links.declaredType && mapper === undefined) { + if (links.declaredType && ctx.mapper === undefined) { // we're not instantiating this model and we've already checked it return links.declaredType as any; } const type = initModel(node); const properties = type.properties; - linkType(links, type, mapper); - linkMapper(type, mapper); + linkType(ctx, links, type); + linkMapper(type, ctx.mapper); ensureResolved( node.properties .filter((x) => x.kind === SyntaxKind.ModelSpreadProperty) - .map((x) => checkSpreadTarget(node, x.target, mapper)), + .map((x) => checkSpreadTarget(ctx, node, x.target)), type, () => { - checkModelProperties(node, properties, type, mapper); + checkModelProperties(ctx, node, properties, type); finishType(type); }, ); @@ -3804,25 +3883,25 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkModelProperties( + ctx: CheckContext, node: ModelExpressionNode | ModelStatementNode, properties: Map, parentModel: Model, - mapper: TypeMapper | undefined, ) { let spreadIndexers: ModelIndexer[] | undefined; for (const prop of node.properties!) { if ("id" in prop) { - const newProp = checkModelProperty(prop, mapper); + const newProp = checkModelProperty(ctx, prop); newProp.model = parentModel; checkPropertyCompatibleWithModelIndexer(parentModel, newProp, prop); defineProperty(properties, newProp); } else { // spread property const [newProperties, additionalIndexer] = checkSpreadProperty( + ctx, node.symbol, prop.target, parentModel, - mapper, ); if (additionalIndexer) { @@ -3833,7 +3912,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } } for (const newProp of newProperties) { - linkIndirectMember(node, newProp, mapper); + linkIndirectMember(ctx, node, newProp); checkPropertyCompatibleWithModelIndexer(parentModel, newProp, prop); defineProperty(properties, newProp, prop); } @@ -3853,11 +3932,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkObjectValue( + ctx: CheckContext, node: ObjectLiteralNode, - mapper: TypeMapper | undefined, constraint: CheckValueConstraint | undefined, ): ObjectValue | null { - const properties = checkObjectLiteralProperties(node, mapper); + const properties = checkObjectLiteralProperties(ctx, node); if (properties === null) { return null; } @@ -3913,21 +3992,21 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkObjectLiteralProperties( + ctx: CheckContext, node: ObjectLiteralNode, - mapper: TypeMapper | undefined, ): Map | null { const properties = new Map(); let hasError = false; for (const prop of node.properties!) { if ("id" in prop) { - const value = getValueForNode(prop.value, mapper); + const value = getValueForNode(prop.value, ctx.mapper); if (value === null) { hasError = true; } else { properties.set(prop.id.sv, { name: prop.id.sv, value: value, node: prop }); } } else { - const targetType = checkObjectSpreadProperty(prop.target, mapper); + const targetType = checkObjectSpreadProperty(ctx, prop.target); if (targetType) { for (const [name, value] of targetType.properties) { properties.set(name, { ...value }); @@ -3939,10 +4018,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkObjectSpreadProperty( + ctx: CheckContext, targetNode: TypeReferenceNode, - mapper: TypeMapper | undefined, ): ObjectValue | null { - const value = getValueForNode(targetNode, mapper); + const value = getValueForNode(targetNode, ctx.mapper); if (value === null) { return null; } @@ -3955,13 +4034,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkArrayValue( + ctx: CheckContext, node: ArrayLiteralNode, - mapper: TypeMapper | undefined, constraint: CheckValueConstraint | undefined, ): ArrayValue | null { let hasError = false; const values = node.values.map((itemNode) => { - const value = getValueForNode(itemNode, mapper); + const value = getValueForNode(itemNode, ctx.mapper); if (value === null) { hasError = true; } @@ -4167,10 +4246,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkCallExpressionTarget( + ctx: CheckContext, node: CallExpressionNode, - mapper: TypeMapper | undefined, ): ScalarConstructor | Scalar | null { - const target = checkTypeReference(node.target, mapper); + const target = checkTypeReference(ctx, node.target); if (target.kind === "Scalar" || target.kind === "ScalarConstructor") { return target; @@ -4224,8 +4303,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function createScalarValue( + ctx: CheckContext, node: CallExpressionNode, - mapper: TypeMapper | undefined, declaration: ScalarConstructor, ): ScalarValue | null { let hasError = false; @@ -4275,7 +4354,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker for (let i = index; i < node.arguments.length; i++) { const argNode = node.arguments[i]; if (argNode) { - const arg = getValueForNode(argNode, mapper, { kind: "argument", type: restType }); + const arg = getValueForNode(argNode, ctx.mapper, { + kind: "argument", + type: restType, + }); if (arg === null) { hasError = true; continue; @@ -4292,7 +4374,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } const argNode = node.arguments[index]; if (argNode) { - const arg = getValueForNode(argNode, mapper, { + const arg = getValueForNode(argNode, ctx.mapper, { kind: "argument", type: parameter.type, }); @@ -4322,16 +4404,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker }; } - function checkCallExpression( - node: CallExpressionNode, - mapper: TypeMapper | undefined, - ): Value | null { - const target = checkCallExpressionTarget(node, mapper); + function checkCallExpression(ctx: CheckContext, node: CallExpressionNode): Value | null { + const target = checkCallExpressionTarget(ctx, node); if (target === null) { return null; } if (target.kind === "ScalarConstructor") { - return createScalarValue(node, mapper, target); + return createScalarValue(ctx, node, target); } if (relation.areScalarsRelated(target, getStdType("string"))) { @@ -4352,8 +4431,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } } - function checkTypeOfExpression(node: TypeOfExpressionNode, mapper: TypeMapper | undefined): Type { - const entity = checkNode(node.target, mapper, undefined); + function checkTypeOfExpression(ctx: CheckContext, node: TypeOfExpressionNode): Type { + const entity = checkNode(ctx, node.target, undefined); if (entity === null) { // Shouldn't need to emit error as we assume null value already emitted error when produced return errorType; @@ -4562,9 +4641,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } } function checkClassHeritage( + ctx: CheckContext, model: ModelStatementNode, heritageRef: Expression, - mapper: TypeMapper | undefined, ): Model | undefined { if (heritageRef.kind === SyntaxKind.ModelExpression) { reportCheckerDiagnostic( @@ -4593,7 +4672,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const target = resolver.getNodeLinks(heritageRef).resolvedSymbol; if (target && pendingResolutions.has(target, ResolutionKind.BaseType)) { - if (mapper === undefined) { + if (ctx.mapper === undefined) { reportCheckerDiagnostic( createDiagnostic({ code: "circular-base-type", @@ -4604,7 +4683,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } return undefined; } - const heritageType = getTypeForNode(heritageRef, mapper); + const heritageType = getTypeForNode(heritageRef, ctx.mapper); pendingResolutions.finish(modelSymId, ResolutionKind.BaseType); if (isErrorType(heritageType)) { compilerAssert(program.hasError(), "Should already have reported an error.", heritageRef); @@ -4630,9 +4709,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkModelIs( + ctx: CheckContext, model: ModelStatementNode, isExpr: Expression | undefined, - mapper: TypeMapper | undefined, ): Model | undefined { if (!isExpr) return undefined; @@ -4649,11 +4728,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ); return undefined; } else if (isExpr.kind === SyntaxKind.ArrayExpression) { - isType = checkArrayExpression(isExpr, mapper); + isType = checkArrayExpression(ctx, isExpr); } else if (isExpr.kind === SyntaxKind.TypeReference) { const target = resolver.getNodeLinks(isExpr).resolvedSymbol; if (target && pendingResolutions.has(target, ResolutionKind.BaseType)) { - if (mapper === undefined) { + if (ctx.mapper === undefined) { reportCheckerDiagnostic( createDiagnostic({ code: "circular-base-type", @@ -4664,7 +4743,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } return undefined; } - isType = getTypeForNode(isExpr, mapper); + isType = getTypeForNode(isExpr, ctx.mapper); } else { reportCheckerDiagnostic(createDiagnostic({ code: "is-model", target: isExpr })); return undefined; @@ -4689,15 +4768,15 @@ export function createChecker(program: Program, resolver: NameResolver): Checker /** Get the type for the spread target */ function checkSpreadTarget( + ctx: CheckContext, model: ModelStatementNode | ModelExpressionNode, target: TypeReferenceNode, - mapper: TypeMapper | undefined, ): Type | undefined { const modelSymId = getNodeSym(model); const targetSym = resolver.getNodeLinks(target).resolvedSymbol; if (targetSym === modelSymId) { - if (mapper === undefined) { + if (ctx.mapper === undefined) { reportCheckerDiagnostic( createDiagnostic({ code: "spread-model", @@ -4708,17 +4787,17 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } return undefined; } - const type = getTypeForNode(target, mapper); + const type = getTypeForNode(target, ctx.mapper); return type; } function checkSpreadProperty( + ctx: CheckContext, parentModelSym: Sym, targetNode: TypeReferenceNode, parentModel: Model, - mapper: TypeMapper | undefined, ): [ModelProperty[], ModelIndexer | undefined] { - const targetType = getTypeForNode(targetNode, mapper); + const targetType = getTypeForNode(targetNode, ctx.mapper); if (targetType.kind === "TemplateParameter" || isErrorType(targetType)) { return [[], undefined]; @@ -4726,14 +4805,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker if (targetType.kind !== "Model") { reportCheckerDiagnostic( createDiagnostic({ code: "spread-model", target: targetNode }), - mapper, + ctx.mapper, ); return [[], undefined]; } if (isArrayModelType(targetType)) { reportCheckerDiagnostic( createDiagnostic({ code: "spread-model", target: targetNode }), - mapper, + ctx.mapper, ); return [[], undefined]; } @@ -4762,11 +4841,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker * @param mapper Type Mapper. */ function linkIndirectMember( + ctx: CheckContext, containerNode: MemberContainerNode, member: MemberType, - mapper: TypeMapper | undefined, ) { - if (mapper !== undefined) { + if (ctx.mapper !== undefined) { return; } compilerAssert(typeof member.name === "string", "Cannot link unmapped unions"); @@ -4777,18 +4856,15 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const memberSym = getMemberSymbol(containerNode.symbol, member.name); if (memberSym) { const links = resolver.getSymbolLinks(memberSym); - linkMemberType(links, member, mapper); + linkMemberType(ctx, links, member); } } - function checkModelProperty( - prop: ModelPropertyNode, - mapper: TypeMapper | undefined, - ): ModelProperty { + function checkModelProperty(ctx: CheckContext, prop: ModelPropertyNode): ModelProperty { const sym = getSymbolForMember(prop)!; const links = getSymbolLinksForMember(prop); - if (links && links.declaredType && mapper === undefined) { + if (links && links.declaredType && ctx.mapper === undefined) { return links.declaredType as ModelProperty; } const name = prop.id.sv; @@ -4802,7 +4878,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker decorators: [], }); - if (pendingResolutions.has(sym, ResolutionKind.Type) && mapper === undefined) { + if (pendingResolutions.has(sym, ResolutionKind.Type) && ctx.mapper === undefined) { reportCheckerDiagnostic( createDiagnostic({ code: "circular-prop", @@ -4813,24 +4889,24 @@ export function createChecker(program: Program, resolver: NameResolver): Checker type.type = errorType; } else { pendingResolutions.start(sym, ResolutionKind.Type); - type.type = getTypeForNode(prop.value, mapper); + type.type = getTypeForNode(prop.value, ctx.mapper); if (prop.default) { - const defaultValue = checkDefaultValue(prop.default, type.type, mapper); + const defaultValue = checkDefaultValue(ctx, prop.default, type.type); if (defaultValue !== null) { type.defaultValue = defaultValue; } } if (links) { - linkType(links, type, mapper); + linkType(ctx, links, type); } } - type.decorators = checkDecorators(type, prop, mapper); + type.decorators = checkDecorators(ctx, type, prop); const parentTemplate = getParentTemplateNode(prop); - linkMapper(type, mapper); + linkMapper(type, ctx.mapper); let runDecorators = false; - if (!parentTemplate || shouldRunDecorators(parentTemplate, mapper)) { + if (!parentTemplate || shouldRunDecorators(ctx, parentTemplate)) { const docComment = docFromCommentForSym.get(sym); if (docComment) { type.decorators.unshift(createDocFromCommentDecorator("self", docComment)); @@ -4852,16 +4928,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker }; } - function checkDefaultValue( - defaultNode: Node, - type: Type, - mapper: TypeMapper | undefined, - ): Value | null { + function checkDefaultValue(ctx: CheckContext, defaultNode: Node, type: Type): Value | null { if (isErrorType(type)) { // if the prop type is an error we don't need to validate again. return null; } - const defaultValue = getValueForNode(defaultNode, mapper, { + const defaultValue = getValueForNode(defaultNode, ctx.mapper, { kind: "assignment", type, }); @@ -4882,11 +4954,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkDecoratorApplication( + ctx: CheckContext, targetType: Type, decNode: DecoratorExpressionNode | AugmentDecoratorStatementNode, - mapper: TypeMapper | undefined, ): DecoratorApplication | undefined { - const sym = resolveTypeReferenceSym(decNode.target, undefined, true); + const sym = resolveTypeReferenceSym(ctx.withMapper(undefined), decNode.target, true); if (!sym) { // Error should already have been reported above return undefined; @@ -4912,7 +4984,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker x.kind === SyntaxKind.DecoratorDeclarationStatement, ); if (decoratorDeclNode) { - checkDecoratorDeclaration(decoratorDeclNode, undefined); + checkDecoratorDeclaration(ctx.withMapper(undefined), decoratorDeclNode); } } if (symbolLinks.declaredType) { @@ -4924,11 +4996,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker hasError = true; } } - const [argsHaveError, args] = checkDecoratorArguments( - decNode, - mapper, - symbolLinks.declaredType, - ); + const [argsHaveError, args] = checkDecoratorArguments(ctx, decNode, symbolLinks.declaredType); if (hasError || argsHaveError) { return undefined; @@ -4967,8 +5035,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkDecoratorArguments( + ctx: CheckContext, node: DecoratorExpressionNode | AugmentDecoratorStatementNode, - mapper: TypeMapper | undefined, declaration: Decorator | undefined, ): [boolean, DecoratorArgument[]] { // if we don't have a declaration we can just return the types or values if @@ -4976,7 +5044,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return [ false, node.arguments.map((argNode): DecoratorArgument => { - let type = checkNode(argNode, mapper) ?? errorType; + let type = checkNode(ctx, argNode) ?? errorType; if (type.entityKind === "Indeterminate") { type = type.type; } @@ -5032,7 +5100,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker argNode: Expression, perParamType: MixedParameterConstraint, ): DecoratorArgument | undefined { - const arg = getTypeOrValueForNode(argNode, mapper, { + const arg = getTypeOrValueForNode(argNode, ctx.mapper, { kind: "argument", constraint: perParamType, }); @@ -5157,15 +5225,15 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkAugmentDecorators( + ctx: CheckContext, sym: Sym, targetType: Type, - mapper: TypeMapper | undefined, ): DecoratorApplication[] { const augmentDecoratorNodes = resolver.getAugmentDecoratorsForSym(sym); const decorators: DecoratorApplication[] = []; for (const decNode of augmentDecoratorNodes) { - const decorator = checkDecoratorApplication(targetType, decNode, mapper); + const decorator = checkDecoratorApplication(ctx, targetType, decNode); if (decorator) { decorators.unshift(decorator); } @@ -5176,9 +5244,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker /** * Check that augment decorator are targeting valid symbols. */ - function checkAugmentDecorator(node: AugmentDecoratorStatementNode) { + function checkAugmentDecorator(ctx: CheckContext, node: AugmentDecoratorStatementNode) { // This will validate the target type is pointing to a valid ref. - resolveTypeReferenceSym(node.targetType, undefined, { resolveDeclarationOfTemplate: true }); + resolveTypeReferenceSym(ctx.withMapper(undefined), node.targetType, { + resolveDeclarationOfTemplate: true, + }); + const links = resolver.getNodeLinks(node.targetType); if (links.isTemplateInstantiation) { program.reportDiagnostic( @@ -5225,8 +5296,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker /** * Check that using statements are targeting valid symbols. */ - function checkUsings(node: UsingStatementNode) { - const usedSym = resolveTypeReferenceSym(node.name, undefined); + function checkUsings(ctx: CheckContext, node: UsingStatementNode) { + const usedSym = resolveTypeReferenceSym(ctx.withMapper(undefined), node.name); if (usedSym) { if (~usedSym.flags & SymbolFlags.Namespace) { reportCheckerDiagnostic(createDiagnostic({ code: "using-invalid-ref", target: node.name })); @@ -5236,9 +5307,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return errorType; } function checkDecorators( + ctx: CheckContext, targetType: Type, node: Node & { decorators: readonly DecoratorExpressionNode[] }, - mapper: TypeMapper | undefined, ) { const sym = isMemberNode(node) ? (getSymbolForMember(node) ?? node.symbol) @@ -5251,7 +5322,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ...node.decorators, ]; for (const decNode of decoratorNodes) { - const decorator = checkDecoratorApplication(targetType, decNode, mapper); + const decorator = checkDecoratorApplication(ctx, targetType, decNode); if (decorator) { decorators.unshift(decorator); } @@ -5275,14 +5346,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return decorators; } - function checkScalar(node: ScalarStatementNode, mapper: TypeMapper | undefined): Scalar { + function checkScalar(ctx: CheckContext, node: ScalarStatementNode): Scalar { const links = getSymbolLinks(node.symbol); - if (links.declaredType && mapper === undefined) { + if (links.declaredType && ctx.mapper === undefined) { // we're not instantiating this model and we've already checked it return links.declaredType as any; } - checkTemplateDeclaration(node, mapper); + checkTemplateDeclaration(ctx, node); const decorators: DecoratorApplication[] = []; @@ -5295,33 +5366,33 @@ export function createChecker(program: Program, resolver: NameResolver): Checker decorators, derivedScalars: [], }); - linkType(links, type, mapper); + linkType(ctx, links, type); if (node.extends) { - type.baseScalar = checkScalarExtends(node, node.extends, mapper); + type.baseScalar = checkScalarExtends(ctx, node, node.extends); if (type.baseScalar) { copyDeprecation(type.baseScalar, type); type.baseScalar.derivedScalars.push(type); } } - checkScalarConstructors(type, node, type.constructors, mapper); - decorators.push(...checkDecorators(type, node, mapper)); + checkScalarConstructors(ctx, type, node, type.constructors); + decorators.push(...checkDecorators(ctx, type, node)); - if (mapper === undefined) { + if (ctx.mapper === undefined) { type.namespace?.scalars.set(type.name, type); } - linkMapper(type, mapper); + linkMapper(type, ctx.mapper); if (isInTypeSpecNamespace(type)) { stdTypes[type.name as any as keyof StdTypes] = type as any; } - return finishType(type, { skipDecorators: !shouldRunDecorators(node, mapper) }); + return finishType(type, { skipDecorators: !shouldRunDecorators(ctx, node) }); } function checkScalarExtends( + ctx: CheckContext, scalar: ScalarStatementNode, extendsRef: TypeReferenceNode, - mapper: TypeMapper | undefined, ): Scalar | undefined { const symId = getNodeSym(scalar); pendingResolutions.start(symId, ResolutionKind.BaseType); @@ -5329,7 +5400,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const target = resolver.getNodeLinks(extendsRef).resolvedSymbol; if (target && pendingResolutions.has(target, ResolutionKind.BaseType)) { - if (mapper === undefined) { + if (ctx.mapper === undefined) { reportCheckerDiagnostic( createDiagnostic({ code: "circular-base-type", @@ -5340,7 +5411,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } return undefined; } - const extendsType = getTypeForNode(extendsRef, mapper); + const extendsType = getTypeForNode(extendsRef, ctx.mapper); pendingResolutions.finish(symId, ResolutionKind.BaseType); if (isErrorType(extendsType)) { compilerAssert(program.hasError(), "Should already have reported an error.", extendsRef); @@ -5356,10 +5427,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkScalarConstructors( + ctx: CheckContext, parentScalar: Scalar, node: ScalarStatementNode, constructors: Map, - mapper: TypeMapper | undefined, ) { if (parentScalar.baseScalar) { for (const member of parentScalar.baseScalar.constructors.values()) { @@ -5370,12 +5441,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker scalar: parentScalar, }, ); - linkIndirectMember(node, newConstructor, mapper); + linkIndirectMember(ctx, node, newConstructor); constructors.set(member.name, newConstructor); } } for (const member of node.members) { - const constructor = checkScalarConstructor(member, mapper, parentScalar); + const constructor = checkScalarConstructor(ctx, member, parentScalar); if (constructors.has(constructor.name as string)) { reportCheckerDiagnostic( createDiagnostic({ @@ -5391,13 +5462,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkScalarConstructor( + ctx: CheckContext, node: ScalarConstructorNode, - mapper: TypeMapper | undefined, parentScalar: Scalar, ): ScalarConstructor { const name = node.id.sv; const links = getSymbolLinksForMember(node); - if (links && links.declaredType && mapper === undefined) { + if (links && links.declaredType && ctx.mapper === undefined) { // we're not instantiating this scalar constructor and we've already checked it return links.declaredType as ScalarConstructor; } @@ -5407,32 +5478,29 @@ export function createChecker(program: Program, resolver: NameResolver): Checker scalar: parentScalar, name, node, - parameters: node.parameters.map((x) => checkFunctionParameter(x, mapper, false)), + parameters: node.parameters.map((x) => checkFunctionParameter(ctx, x, false)), }); - linkMapper(member, mapper); + linkMapper(member, ctx.mapper); if (links) { - linkType(links, member, mapper); + linkType(ctx, links, member); } return finishType(member, { - skipDecorators: !shouldRunDecorators(node.parent!, mapper), + skipDecorators: !shouldRunDecorators(ctx, node.parent!), }); } - function checkAlias( - node: AliasStatementNode, - mapper: TypeMapper | undefined, - ): Type | IndeterminateEntity { + function checkAlias(ctx: CheckContext, node: AliasStatementNode): Type | IndeterminateEntity { const links = getSymbolLinks(node.symbol); - if (links.declaredType && mapper === undefined) { + if (links.declaredType && ctx.mapper === undefined) { return links.declaredType; } - checkTemplateDeclaration(node, mapper); + checkTemplateDeclaration(ctx, node); const aliasSymId = getNodeSym(node); if (pendingResolutions.has(aliasSymId, ResolutionKind.Type)) { - if (mapper === undefined) { + if (ctx.mapper === undefined) { reportCheckerDiagnostic( createDiagnostic({ code: "circular-alias-type", @@ -5446,7 +5514,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } pendingResolutions.start(aliasSymId, ResolutionKind.Type); - const type = checkNode(node.value, mapper); + const type = checkNode(ctx, node.value); if (type === null) { links.declaredType = errorType; return errorType; @@ -5456,7 +5524,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker links.declaredType = errorType; return errorType; } - linkType(links, type as any, mapper); + linkType(ctx, links, type as any); pendingResolutions.finish(aliasSymId, ResolutionKind.Type); return type; @@ -5511,7 +5579,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } } - function checkEnum(node: EnumStatementNode, mapper: TypeMapper | undefined): Type { + function checkEnum(ctx: CheckContext, node: EnumStatementNode): Type { const links = getSymbolLinks(node.symbol); if (!links.type) { const enumType: Enum = (links.type = createType({ @@ -5526,7 +5594,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker for (const member of node.members) { if (member.kind === SyntaxKind.EnumMember) { - const memberType = checkEnumMember(member, mapper, enumType); + const memberType = checkEnumMember(ctx, member, enumType); if (memberNames.has(memberType.name)) { reportCheckerDiagnostic( createDiagnostic({ @@ -5541,14 +5609,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker enumType.members.set(memberType.name, memberType); } else { const members = checkEnumSpreadMember( + ctx, node.symbol, enumType, member.target, - mapper, memberNames, ); for (const memberType of members) { - linkIndirectMember(node, memberType, mapper); + linkIndirectMember(ctx, node, memberType); enumType.members.set(memberType.name, memberType); } } @@ -5557,22 +5625,22 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const namespace = getParentNamespaceType(node); enumType.namespace = namespace; enumType.namespace?.enums.set(enumType.name!, enumType); - enumType.decorators = checkDecorators(enumType, node, mapper); - linkMapper(enumType, mapper); + enumType.decorators = checkDecorators(ctx, enumType, node); + linkMapper(enumType, ctx.mapper); finishType(enumType); } return links.type; } - function checkInterface(node: InterfaceStatementNode, mapper: TypeMapper | undefined): Interface { + function checkInterface(ctx: CheckContext, node: InterfaceStatementNode): Interface { const links = getSymbolLinks(node.symbol); - if (links.declaredType && mapper === undefined) { + if (links.declaredType && ctx.mapper === undefined) { // we're not instantiating this interface and we've already checked it return links.declaredType as Interface; } - checkTemplateDeclaration(node, mapper); + checkTemplateDeclaration(ctx, node); const interfaceType: Interface = createType({ kind: "Interface", @@ -5584,14 +5652,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker name: node.id.sv, }); - linkType(links, interfaceType, mapper); + linkType(ctx, links, interfaceType); - interfaceType.decorators = checkDecorators(interfaceType, node, mapper); + interfaceType.decorators = checkDecorators(ctx, interfaceType, node); - const ownMembers = checkInterfaceMembers(node, mapper, interfaceType); + const ownMembers = checkInterfaceMembers(ctx, node, interfaceType); for (const extendsNode of node.extends) { - const extendsType = getTypeForNode(extendsNode, mapper); + const extendsType = getTypeForNode(extendsNode, ctx.mapper); if (extendsType.kind !== "Interface") { reportCheckerDiagnostic( createDiagnostic({ code: "extends-interface", target: extendsNode }), @@ -5615,7 +5683,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker }); // Don't link it it is overritten if (!ownMembers.has(member.name)) { - linkIndirectMember(node, newMember, mapper); + linkIndirectMember(ctx, node, newMember); } // Clone deprecation information @@ -5630,22 +5698,22 @@ export function createChecker(program: Program, resolver: NameResolver): Checker interfaceType.operations.set(key, value); } - linkMapper(interfaceType, mapper); + linkMapper(interfaceType, ctx.mapper); - if (mapper === undefined) { + if (ctx.mapper === undefined) { interfaceType.namespace?.interfaces.set(interfaceType.name, interfaceType); } lateBindMemberContainer(interfaceType); lateBindMembers(interfaceType); return finishType(interfaceType, { - skipDecorators: !shouldRunDecorators(node, mapper), + skipDecorators: !shouldRunDecorators(ctx, node), }); } function checkInterfaceMembers( + ctx: CheckContext, node: InterfaceStatementNode, - mapper: TypeMapper | undefined, interfaceType: Interface, ): Map { const ownMembers = new Map(); @@ -5659,7 +5727,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } } for (const opNode of node.operations) { - const opType = checkOperation(opNode, mapper, interfaceType); + const opType = checkOperation(ctx, opNode, interfaceType); if (ownMembers.has(opType.name)) { reportCheckerDiagnostic( createDiagnostic({ @@ -5675,14 +5743,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return ownMembers; } - function checkUnion(node: UnionStatementNode, mapper: TypeMapper | undefined) { + function checkUnion(ctx: CheckContext, node: UnionStatementNode) { const links = getSymbolLinks(node.symbol); - if (links.declaredType && mapper === undefined) { + if (links.declaredType && ctx.mapper === undefined) { // we're not instantiating this union and we've already checked it return links.declaredType as Union; } - checkTemplateDeclaration(node, mapper); + checkTemplateDeclaration(ctx, node); const variants = createRekeyableMap(); const unionType: Union = createType({ @@ -5697,31 +5765,31 @@ export function createChecker(program: Program, resolver: NameResolver): Checker }, expression: false, }); - linkType(links, unionType, mapper); + linkType(ctx, links, unionType); - unionType.decorators = checkDecorators(unionType, node, mapper); + unionType.decorators = checkDecorators(ctx, unionType, node); - checkUnionVariants(unionType, node, variants, mapper); + checkUnionVariants(ctx, unionType, node, variants); - linkMapper(unionType, mapper); + linkMapper(unionType, ctx.mapper); - if (mapper === undefined) { + if (ctx.mapper === undefined) { unionType.namespace?.unions.set(unionType.name!, unionType); } lateBindMemberContainer(unionType); lateBindMembers(unionType); - return finishType(unionType, { skipDecorators: !shouldRunDecorators(node, mapper) }); + return finishType(unionType, { skipDecorators: !shouldRunDecorators(ctx, node) }); } function checkUnionVariants( + ctx: CheckContext, parentUnion: Union, node: UnionStatementNode, variants: Map, - mapper: TypeMapper | undefined, ) { for (const variantNode of node.options) { - const variantType = checkUnionVariant(variantNode, mapper); + const variantType = checkUnionVariant(ctx, variantNode); variantType.union = parentUnion; if (variants.has(variantType.name as string)) { reportCheckerDiagnostic( @@ -5737,18 +5805,15 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } } - function checkUnionVariant( - variantNode: UnionVariantNode, - mapper: TypeMapper | undefined, - ): UnionVariant { + function checkUnionVariant(ctx: CheckContext, variantNode: UnionVariantNode): UnionVariant { const links = getSymbolLinksForMember(variantNode); - if (links && links.declaredType && mapper === undefined) { + if (links && links.declaredType && ctx.mapper === undefined) { // we're not instantiating this union variant and we've already checked it return links.declaredType as UnionVariant; } const name = variantNode.id ? variantNode.id.sv : Symbol("name"); - const type = getTypeForNode(variantNode.value, mapper); + const type = getTypeForNode(variantNode.value, ctx.mapper); const variantType: UnionVariant = createType({ kind: "UnionVariant", name, @@ -5757,14 +5822,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker type, union: undefined as any, }); - variantType.decorators = checkDecorators(variantType, variantNode, mapper); + variantType.decorators = checkDecorators(ctx, variantType, variantNode); - linkMapper(variantType, mapper); + linkMapper(variantType, ctx.mapper); if (links) { - linkType(links, variantType, mapper); + linkType(ctx, links, variantType); } return finishType(variantType, { - skipDecorators: !shouldRunDecorators(variantNode.parent!, mapper), + skipDecorators: !shouldRunDecorators(ctx, variantNode.parent!), }); } @@ -5790,11 +5855,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return sym ? (getSymNode(sym) === node ? getSymbolLinks(sym) : undefined) : undefined; } - function checkEnumMember( - node: EnumMemberNode, - mapper: TypeMapper | undefined, - parentEnum?: Enum, - ): EnumMember { + function checkEnumMember(ctx: CheckContext, node: EnumMemberNode, parentEnum?: Enum): EnumMember { const name = node.id.sv; const links = getSymbolLinksForMember(node); if (links?.type) { @@ -5815,19 +5876,19 @@ export function createChecker(program: Program, resolver: NameResolver): Checker links.type = member; } - member.decorators = checkDecorators(member, node, mapper); + member.decorators = checkDecorators(ctx, member, node); return finishType(member); } function checkEnumSpreadMember( + ctx: CheckContext, parentEnumSym: Sym, parentEnum: Enum, targetNode: TypeReferenceNode, - mapper: TypeMapper | undefined, existingMemberNames: Set, ): EnumMember[] { const members: EnumMember[] = []; - const targetType = getTypeForNode(targetNode, mapper); + const targetType = getTypeForNode(targetNode, ctx.mapper); if (!isErrorType(targetType)) { if (targetType.kind !== "Enum") { @@ -6040,7 +6101,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker decorators: [], }); getSymbolLinks(sym).type = type; - type.decorators = checkAugmentDecorators(sym, type, undefined); + type.decorators = checkAugmentDecorators(CheckContext.default, sym, type); return finishType(type); } @@ -6173,7 +6234,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker if (docComment) { clone.decorators.push(createDocFromCommentDecorator("self", docComment)); } - for (const dec of checkAugmentDecorators(sym, clone, undefined)) { + for (const dec of checkAugmentDecorators(CheckContext.default, sym, clone)) { clone.decorators.push(dec); } } From 4ac027b0a80c7346d09db941584e618b7b5e6f39 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 4 Feb 2026 13:37:55 -0500 Subject: [PATCH 2/8] Implemented context logic, refactored, all tests passing. --- packages/compiler/src/core/checker.ts | 379 ++++++++++++------ .../compiler/test/checker/interface.test.ts | 12 +- packages/compiler/test/checker/model.test.ts | 47 ++- .../compiler/test/checker/templates.test.ts | 4 +- 4 files changed, 303 insertions(+), 139 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index aeda9b19eb3..b83c8de0999 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -165,89 +165,139 @@ import { export type CreateTypeProps = Omit; -/** - * Context passed through type checking operations. - * - * Check contexts are immutable. - */ -interface CheckContext< - Options = {}, - Mapper extends TypeMapper | undefined = TypeMapper | undefined, -> { - /** The type mapper, if any. */ - mapper: Mapper; +enum CheckFlags { + /** No flags set. */ + None = 0, + /** Currently checking within an uninstantiated template declaration. */ + InTemplateDeclaration = 1 << 0, +} - /** The set of active checking flags. */ +class CheckContext { + /** The type mapper associated with this context, if any. */ + mapper: Mapper; + /** The flags enabled in this context. */ flags: CheckFlags; + #templateParametersObserved: Set | undefined; + /** - * Options for the checker operation. + * Creates a new CheckContext from a type mapper. + * @param mapper - the type mapper */ - options: Options; + static from(mapper: TypeMapper): CheckContext; + /** + * Creates a new CheckContext with no mapper. + */ + static from(mapper: undefined): CheckContext; + /** + * Creates a new CheckContext from an optional TypeMapper. + * @param mapper + */ + static from(mapper: TypeMapper | undefined): CheckContext; + /** + * Copies an existing CheckContext. + * @param context + */ + static from(context: C): C; + /** + * Coerces a CheckContext from either a CheckContext or TypeMapper. + */ + static from(contextOrMapper: CheckContext | TypeMapper | undefined): CheckContext; + static from(contextOrMapper: CheckContext | TypeMapper | undefined): CheckContext { + if (contextOrMapper instanceof CheckContext) { + return contextOrMapper; + } + + return new CheckContext(contextOrMapper, CheckFlags.None); + } - /** Derives a new CheckContext with the given flags. */ - withFlags(flags: CheckFlags): CheckContext; + /** + * The default CheckContext to use at API entrypoints. + */ + static DEFAULT = new CheckContext(undefined, CheckFlags.None); - /** Derives a new CheckContext with the given mapper. */ - withMapper( - mapper: NewMapper, - ): CheckContext; + private constructor( + mapper: Mapper, + flags: CheckFlags, + templateParametersObserved?: Set, + ) { + this.mapper = mapper; + this.flags = flags; + this.#templateParametersObserved = templateParametersObserved; + Object.freeze(this); + } - /** Derives a new CheckContext with the given options. */ - withOptions( - options: NewOptions, - ): CheckContext; -} + /** + * Returns a new context with the given flags _added_ to the existing flags. + * + * @param flags - the flags to enable + * @returns a new CheckContext with the given flags enabled. + */ + withFlags(flags: CheckFlags): CheckContext { + return new CheckContext(this.mapper, this.flags | flags, this.#templateParametersObserved); + } -enum CheckFlags { - /** No flags set. */ - None = 0, - /** Currently checking within an uninstantiated template declaration. */ - InTemplateDeclaration = 1 << 0, -} + /** + * Returns a new context with the given flags disabled. + * + * @param flags - the flags to disable + * @returns a new CheckContext with the given flags disabled. + */ + maskFlags(flags: CheckFlags): CheckContext { + return new CheckContext(this.mapper, this.flags & ~flags, this.#templateParametersObserved); + } -const DEFAULT_CHECK_CONTEXT: CheckContext<{}, undefined> = Object.freeze({ - withFlags(flags: CheckFlags) { - return Object.freeze({ - withFlags: this.withFlags, - withMapper: this.withMapper, - withOptions: this.withOptions, - mapper: this.mapper, - flags, - options: this.options, - }); - }, - - withMapper(mapper: NewMapper) { - return Object.freeze({ - withFlags: this.withFlags, - withMapper: this.withMapper, - withOptions: this.withOptions, - mapper, - flags: this.flags, - options: this.options, - } as CheckContext<{}, NewMapper>); - }, - - withOptions(options: NewOptions) { - return Object.freeze({ - withFlags: this.withFlags, - withMapper: this.withMapper, - withOptions: this.withOptions, - mapper: this.mapper, - flags: this.flags, - options, - } as CheckContext); - }, + /** + * Returns a new context with the given mapper. + * + * @param mapper - the new type mapper, or undefined to clear the mapper + * @returns a new CheckContext with the given mapper. + */ + withMapper(mapper: NewMapper): CheckContext { + return new CheckContext(mapper, this.flags, this.#templateParametersObserved); + } - mapper: undefined, - flags: CheckFlags.None, - options: {}, -}); + /** + * Observes a template parameter within the current observation scope, if any. + * + * @param param - the TemplateParameter type instance to observe + */ + observeTemplateParameter(param: TemplateParameter): void { + this.#templateParametersObserved?.add(param); + } -const CheckContext = Object.freeze({ - default: DEFAULT_CHECK_CONTEXT, -} as const); + /** + * Returns a new CheckContext with a new (empty) template parameter observation scope. + * + * Call this when you need to observe template parameters used within a specific context. + * + * @returns a new CheckContext with an empty template parameter observation scope. + */ + enterTemplateObserverScope(): CheckContext { + return new CheckContext(this.mapper, this.flags, new Set()); + } + + /** + * Creates a new CheckContext with no template parameter observation enabled. + * + * Call this when the checker is moving from one declaration to another, where usage of template parameters + * from the next scope should not impact the usage from the previous scope. + * + * @returns a new CheckContext with no template parameter observation. + */ + exitTemplateObserverScope(): CheckContext { + return new CheckContext(this.mapper, this.flags, undefined); + } + + /** + * @returns true if the observer scope in this context has seen any template parameter usage. + */ + hasObservedTemplateParameters(): boolean { + return this.#templateParametersObserved !== undefined + ? this.#templateParametersObserved.size > 0 + : false; + } +} export interface Checker { /** @internal */ @@ -555,9 +605,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const sym = typespecNamespaceBinding?.exports?.get(name); compilerAssert(sym, `Unexpected missing symbol to std type "${name}"`); if (sym.flags & SymbolFlags.Model) { - checkModelStatement(CheckContext.default, sym!.declarations[0] as any); + checkModelStatement(CheckContext.DEFAULT, sym!.declarations[0] as any); } else { - checkScalar(CheckContext.default, sym.declarations[0] as any); + checkScalar(CheckContext.DEFAULT, sym.declarations[0] as any); } const loadedType = stdTypes[name]; @@ -599,7 +649,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker */ function checkMemberSym(ctx: CheckContext, sym: Sym): Type { const symbolLinks = getSymbolLinks(sym); - const memberContainer = getTypeForNode(getSymNode(sym.parent!), ctx.mapper); + const memberContainer = getTypeForNode(getSymNode(sym.parent!), ctx); const type = symbolLinks.declaredType ?? symbolLinks.type; if (type) { @@ -646,8 +696,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return entity; } - function getTypeForNode(node: Node, mapper?: TypeMapper): Type { - const ctx = CheckContext.default.withMapper(mapper); + function getTypeForNode(node: Node, mapperOrContext?: TypeMapper | CheckContext): Type { + const ctx = CheckContext.from(mapperOrContext); const entity = checkNode(ctx, node); if (entity === null) { return errorType; @@ -665,6 +715,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return errorType; } if (entity.kind === "TemplateParameter") { + // TODO/witemple was observing template parameter usage here, is that needed? if (entity.constraint?.valueType) { // means this template constraint will accept values reportCheckerDiagnostic( @@ -681,10 +732,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function getValueForNode( node: Node, - mapper?: TypeMapper, + mapperOrContext?: TypeMapper | CheckContext, constraint?: CheckValueConstraint, ): Value | null { - const ctx = CheckContext.default.withMapper(mapper); + const ctx = CheckContext.from(mapperOrContext); const initial = checkNode(ctx, node, constraint); if (initial === null) { return null; @@ -706,8 +757,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker entity.kind === "TemplateParameter" && entity.constraint?.valueType && entity.constraint.type === undefined && - mapper === undefined + ctx.mapper === undefined ) { + // We must also observe that the template parameter is used here. + // ctx.observeTemplateParameter(entity); return createValue( { entityKind: "Value", @@ -858,7 +911,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker mapper?: TypeMapper, constraint?: CheckConstraint | undefined, ): Type | Value | null { - const ctx = CheckContext.default.withMapper(mapper); + const ctx = CheckContext.from(mapper); const valueConstraint = extractValueOfConstraints(constraint); const entity = checkNode(ctx, node, valueConstraint); if (entity === null) { @@ -1028,7 +1081,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkTemplateParameterDeclaration( - ctx: CheckContext<{}, undefined>, + ctx: CheckContext, node: TemplateParameterDeclarationNode, ): TemplateParameter; function checkTemplateParameterDeclaration( @@ -1100,7 +1153,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function getResolvedTypeParameterDefault( - ctx: CheckContext<{}, TypeMapper>, + ctx: CheckContext, declaredType: TemplateParameter, node: TemplateParameterDeclarationNode, ): Type | Value | IndeterminateEntity | null | undefined { @@ -1210,7 +1263,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const oldDiagnosticHook = onCheckerDiagnostic; const diagnostics: Diagnostic[] = []; onCheckerDiagnostic = (x: Diagnostic) => diagnostics.push(x); - const entity = checkTypeOrValueReference(CheckContext.default, node, false); + const entity = checkTypeOrValueReference(CheckContext.DEFAULT, node, false); onCheckerDiagnostic = oldDiagnosticHook; return [entity === errorType ? undefined : entity, diagnostics]; } @@ -1221,7 +1274,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const oldDiagnosticHook = onCheckerDiagnostic; const diagnostics: Diagnostic[] = []; onCheckerDiagnostic = (x: Diagnostic) => diagnostics.push(x); - const type = checkTypeReference(CheckContext.default, node, false); + const type = checkTypeReference(CheckContext.DEFAULT, node, false); onCheckerDiagnostic = oldDiagnosticHook; return [type === errorType ? undefined : type, diagnostics]; } @@ -1306,7 +1359,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } const initMap = new Map( decls.map((decl) => { - const declaredType = checkTemplateParameterDeclaration(CheckContext.default, decl); + const declaredType = checkTemplateParameterDeclaration(CheckContext.DEFAULT, decl); positional.push(declaredType); params.set(decl.id.sv, declaredType); @@ -1521,6 +1574,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const entity = checkTypeOrValueReferenceSymbolWorker(ctx, sym, node, instantiateTemplates); if (entity !== null && isType(entity) && entity.kind === "TemplateParameter") { + ctx.observeTemplateParameter(entity); templateParameterUsageMap.set(entity.node!, true); } return entity; @@ -1567,6 +1621,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ) { const decl = sym.declarations[0] as TemplateableNode; if (!isTemplatedNode(decl)) { + // Not a templated node, and we are moving through a typeref to a new declaration. + // Therefore, we are no longer in a template declaration if we were before, and we are + // visiting a new declaration, so we exit the active template observer scope, if any. + const innerCtx = ctx + .maskFlags(CheckFlags.InTemplateDeclaration) + .exitTemplateObserverScope(); if (argumentNodes.length > 0) { reportCheckerDiagnostic( createDiagnostic({ @@ -1583,23 +1643,36 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } else if (symbolLinks.declaredType) { baseType = symbolLinks.declaredType; } else if (sym.flags & SymbolFlags.Member) { - baseType = checkMemberSym(ctx, sym); + baseType = checkMemberSym(innerCtx, sym); } else { - baseType = checkDeclaredTypeOrIndeterminate(ctx, sym, decl); + // + baseType = checkDeclaredTypeOrIndeterminate(innerCtx, sym, decl); } } else { - const declaredType = getOrCheckDeclaredType(ctx, sym, decl); + // Checking the declaration to ensure we have the template itself, so we don't need the mapper. + const declaredType = getOrCheckDeclaredType(ctx.withMapper(undefined), sym, decl); const templateParameters = decl.templateParameters; + const instantiationArgsCtx = ctx.enterTemplateObserverScope(); const instantiation = checkTemplateInstantiationArgs( - ctx, + instantiationArgsCtx, node, argumentNodes, templateParameters, declaredType.templateMapper, ); + // If we didn't see any template parameters during argument checking, then this type reference is "pure" + // and we can mask the InTemplateDeclaration flag downstream. In either case, we are going to a new declaration + // so we exit the active template observer scope, if any. + const innerCtx = ( + instantiationArgsCtx.hasObservedTemplateParameters() + ? ctx + : ctx.maskFlags(CheckFlags.InTemplateDeclaration) + ).exitTemplateObserverScope(); + baseType = getOrInstantiateTemplate( + innerCtx, decl, [...instantiation.keys()], [...instantiation.values()], @@ -1636,11 +1709,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } else if (symbolLinks.declaredType) { baseType = symbolLinks.declaredType; } else { + // TODO/witemple - what kind of context updates do we need to make here, if any? if (sym.flags & SymbolFlags.Member) { baseType = checkMemberSym(ctx, sym); } else { // don't have a cached type for this symbol, so go grab it and cache it - baseType = getTypeForNode(symNode, ctx.mapper); + baseType = getTypeForNode(symNode, ctx); symbolLinks.type = baseType; } } @@ -1729,6 +1803,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function getOrInstantiateTemplate( + ctx: CheckContext, templateNode: TemplateableNode, params: TemplateParameter[], args: (Type | Value | IndeterminateEntity)[], @@ -1766,7 +1841,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return cached; } if (instantiateTempalates) { - return instantiateTemplate(symbolLinks.instantiations, templateNode, params, mapper); + return instantiateTemplate( + ctx.withMapper(mapper), + symbolLinks.instantiations, + templateNode, + params, + ); } else { return errorType; } @@ -1781,14 +1861,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker * are ever in scope at once. */ function instantiateTemplate( + ctx: CheckContext, instantiations: TypeInstantiationMap, templateNode: TemplateableNode, params: TemplateParameter[], - mapper: TypeMapper, ): Type { - const type = getTypeForNode(templateNode, mapper); - if (!instantiations.get(mapper.args)) { - instantiations.set(mapper.args, type); + const type = getTypeForNode(templateNode, ctx); + if (!instantiations.get(ctx.mapper.args)) { + instantiations.set(ctx.mapper.args, type); } if (type.kind === "Model") { type.templateNode = templateNode; @@ -1871,7 +1951,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker }); for (const o of node.options) { - const type = getTypeForNode(o, ctx.mapper); + const type = getTypeForNode(o, ctx); // The type `A | never` is just `A` if (type === neverType) { @@ -2203,6 +2283,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const arrayNode: ModelStatementNode = arrayType.node as any; const param: TemplateParameter = getTypeForNode(arrayNode.templateParameters[0]) as any; return getOrInstantiateTemplate( + ctx, arrayNode, [param], [elementType], @@ -2262,7 +2343,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker // namespaces created from TypeSpec scripts don't have decorators if (sourceNode.kind !== SyntaxKind.NamespaceStatement) continue; type.decorators = type.decorators.concat( - checkDecorators(CheckContext.default, type, sourceNode), + checkDecorators(CheckContext.DEFAULT, type, sourceNode), ); } finishType(type); @@ -2364,6 +2445,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const inInterface = node.parent?.kind === SyntaxKind.InterfaceStatement; const symbol = inInterface ? getSymbolForMember(node) : node.symbol; const links = symbol && getSymbolLinks(symbol); + if (links) { if (links.declaredType && ctx.mapper === undefined) { // we're not instantiating this operation and we've already checked it @@ -2385,6 +2467,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ctx = ctx.withMapper({ ...ctx.mapper, partial: true }); } + if ((ctx.mapper === undefined || ctx.mapper.partial) && node.templateParameters.length > 0) { + ctx = ctx.withFlags(CheckFlags.InTemplateDeclaration); + } + const namespace = getParentNamespaceType(node); const name = node.id.sv; @@ -2424,12 +2510,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker operationType.parameters.namespace = namespace; operationType.decorators.push(...checkDecorators(ctx, operationType, node)); - const runDecorators = - parent.kind === SyntaxKind.InterfaceStatement - ? shouldRunDecorators(ctx, parent) && shouldRunDecorators(ctx, node) - : shouldRunDecorators(ctx, node); - return finishType(operationType, { skipDecorators: !runDecorators }); + return finishType(operationType, { + skipDecorators: !!(ctx.flags & CheckFlags.InTemplateDeclaration), + }); } // Is this a definition or reference? if (node.signature.kind === SyntaxKind.OperationSignatureReference) { @@ -2466,8 +2550,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker finishOperation(); } } else { - operationType.parameters = getTypeForNode(node.signature.parameters, ctx.mapper) as Model; - operationType.returnType = getTypeForNode(node.signature.returnType, ctx.mapper); + operationType.parameters = getTypeForNode(node.signature.parameters, ctx) as Model; + operationType.returnType = getTypeForNode(node.signature.returnType, ctx); ensureResolved([operationType.parameters], operationType, () => { finishOperation(); }); @@ -2511,7 +2595,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } // Resolve the base operation type - const baseOperation = getTypeForNode(opReference, ctx.mapper); + const baseOperation = getTypeForNode(opReference, ctx); if (opSymId) { pendingResolutions.finish(opSymId, ResolutionKind.BaseType); } @@ -2541,7 +2625,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return createAndFinishType({ kind: "Tuple", node: node, - values: node.values.map((v) => getTypeForNode(v, ctx.mapper)), + values: node.values.map((v) => getTypeForNode(v, ctx)), }); } @@ -2550,7 +2634,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function resolveRelatedSymbols(id: IdentifierNode, mapper?: TypeMapper): Sym[] | undefined { - const ctx = CheckContext.default.withMapper(mapper); + const ctx = CheckContext.from(mapper); let sym: Sym | undefined; const { node, kind } = getIdentifierContext(id); @@ -2771,7 +2855,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const refNode = node.parent.parent; const decl = getTemplateDeclarationsForArgument( // We should be giving the argument so the mapper here should be undefined - CheckContext.default, + CheckContext.DEFAULT, argNode, ); @@ -2816,7 +2900,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return undefined; } - const ctorType = checkCallExpressionTarget(CheckContext.default, callExpNode); + const ctorType = checkCallExpressionTarget(CheckContext.DEFAULT, callExpNode); if (ctorType?.kind !== "ScalarConstructor") { return undefined; @@ -2913,7 +2997,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return completions; // cannot complete, name can be chosen arbitrarily case IdentifierKind.TemplateArgument: { const templates = getTemplateDeclarationsForArgument( - CheckContext.default, + CheckContext.DEFAULT, ancestor as TemplateArgumentNode, ); @@ -2978,7 +3062,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker if (base) { if (base.flags & SymbolFlags.Alias) { - base = getAliasedSymbol(CheckContext.default, base); + base = getAliasedSymbol(CheckContext.DEFAULT, base); } if (base) { @@ -3011,7 +3095,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ancestor.parent.name === undefined ) { const templates = getTemplateDeclarationsForArgument( - CheckContext.default, + CheckContext.DEFAULT, ancestor.parent as TemplateArgumentNode, ); @@ -3336,7 +3420,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } // Otherwise for templates we need to get the type and retrieve the late bound symbol. - const aliasType = getTypeForNode(node as AliasStatementNode, ctx.mapper); + const aliasType = getTypeForNode(node as AliasStatementNode, ctx); return lateBindContainer(aliasType, aliasSymbol); } @@ -3646,7 +3730,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function checkSourceFile(file: TypeSpecScriptNode) { for (const statement of file.statements) { - checkNode(CheckContext.default, statement, undefined); + checkNode(CheckContext.DEFAULT, statement, undefined); } } @@ -3675,6 +3759,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function checkModelStatement(ctx: CheckContext, node: ModelStatementNode): Model { const links = getSymbolLinks(node.symbol); + if (ctx.mapper === undefined && node.templateParameters.length > 0) { + ctx = ctx.withFlags(CheckFlags.InTemplateDeclaration); + } + if (links.declaredType && ctx.mapper === undefined) { // we're not instantiating this model and we've already checked it return links.declaredType as any; @@ -3759,7 +3847,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker decorators.push(...checkDecorators(ctx, type, node)); linkMapper(type, ctx.mapper); - finishType(type, { skipDecorators: !shouldRunDecorators(ctx, node) }); + finishType(type, { skipDecorators: !!(ctx.flags & CheckFlags.InTemplateDeclaration) }); lateBindMemberContainer(type); lateBindMembers(type); @@ -3780,6 +3868,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function shouldRunDecorators(ctx: CheckContext, node: TemplateDeclarationNode) { + if (ctx.flags & CheckFlags.InTemplateDeclaration) { + return false; + } + // Node is not a template we should create the type. if (node.templateParameters.length === 0) { return true; @@ -3818,7 +3910,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker type, () => { checkModelProperties(ctx, node, properties, type); - finishType(type); + finishType(type, { skipDecorators: !!(ctx.flags & CheckFlags.InTemplateDeclaration) }); }, ); return type; @@ -4683,7 +4775,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } return undefined; } - const heritageType = getTypeForNode(heritageRef, ctx.mapper); + const heritageType = getTypeForNode(heritageRef, ctx); pendingResolutions.finish(modelSymId, ResolutionKind.BaseType); if (isErrorType(heritageType)) { compilerAssert(program.hasError(), "Should already have reported an error.", heritageRef); @@ -4743,7 +4835,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } return undefined; } - isType = getTypeForNode(isExpr, ctx.mapper); + isType = getTypeForNode(isExpr, ctx); } else { reportCheckerDiagnostic(createDiagnostic({ code: "is-model", target: isExpr })); return undefined; @@ -4787,7 +4879,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } return undefined; } - const type = getTypeForNode(target, ctx.mapper); + const type = getTypeForNode(target, ctx); return type; } @@ -4797,7 +4889,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker targetNode: TypeReferenceNode, parentModel: Model, ): [ModelProperty[], ModelIndexer | undefined] { - const targetType = getTypeForNode(targetNode, ctx.mapper); + const targetType = getTypeForNode(targetNode, ctx); if (targetType.kind === "TemplateParameter" || isErrorType(targetType)) { return [[], undefined]; @@ -4889,7 +4981,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker type.type = errorType; } else { pendingResolutions.start(sym, ResolutionKind.Type); - type.type = getTypeForNode(prop.value, ctx.mapper); + type.type = getTypeForNode(prop.value, ctx); if (prop.default) { const defaultValue = checkDefaultValue(ctx, prop.default, type.type); if (defaultValue !== null) { @@ -4915,7 +5007,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } pendingResolutions.finish(sym, ResolutionKind.Type); - return finishType(type, { skipDecorators: !runDecorators }); + return finishType(type, { skipDecorators: !!(ctx.flags & CheckFlags.InTemplateDeclaration) }); } function createDocFromCommentDecorator(key: "self" | "returns" | "errors", doc: string) { @@ -5349,6 +5441,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function checkScalar(ctx: CheckContext, node: ScalarStatementNode): Scalar { const links = getSymbolLinks(node.symbol); + if (ctx.mapper === undefined && node.templateParameters.length > 0) { + // This is a templated declaration and we are not instantiating it, so we need to update the flags. + ctx = ctx.withFlags(CheckFlags.InTemplateDeclaration); + } + if (links.declaredType && ctx.mapper === undefined) { // we're not instantiating this model and we've already checked it return links.declaredType as any; @@ -5386,7 +5483,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker stdTypes[type.name as any as keyof StdTypes] = type as any; } - return finishType(type, { skipDecorators: !shouldRunDecorators(ctx, node) }); + return finishType(type, { skipDecorators: !!(ctx.flags & CheckFlags.InTemplateDeclaration) }); } function checkScalarExtends( @@ -5411,7 +5508,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } return undefined; } - const extendsType = getTypeForNode(extendsRef, ctx.mapper); + const extendsType = getTypeForNode(extendsRef, ctx); pendingResolutions.finish(symId, ResolutionKind.BaseType); if (isErrorType(extendsType)) { compilerAssert(program.hasError(), "Should already have reported an error.", extendsRef); @@ -5486,13 +5583,18 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } return finishType(member, { - skipDecorators: !shouldRunDecorators(ctx, node.parent!), + skipDecorators: !!(ctx.flags & CheckFlags.InTemplateDeclaration), }); } function checkAlias(ctx: CheckContext, node: AliasStatementNode): Type | IndeterminateEntity { const links = getSymbolLinks(node.symbol); + if (ctx.mapper === undefined && node.templateParameters.length > 0) { + // This is a templated declaration and we are not instantiating it, so we need to update the flags. + ctx = ctx.withFlags(CheckFlags.InTemplateDeclaration); + } + if (links.declaredType && ctx.mapper === undefined) { return links.declaredType; } @@ -5636,6 +5738,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function checkInterface(ctx: CheckContext, node: InterfaceStatementNode): Interface { const links = getSymbolLinks(node.symbol); + if (ctx.mapper === undefined && node.templateParameters.length > 0) { + // This is a templated declaration and we are not instantiating it, so we need to update the flags. + ctx = ctx.withFlags(CheckFlags.InTemplateDeclaration); + } + if (links.declaredType && ctx.mapper === undefined) { // we're not instantiating this interface and we've already checked it return links.declaredType as Interface; @@ -5659,7 +5766,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const ownMembers = checkInterfaceMembers(ctx, node, interfaceType); for (const extendsNode of node.extends) { - const extendsType = getTypeForNode(extendsNode, ctx.mapper); + const extendsType = getTypeForNode(extendsNode, ctx); if (extendsType.kind !== "Interface") { reportCheckerDiagnostic( createDiagnostic({ code: "extends-interface", target: extendsNode }), @@ -5707,7 +5814,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker lateBindMemberContainer(interfaceType); lateBindMembers(interfaceType); return finishType(interfaceType, { - skipDecorators: !shouldRunDecorators(ctx, node), + skipDecorators: !!(ctx.flags & CheckFlags.InTemplateDeclaration), }); } @@ -5746,6 +5853,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function checkUnion(ctx: CheckContext, node: UnionStatementNode) { const links = getSymbolLinks(node.symbol); + if (ctx.mapper === undefined && node.templateParameters.length > 0) { + ctx = ctx.withFlags(CheckFlags.InTemplateDeclaration); + } + if (links.declaredType && ctx.mapper === undefined) { // we're not instantiating this union and we've already checked it return links.declaredType as Union; @@ -5779,7 +5890,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker lateBindMemberContainer(unionType); lateBindMembers(unionType); - return finishType(unionType, { skipDecorators: !shouldRunDecorators(ctx, node) }); + return finishType(unionType, { + skipDecorators: !!(ctx.flags & CheckFlags.InTemplateDeclaration), + }); } function checkUnionVariants( @@ -5813,7 +5926,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } const name = variantNode.id ? variantNode.id.sv : Symbol("name"); - const type = getTypeForNode(variantNode.value, ctx.mapper); + const type = getTypeForNode(variantNode.value, ctx); const variantType: UnionVariant = createType({ kind: "UnionVariant", name, @@ -5829,7 +5942,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker linkType(ctx, links, variantType); } return finishType(variantType, { - skipDecorators: !shouldRunDecorators(ctx, variantNode.parent!), + skipDecorators: !!(ctx.flags & CheckFlags.InTemplateDeclaration), }); } @@ -5888,7 +6001,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker existingMemberNames: Set, ): EnumMember[] { const members: EnumMember[] = []; - const targetType = getTypeForNode(targetNode, ctx.mapper); + const targetType = getTypeForNode(targetNode, ctx); if (!isErrorType(targetType)) { if (targetType.kind !== "Enum") { @@ -6101,7 +6214,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker decorators: [], }); getSymbolLinks(sym).type = type; - type.decorators = checkAugmentDecorators(CheckContext.default, sym, type); + type.decorators = checkAugmentDecorators(CheckContext.DEFAULT, sym, type); return finishType(type); } @@ -6234,7 +6347,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker if (docComment) { clone.decorators.push(createDocFromCommentDecorator("self", docComment)); } - for (const dec of checkAugmentDecorators(CheckContext.default, sym, clone)) { + for (const dec of checkAugmentDecorators(CheckContext.DEFAULT, sym, clone)) { clone.decorators.push(dec); } } diff --git a/packages/compiler/test/checker/interface.test.ts b/packages/compiler/test/checker/interface.test.ts index 367ae54782f..65a5fa8d712 100644 --- a/packages/compiler/test/checker/interface.test.ts +++ b/packages/compiler/test/checker/interface.test.ts @@ -443,14 +443,18 @@ describe("compiler: interfaces", () => { }); it("instantiating an templated interface doesn't finish template operation inside", async () => { - const $track = vi.fn(); - testHost.addJsFile("dec.js", { $track }); + const _track = vi.fn(); + testHost.addJsFile("dec.js", { + $track() { + _track(); + }, + }); testHost.addTypeSpecFile( "main.tsp", ` import "./dec.js"; - interface Base { + interface Base { @track bar(input: A): B; } @@ -458,7 +462,7 @@ describe("compiler: interfaces", () => { `, ); await testHost.compile("./"); - expect($track).not.toHaveBeenCalled(); + expect(_track).not.toHaveBeenCalled(); }); it("templated interface extending another templated interface doesn't run decorator on extended interface operations", async () => { diff --git a/packages/compiler/test/checker/model.test.ts b/packages/compiler/test/checker/model.test.ts index 46f9d7a63a4..bc56d8e6916 100644 --- a/packages/compiler/test/checker/model.test.ts +++ b/packages/compiler/test/checker/model.test.ts @@ -1,5 +1,5 @@ import { deepStrictEqual, match, ok, strictEqual } from "assert"; -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { isTemplateDeclaration } from "../../src/core/type-utils.js"; import { Model, ModelProperty, SyntaxKind, Type } from "../../src/core/types.js"; import { @@ -1076,6 +1076,51 @@ describe("compiler: models", () => { strictEqual(((C as Model).properties.get("c")?.type as any).name, "int32"); strictEqual(((C as Model).properties.get("b")?.type as any).name, "B"); }); + + it("resolves a recursive template model when the recursion is also templated", async () => { + const $observe = vi.fn(); + testHost.addJsFile("utils.js", { + $observe, + }); + testHost.addTypeSpecFile( + "main.tsp", + ` + import "./utils.js"; + + model A { + b: T; + c: C; + } + + @observe + model C is A { + d: U; + } + + @test + model Result is A; + `, + ); + + const { Result } = (await testHost.compile("main.tsp")) as { Result: Model | undefined }; + + ok(Result); + strictEqual(Result.properties.size, 2); + strictEqual((Result.properties.get("b")?.type as any).name, "boolean"); + const cProp = Result.properties.get("c")?.type; + ok(cProp); + ok(cProp.kind === "Model"); + strictEqual(cProp.properties.size, 3); + strictEqual((cProp.properties.get("b")?.type as any).name, "int32"); + strictEqual((cProp.properties.get("c")?.type as any).name, "C"); + strictEqual((cProp.properties.get("d")?.type as any).name, "string"); + + // Just checking that the inner layer is identical + const innerCProp = cProp.properties.get("c")?.type; + strictEqual(innerCProp, cProp); + + expect($observe).toHaveBeenCalledTimes(1); + }); }); describe("spread", () => { diff --git a/packages/compiler/test/checker/templates.test.ts b/packages/compiler/test/checker/templates.test.ts index 1d06680fd6e..9597de6d11d 100644 --- a/packages/compiler/test/checker/templates.test.ts +++ b/packages/compiler/test/checker/templates.test.ts @@ -629,7 +629,9 @@ describe("compiler: templates", () => { describe("doesn't run decorators when checking template declarations", () => { async function expectMarkDecoratorNotCalled(code: string) { testHost.addJsFile("mark.js", { - $mark: () => fail("Should not have called decorator"), + $mark: () => { + fail("Should not have called decorator"); + }, }); testHost.addTypeSpecFile( From 63d07c2e8cbeac5ad0856fd636f16741f3a0e503 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 4 Feb 2026 16:12:48 -0500 Subject: [PATCH 3/8] More tests --- packages/compiler/test/checker/model.test.ts | 68 +++++++++++++++++++ .../compiler/test/checker/templates.test.ts | 40 +++++++++++ 2 files changed, 108 insertions(+) diff --git a/packages/compiler/test/checker/model.test.ts b/packages/compiler/test/checker/model.test.ts index bc56d8e6916..5149d7e810a 100644 --- a/packages/compiler/test/checker/model.test.ts +++ b/packages/compiler/test/checker/model.test.ts @@ -1121,6 +1121,74 @@ describe("compiler: models", () => { expect($observe).toHaveBeenCalledTimes(1); }); + + it("resolves a cyclic recursion with a property aliased to a recursive spread", async () => { + const $observe = vi.fn(); + testHost.addJsFile("utils.js", { + $observe, + }); + testHost.addTypeSpecFile( + "main.tsp", + ` + import "./utils.js"; + + model X { + prop: T; + y: Y; + } + + model Y is X { + extra: Z; + } + + alias Z = { + @observe foo: T; + ...X + }; + + @test model Result is X; + `, + ); + + const { Result } = (await testHost.compile("main.tsp")) as { Result: Model | undefined }; + + ok(Result); + strictEqual(Result.properties.size, 2); + strictEqual((Result.properties.get("prop")?.type as any).name, "int32"); + const yProp = Result.properties.get("y")?.type; + ok(yProp); + ok(yProp.kind === "Model"); + strictEqual(yProp.properties.size, 3); + strictEqual((yProp.properties.get("prop")?.type as any).name, "string"); + const zProp = yProp.properties.get("extra")?.type; + ok(zProp); + ok(zProp.kind === "Model"); + strictEqual(zProp.properties.size, 3); + strictEqual((zProp.properties.get("foo")?.type as any).name, "int32"); + strictEqual((zProp.properties.get("y")?.type as any).name, "Y"); + strictEqual((zProp.properties.get("prop")?.type as any).name, "string"); + + const innerXFromZ = zProp.properties.get("y")?.type; + ok(innerXFromZ); + ok(innerXFromZ.kind === "Model"); + strictEqual(innerXFromZ.properties.size, 3); + strictEqual((innerXFromZ.properties.get("prop")?.type as any).name, "string"); + const innerYFromZ = innerXFromZ.properties.get("y")?.type; + ok(innerYFromZ); + ok(innerYFromZ.kind === "Model"); + strictEqual(innerYFromZ.properties.size, 3); + strictEqual((innerYFromZ.properties.get("prop")?.type as any).name, "string"); + const innerZFromZ = innerYFromZ.properties.get("extra")?.type; + ok(innerZFromZ); + ok(innerZFromZ.kind === "Model"); + strictEqual(innerZFromZ.properties.size, 3); + strictEqual((innerZFromZ.properties.get("foo")?.type as any).name, "string"); + strictEqual((innerZFromZ.properties.get("y")?.type as any).name, "Y"); + strictEqual((innerZFromZ.properties.get("prop")?.type as any).name, "string"); + + // Called twice, once for Z and once for Z + expect($observe).toHaveBeenCalledTimes(2); + }); }); describe("spread", () => { diff --git a/packages/compiler/test/checker/templates.test.ts b/packages/compiler/test/checker/templates.test.ts index 9597de6d11d..3a878ac75f6 100644 --- a/packages/compiler/test/checker/templates.test.ts +++ b/packages/compiler/test/checker/templates.test.ts @@ -673,6 +673,46 @@ describe("compiler: templates", () => { `); }); + describe("within aliases", () => { + it("on model property", async () => { + await expectMarkDecoratorNotCalled(` + alias Foo = { + @mark(T) prop: string; + }; + `); + }); + + it("on model property, when no argument is provided", async () => { + await expectMarkDecoratorNotCalled(` + alias Foo = { + @mark prop: string; + }; + `); + }); + + it("when instantiation is indirect", async () => { + await expectMarkDecoratorNotCalled(` + alias Bar = Foo; + + alias Foo = { + @mark(T) prop: string; + }; + `); + }); + + it("when instantiation is indirect and nested", async () => { + await expectMarkDecoratorNotCalled(` + alias Bar = { + nested: Foo; + }; + + alias Foo = { + @mark(T) prop: string; + }; + `); + }); + }); + describe("on model properties", () => { it("under model", async () => { await expectMarkDecoratorNotCalled(` From 9f7c02b41290fdf3255b5c66bbe05ce2f26d6ec1 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 4 Feb 2026 16:19:08 -0500 Subject: [PATCH 4/8] chronus --- .../changes/witemple-msft-check-ctx-2026-1-4-16-13-31.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/witemple-msft-check-ctx-2026-1-4-16-13-31.md diff --git a/.chronus/changes/witemple-msft-check-ctx-2026-1-4-16-13-31.md b/.chronus/changes/witemple-msft-check-ctx-2026-1-4-16-13-31.md new file mode 100644 index 00000000000..601ff7c9519 --- /dev/null +++ b/.chronus/changes/witemple-msft-check-ctx-2026-1-4-16-13-31.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Fixed several checking errors around template instantiations that could cause TemplateParameter instances to leak into decorator calls. \ No newline at end of file From 8af4efeda052ca930c3662dffc35d43c0298ca7b Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 4 Feb 2026 16:27:52 -0500 Subject: [PATCH 5/8] Reworked a bit of logic. --- packages/compiler/src/core/checker.ts | 77 ++++++++++++++------------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index b83c8de0999..89532b4a577 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -136,7 +136,6 @@ import { SymbolTable, SyntaxKind, TemplateArgumentNode, - TemplateDeclarationNode, TemplateParameter, TemplateParameterDeclarationNode, TemplateableNode, @@ -247,6 +246,13 @@ class CheckContext isValue(t) || t.entityKind === "Indeterminate" || t.kind !== "TemplateParameter", - ) - ); - } + // function shouldRunDecorators(ctx: CheckContext, node: TemplateDeclarationNode) { + // if (ctx.flags & CheckFlags.InTemplateDeclaration) { + // return false; + // } + + // // Node is not a template we should create the type. + // if (node.templateParameters.length === 0) { + // return true; + // } + // // There is no mapper so we shouldn't be instantiating the template. + // if (ctx.mapper === undefined) { + // return false; + // } + + // // Some of the mapper args are still template parameter so we shouldn't create the type. + // return ( + // !ctx.mapper.partial && + // ctx.mapper.args.every( + // (t) => isValue(t) || t.entityKind === "Indeterminate" || t.kind !== "TemplateParameter", + // ) + // ); + // } function checkModelExpression(ctx: CheckContext, node: ModelExpressionNode) { const links = getSymbolLinks(node.symbol); @@ -3910,7 +3914,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker type, () => { checkModelProperties(ctx, node, properties, type); - finishType(type, { skipDecorators: !!(ctx.flags & CheckFlags.InTemplateDeclaration) }); + finishType(type, { skipDecorators: ctx.hasFlags(CheckFlags.InTemplateDeclaration) }); }, ); return type; @@ -4997,17 +5001,16 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const parentTemplate = getParentTemplateNode(prop); linkMapper(type, ctx.mapper); - let runDecorators = false; - if (!parentTemplate || shouldRunDecorators(ctx, parentTemplate)) { + const shouldRunDecorators = !ctx.hasFlags(CheckFlags.InTemplateDeclaration); + if (!parentTemplate || shouldRunDecorators) { const docComment = docFromCommentForSym.get(sym); if (docComment) { type.decorators.unshift(createDocFromCommentDecorator("self", docComment)); } - runDecorators = true; } pendingResolutions.finish(sym, ResolutionKind.Type); - return finishType(type, { skipDecorators: !!(ctx.flags & CheckFlags.InTemplateDeclaration) }); + return finishType(type, { skipDecorators: !shouldRunDecorators }); } function createDocFromCommentDecorator(key: "self" | "returns" | "errors", doc: string) { @@ -5483,7 +5486,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker stdTypes[type.name as any as keyof StdTypes] = type as any; } - return finishType(type, { skipDecorators: !!(ctx.flags & CheckFlags.InTemplateDeclaration) }); + return finishType(type, { skipDecorators: ctx.hasFlags(CheckFlags.InTemplateDeclaration) }); } function checkScalarExtends( @@ -5583,7 +5586,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } return finishType(member, { - skipDecorators: !!(ctx.flags & CheckFlags.InTemplateDeclaration), + skipDecorators: ctx.hasFlags(CheckFlags.InTemplateDeclaration), }); } @@ -5814,7 +5817,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker lateBindMemberContainer(interfaceType); lateBindMembers(interfaceType); return finishType(interfaceType, { - skipDecorators: !!(ctx.flags & CheckFlags.InTemplateDeclaration), + skipDecorators: ctx.hasFlags(CheckFlags.InTemplateDeclaration), }); } @@ -5891,7 +5894,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker lateBindMemberContainer(unionType); lateBindMembers(unionType); return finishType(unionType, { - skipDecorators: !!(ctx.flags & CheckFlags.InTemplateDeclaration), + skipDecorators: ctx.hasFlags(CheckFlags.InTemplateDeclaration), }); } @@ -5942,7 +5945,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker linkType(ctx, links, variantType); } return finishType(variantType, { - skipDecorators: !!(ctx.flags & CheckFlags.InTemplateDeclaration), + skipDecorators: ctx.hasFlags(CheckFlags.InTemplateDeclaration), }); } From 4d94cb386bb872c48e92f1748e3066657565d5fb Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 4 Feb 2026 16:42:01 -0500 Subject: [PATCH 6/8] Remove shouldRunDecorators --- packages/compiler/src/core/checker.ts | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 89532b4a577..eb817b47856 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3871,29 +3871,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return type; } - // function shouldRunDecorators(ctx: CheckContext, node: TemplateDeclarationNode) { - // if (ctx.flags & CheckFlags.InTemplateDeclaration) { - // return false; - // } - - // // Node is not a template we should create the type. - // if (node.templateParameters.length === 0) { - // return true; - // } - // // There is no mapper so we shouldn't be instantiating the template. - // if (ctx.mapper === undefined) { - // return false; - // } - - // // Some of the mapper args are still template parameter so we shouldn't create the type. - // return ( - // !ctx.mapper.partial && - // ctx.mapper.args.every( - // (t) => isValue(t) || t.entityKind === "Indeterminate" || t.kind !== "TemplateParameter", - // ) - // ); - // } - function checkModelExpression(ctx: CheckContext, node: ModelExpressionNode) { const links = getSymbolLinks(node.symbol); From cdf331afc7323342a697fca1abea4576636bf770 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 5 Feb 2026 13:14:10 -0500 Subject: [PATCH 7/8] Repaired some cases where we were still using mapper instead of ctx. --- packages/compiler/src/core/checker.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index eb817b47856..95053495a2f 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1888,7 +1888,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const values: Type[] = []; const types: Type[] = []; for (const option of node.options) { - const [kind, type] = getTypeOrValueOfTypeForNode(option, ctx.mapper); + const [kind, type] = getTypeOrValueOfTypeForNode(ctx, option); if (kind === "value") { values.push(type); } else { @@ -1998,7 +1998,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } const intersection: Model = initModel(node); - const options = node.options.map((o): [Expression, Type] => [o, getTypeForNode(o, ctx.mapper)]); + const options = node.options.map((o): [Expression, Type] => [o, getTypeForNode(o, ctx)]); ensureResolved( options.map(([, type]) => type), @@ -2165,13 +2165,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return parameterType; } - function getTypeOrValueOfTypeForNode(node: Node, mapper?: TypeMapper): ["type" | "value", Type] { + function getTypeOrValueOfTypeForNode(ctx: CheckContext, node: Node): ["type" | "value", Type] { switch (node.kind) { case SyntaxKind.ValueOfExpression: - const target = getTypeForNode(node.target, mapper); + const target = getTypeForNode(node.target, ctx); return ["value", target]; default: - return ["type", getTypeForNode(node, mapper)]; + return ["type", getTypeForNode(node, ctx)]; } } @@ -2183,7 +2183,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker case SyntaxKind.UnionExpression: return checkMixedParameterConstraintUnion(ctx, node); default: - const [kind, entity] = getTypeOrValueOfTypeForNode(node, ctx.mapper); + const [kind, entity] = getTypeOrValueOfTypeForNode(ctx, node); return { entityKind: "MixedParameterConstraint", node: node, @@ -2282,7 +2282,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkArrayExpression(ctx: CheckContext, node: ArrayExpressionNode): Model { - const elementType = getTypeForNode(node.elementType, ctx.mapper); + const elementType = getTypeForNode(node.elementType, ctx); const arrayType = getStdType("Array"); const arrayNode: ModelStatementNode = arrayType.node as any; const param: TemplateParameter = getTypeForNode(arrayNode.templateParameters[0]) as any; From f48892a46bd216e8d8f4895b6022a93768fca1ee Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 5 Feb 2026 13:14:38 -0500 Subject: [PATCH 8/8] Implemented instantiation logic for checking ArrayExpressions, which act as template instantiations. --- packages/compiler/src/core/checker.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 95053495a2f..6bc130f0af5 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -2282,12 +2282,18 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkArrayExpression(ctx: CheckContext, node: ArrayExpressionNode): Model { - const elementType = getTypeForNode(node.elementType, ctx); + const elementCtx = ctx.enterTemplateObserverScope(); + const elementType = getTypeForNode(node.elementType, elementCtx); + + const instantiationCtx = elementCtx.hasObservedTemplateParameters() + ? ctx + : ctx.maskFlags(CheckFlags.InTemplateDeclaration); + const arrayType = getStdType("Array"); const arrayNode: ModelStatementNode = arrayType.node as any; const param: TemplateParameter = getTypeForNode(arrayNode.templateParameters[0]) as any; return getOrInstantiateTemplate( - ctx, + instantiationCtx, arrayNode, [param], [elementType],