From 910dcb6d6aaf0fd5f592cda00a1f45c5ed3e71c1 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Tue, 7 Apr 2026 11:53:00 -0700 Subject: [PATCH] refactor(compiler-cli): decouple TemplateSymbolBuilder from ts.TypeChecker This updates the SymbolBuilder to no longer use ts.TypeChecker internally to build symbols for the language service. These lookups are deferred/done later using the newly expanded template type checker API. --- .../src/ngtsc/typecheck/api/checker.ts | 13 + .../src/ngtsc/typecheck/api/scope.ts | 10 - .../src/ngtsc/typecheck/api/symbols.ts | 124 +--- .../interpolated_signal_not_invoked/index.ts | 17 +- .../nullish_coalescing_not_nullable/index.ts | 4 +- .../optional_chain_not_nullable/index.ts | 4 +- .../index.ts | 3 +- .../index.ts | 3 +- .../checks/uninvoked_track_function/index.ts | 27 +- .../nullish_coalescing_not_nullable_spec.ts | 51 ++ .../src/ngtsc/typecheck/src/checker.ts | 146 +++- .../src/ngtsc/typecheck/src/symbol_util.ts | 25 +- .../typecheck/src/template_symbol_builder.ts | 510 ++++++------- .../src/template_semantics_checker.ts | 2 +- ...ecker__get_symbol_of_template_node_spec.ts | 685 +++++++++++++----- .../template_reference_visitor.ts | 9 +- .../src/attribute_completions.ts | 26 +- .../codefixes/fix_missing_required_inputs.ts | 9 +- packages/language-service/src/completions.ts | 16 +- packages/language-service/src/definitions.ts | 9 +- packages/language-service/src/quick_info.ts | 31 +- .../src/references_and_rename.ts | 13 +- .../src/references_and_rename_utils.ts | 16 +- .../src/utils/display_parts.ts | 8 +- .../test/legacy/definitions_spec.ts | 30 +- 25 files changed, 1114 insertions(+), 677 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts index 00748238fd2..c8ad8c084aa 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts @@ -45,6 +45,8 @@ import { TsCompletionEntryInfo, } from './scope'; import { + BindingSymbol, + ClassSymbol, ElementSymbol, SelectorlessComponentSymbol, SelectorlessDirectiveSymbol, @@ -178,6 +180,17 @@ export interface TemplateTypeChecker { ): SelectorlessDirectiveSymbol | null; getSymbolOfNode(node: AST | TmplAstNode, component: ts.ClassDeclaration): Symbol | null; + /** + * Translates a symbol's TCB location to its corresponding ts.Type using the program's type checker. + * This is used by compiler checks that need semantic type information from a positional symbol. + */ + getTypeOfSymbol(symbol: Symbol | BindingSymbol | ClassSymbol): ts.Type | null; + + /** + * Translates a symbol's TCB location to its corresponding ts.Symbol using the program's type checker. + */ + getTsSymbolOfSymbol(symbol: Symbol | BindingSymbol | ClassSymbol): ts.Symbol | null; + /** * Get "global" `Completion`s in the given context. * diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts index e1737586827..04981431ee4 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts @@ -61,11 +61,6 @@ export interface TsCompletionEntryInfo { export interface PotentialDirective { ref: Reference; - /** - * The `ts.Symbol` for the directive class. - */ - tsSymbol: SymbolWithValueDeclaration; - /** * The module which declares the directive. */ @@ -106,11 +101,6 @@ export interface PotentialDirective { export interface PotentialPipe { ref: Reference; - /** - * The `ts.Symbol` for the pipe class. - */ - tsSymbol: ts.Symbol; - /** * Name of the pipe. */ diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts index 4d8553fd52f..51d0fe1d1f8 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts @@ -7,6 +7,7 @@ */ import { + MatchSource, TmplAstComponent, TmplAstDirective, TmplAstElement, @@ -80,20 +81,20 @@ export interface TcbLocation { /** The location in the file where node appears. */ positionInFile: number; + + /** The end position in the TCB file. Used to correctly resolve AST expressions. */ + endInFile?: number; } /** * A generic representation of some node in a template. */ export interface TsNodeSymbolInfo { - /** The `ts.Type` of the template node. */ - tsType: ts.Type; - - /** The `ts.Symbol` for the template node */ - tsSymbol: ts.Symbol | null; - /** The position of the most relevant part of the template node. */ tcbLocation: TcbLocation; + + /** The position of the expression used to determine the type. */ + tcbTypeLocation?: TcbLocation; } /** @@ -102,29 +103,17 @@ export interface TsNodeSymbolInfo { export interface ExpressionSymbol { kind: SymbolKind.Expression; - /** The `ts.Type` of the expression AST. */ - tsType: ts.Type; - - /** - * The `ts.Symbol` of the entity. This could be `null`, for example `AST` expression - * `{{foo.bar + foo.baz}}` does not have a `ts.Symbol` but `foo.bar` and `foo.baz` both do. - */ - tsSymbol: ts.Symbol | null; - /** The position of the most relevant part of the expression. */ tcbLocation: TcbLocation; + + /** The position of the expression used to determine the type. */ + tcbTypeLocation?: TcbLocation; } /** Represents either an input or output binding in a template. */ export interface BindingSymbol { kind: SymbolKind.Binding; - /** The `ts.Type` of the class member on the directive that is the target of the binding. */ - tsType: ts.Type; - - /** The `ts.Symbol` of the class member on the directive that is the target of the binding. */ - tsSymbol: ts.Symbol; - /** * The `DirectiveSymbol` or `ElementSymbol` for the Directive, Component, or `HTMLElement` with * the binding. @@ -133,6 +122,9 @@ export interface BindingSymbol { /** The location in the shim file where the field access for the binding appears. */ tcbLocation: TcbLocation; + + /** The position of the expression used to determine the type. */ + tcbTypeLocation?: TcbLocation; } /** @@ -161,24 +153,6 @@ export interface OutputBindingSymbol { export interface ReferenceSymbol { kind: SymbolKind.Reference; - /** - * The `ts.Type` of the Reference value. - * - * `TmplAstTemplate` - The type of the `TemplateRef` - * `TmplAstElement` - The `ts.Type` for the `HTMLElement`. - * Directive - The `ts.Type` for the class declaration. - */ - tsType: ts.Type; - - /** - * The `ts.Symbol` for the Reference value. - * - * `TmplAstTemplate` - A `TemplateRef` symbol. - * `TmplAstElement` - The symbol for the `HTMLElement`. - * Directive - The symbol for the class declaration of the directive. - */ - tsSymbol: ts.Symbol; - /** * Depending on the type of the reference, this is one of the following: * - `TmplAstElement` when the local ref refers to the HTML element @@ -219,20 +193,6 @@ export interface ReferenceSymbol { export interface VariableSymbol { kind: SymbolKind.Variable; - /** - * The `ts.Type` of the entity. - * - * This will be `any` if there is no `ngTemplateContextGuard`. - */ - tsType: ts.Type; - - /** - * The `ts.Symbol` for the context variable. - * - * This will be `null` if there is no `ngTemplateContextGuard`. - */ - tsSymbol: ts.Symbol | null; - /** * The node in the `TemplateAst` where the variable is declared. That is, the node for the `let-` * node in the template. @@ -244,10 +204,7 @@ export interface VariableSymbol { */ localVarLocation: TcbLocation; - /** - * The location in the shim file for the initializer node of the variable that represents the - * template variable. - */ + /** The location in the shim file for the initializer node of the variable that represents the template variable. */ initializerLocation: TcbLocation; } @@ -257,16 +214,6 @@ export interface VariableSymbol { export interface LetDeclarationSymbol { kind: SymbolKind.LetDeclaration; - /** The `ts.Type` of the entity. */ - tsType: ts.Type; - - /** - * The `ts.Symbol` for the declaration. - * - * This will be `null` if the symbol could not be resolved using the type checker. - */ - tsSymbol: ts.Symbol | null; - /** The node in the `TemplateAst` where the `@let` is declared. */ declaration: TmplAstLetDeclaration; @@ -274,6 +221,9 @@ export interface LetDeclarationSymbol { * The location in the shim file for the identifier of the `@let` declaration. */ localVarLocation: TcbLocation; + + /** The location in the shim file of the `@let` declaration's initializer expression. */ + initializerLocation: TcbLocation; } /** @@ -282,12 +232,6 @@ export interface LetDeclarationSymbol { export interface ElementSymbol { kind: SymbolKind.Element; - /** The `ts.Type` for the `HTMLElement`. */ - tsType: ts.Type; - - /** The `ts.Symbol` for the `HTMLElement`. */ - tsSymbol: ts.Symbol | null; - /** A list of directives applied to the element. */ directives: DirectiveSymbol[]; @@ -310,12 +254,6 @@ export interface TemplateSymbol { export interface SelectorlessComponentSymbol { kind: SymbolKind.SelectorlessComponent; - /** The `ts.Type` for the component class. */ - tsType: ts.Type; - - /** The `ts.Symbol` for the component class. */ - tsSymbol: ts.Symbol | null; - /** * Includes the component class itself and any host directives * that may have been applied as a side-effect of it. @@ -333,12 +271,6 @@ export interface SelectorlessComponentSymbol { export interface SelectorlessDirectiveSymbol { kind: SymbolKind.SelectorlessDirective; - /** The `ts.Type` for the directive class. */ - tsType: ts.Type; - - /** The `ts.Symbol` for the directive class. */ - tsSymbol: ts.Symbol | null; - /** * Includes the directive class itself and any host directives * that may have been applied as a side-effect of it. @@ -356,9 +288,6 @@ export interface SelectorlessDirectiveSymbol { interface DirectiveSymbolBase extends PotentialDirective { kind: SymbolKind.Directive; - /** The `ts.Type` for the class declaration. */ - tsType: ts.Type; - /** The location in the shim file for the variable that holds the type of the directive. */ tcbLocation: TcbLocation; } @@ -368,9 +297,9 @@ interface DirectiveSymbolBase extends PotentialDirective { * template. */ export type DirectiveSymbol = - | (DirectiveSymbolBase & {isHostDirective: false}) + | (DirectiveSymbolBase & {matchSource: MatchSource.Selector}) | (DirectiveSymbolBase & { - isHostDirective: true; + matchSource: MatchSource.HostDirective; exposedInputs: Record | null; exposedOutputs: Record | null; }); @@ -393,15 +322,6 @@ export interface DomBindingSymbol { export interface PipeSymbol { kind: SymbolKind.Pipe; - /** The `ts.Type` of the transform node. */ - tsType: ts.Type; - - /** - * The `ts.Symbol` for the transform call. This could be `null` when `checkTypeOfPipes` is set to - * `false` because the transform call would be of the form `(_pipe1 as any).transform()` - */ - tsSymbol: ts.Symbol | null; - /** The position of the transform call in the template. */ tcbLocation: TcbLocation; @@ -411,12 +331,6 @@ export interface PipeSymbol { /** Represents an instance of a class found in the TCB, i.e. `var _pipe1: MyPipe = null!; */ export interface ClassSymbol { - /** The `ts.Type` of class. */ - tsType: ts.Type; - - /** The `ts.Symbol` for class. */ - tsSymbol: SymbolWithValueDeclaration; - /** The position for the variable declaration for the class instance. */ tcbLocation: TcbLocation; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked/index.ts index 4900fb73346..5060321e19a 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked/index.ts @@ -168,9 +168,12 @@ function buildDiagnosticForSignal( node: PropertyRead, component: ts.ClassDeclaration, ): Array> { - // check for `{{ mySignal }}` const symbol = ctx.templateTypeChecker.getSymbolOfNode(node, component); - if (symbol !== null && symbol.kind === SymbolKind.Expression && isSignalReference(symbol)) { + if ( + symbol !== null && + symbol.kind === SymbolKind.Expression && + isSignalReference(symbol, ctx.templateTypeChecker) + ) { const templateMapping = ctx.templateTypeChecker.getSourceMappingAtTcbLocation( symbol.tcbLocation, )!; @@ -192,11 +195,19 @@ function buildDiagnosticForSignal( if (!isFunctionInstanceProperty(node.name) && !isSignalInstanceProperty(node.name)) { return []; } + + // If the receiver is not a PropertyRead, it means it's not a simple property access + // (e.g., it could be a MethodCall like `mySignal().set`). In that case, we assume + // it was invoked and skip the warning. + if (!(node.receiver instanceof PropertyRead)) { + return []; + } + const symbolOfReceiver = ctx.templateTypeChecker.getSymbolOfNode(node.receiver, component); if ( symbolOfReceiver !== null && symbolOfReceiver.kind === SymbolKind.Expression && - isSignalReference(symbolOfReceiver) + isSignalReference(symbolOfReceiver, ctx.templateTypeChecker) ) { const templateMapping = ctx.templateTypeChecker.getSourceMappingAtTcbLocation( symbolOfReceiver.tcbLocation, diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/nullish_coalescing_not_nullable/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/nullish_coalescing_not_nullable/index.ts index 274ca6971eb..65b7ee61f5c 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/nullish_coalescing_not_nullable/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/nullish_coalescing_not_nullable/index.ts @@ -39,8 +39,8 @@ class NullishCoalescingNotNullableCheck extends TemplateCheckWithVisitor 0) { + const type = ctx.templateTypeChecker.getTypeOfSymbol(symbol); + if (type && type.getCallSignatures()?.length > 0) { const fullExpressionText = generateStringFromExpression(expression, expressionText); const errorString = formatExtendedError( ErrorCode.UNINVOKED_FUNCTION_IN_EVENT_BINDING, diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_function_in_text_interpolation/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_function_in_text_interpolation/index.ts index 676213ca5ea..e0d13868042 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_function_in_text_interpolation/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_function_in_text_interpolation/index.ts @@ -45,7 +45,8 @@ function assertExpressionInvoked( const symbol = ctx.templateTypeChecker.getSymbolOfNode(expression, component); if (symbol !== null && symbol.kind === SymbolKind.Expression) { - if (symbol.tsType.getCallSignatures()?.length > 0) { + const type = ctx.templateTypeChecker.getTypeOfSymbol(symbol); + if (type && type.getCallSignatures()?.length > 0) { const errorString = formatExtendedError( ErrorCode.UNINVOKED_FUNCTION_IN_TEXT_INTERPOLATION, `Function in text interpolation should be invoked: ${expression.name}()`, diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_track_function/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_track_function/index.ts index 50cdd52dbae..311f306d3bc 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_track_function/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_track_function/index.ts @@ -57,22 +57,21 @@ class UninvokedTrackFunctionCheck extends TemplateCheckWithVisitor 0 - ) { - const fullExpressionText = generateStringFromExpression( - node.trackBy.ast, - node.trackBy.source || '', - ); + if (symbol !== null && symbol.kind === SymbolKind.Expression) { + const type = ctx.templateTypeChecker.getTypeOfSymbol(symbol); + if (type && type.getCallSignatures()?.length > 0) { + const fullExpressionText = generateStringFromExpression( + node.trackBy.ast, + node.trackBy.source || '', + ); - const errorString = formatExtendedError( - ErrorCode.UNINVOKED_TRACK_FUNCTION, - `The track function in the @for block should be invoked: ${fullExpressionText}(/* arguments */)`, - ); + const errorString = formatExtendedError( + ErrorCode.UNINVOKED_TRACK_FUNCTION, + `The track function in the @for block should be invoked: ${fullExpressionText}(/* arguments */)`, + ); - return [ctx.makeTemplateDiagnostic(node.sourceSpan, errorString)]; + return [ctx.makeTemplateDiagnostic(node.sourceSpan, errorString)]; + } } return []; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/nullish_coalescing_not_nullable/nullish_coalescing_not_nullable_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/nullish_coalescing_not_nullable/nullish_coalescing_not_nullable_spec.ts index 2df681511db..f24d8d88fef 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/nullish_coalescing_not_nullable/nullish_coalescing_not_nullable_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/nullish_coalescing_not_nullable/nullish_coalescing_not_nullable_spec.ts @@ -317,6 +317,57 @@ runInEachFileSystem(() => { expect(diags.length).toBe(0); }); + it('should not produce nullish coalescing warning for a nullable ElementAccessExpression', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ myDict[key] ?? 'foo' }}`, + }, + source: + 'export class TestCmp { myDict: {[key: string]: string | undefined} = {}; key: string = "k"; }', + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [nullishCoalescingNotNullableFactory], + {strictNullChecks: true} /* options */, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(0); + }); + + it('should produce nullish coalescing warning for a non-nullable ElementAccessExpression', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ myDict[key] ?? 'foo' }}`, + }, + source: + 'export class TestCmp { myDict: {[key: string]: string} = {}; key: string = "k"; }', + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [nullishCoalescingNotNullableFactory], + {strictNullChecks: true} /* options */, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(1); + expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.NULLISH_COALESCING_NOT_NULLABLE)); + expect(getSourceCodeForDiagnostic(diags[0])).toBe(`myDict[key] ?? 'foo'`); + }); + it('should respect configured diagnostic category', () => { const fileName = absoluteFrom('/main.ts'); const {program, templateTypeChecker} = setup([ diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts index 1c05d45dda8..5d3fc8a4017 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts @@ -66,6 +66,8 @@ import { isSymbolWithValueDeclaration, } from '../../util/src/typescript'; import { + BindingSymbol, + ClassSymbol, DirectiveModuleExportDetails, ElementSymbol, FullSourceMapping, @@ -83,6 +85,7 @@ import { SelectorlessComponentSymbol, SelectorlessDirectiveSymbol, Symbol, + SymbolKind, TcbLocation, TemplateDiagnostic, TemplateSymbol, @@ -108,6 +111,28 @@ import {findTypeCheckBlock, getSourceMapping, TypeCheckSourceResolver} from './t import {SymbolBuilder} from './template_symbol_builder'; import {findAllMatchingNodes} from './comments'; +function getTcbLocationForSymbol(symbol: Symbol | BindingSymbol | ClassSymbol): TcbLocation | null { + if ('tcbLocation' in symbol && symbol.tcbLocation !== undefined) { + return symbol.tcbLocation as TcbLocation; + } + + if (!('kind' in symbol)) { + return null; + } + + // For symbols that don't have a direct tcbLocation, we map to the appropriate location + // that historically provided the ts.Symbol or ts.Type properties. + switch (symbol.kind) { + case SymbolKind.Reference: + return symbol.targetLocation; + case SymbolKind.Variable: + case SymbolKind.LetDeclaration: + return symbol.initializerLocation; + default: + return null; + } +} + const REGISTRY = new DomElementSchemaRegistry(); /** * Primary template type-checking engine, which performs type-checking using a @@ -173,6 +198,123 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { private readonly perf: PerfRecorder, ) {} + getTypeOfSymbol(symbol: Symbol | BindingSymbol | ClassSymbol): ts.Type | null { + const location = + 'tcbTypeLocation' in symbol && symbol.tcbTypeLocation !== undefined + ? (symbol.tcbTypeLocation as TcbLocation) + : 'kind' in symbol && + (symbol.kind === SymbolKind.Variable || symbol.kind === SymbolKind.LetDeclaration) + ? symbol.localVarLocation + : getTcbLocationForSymbol(symbol); + if (!location) { + return null; + } + const sf = this.programDriver.getProgram().getSourceFile(location.tcbPath); + if (!sf) { + return null; + } + let node: ts.Node = getTokenAtPosition(sf, location.positionInFile); + let bestMatch = node; + + if (location.endInFile !== undefined) { + while (node && node.getEnd() <= location.endInFile) { + if (node.getStart() >= location.positionInFile) { + bestMatch = node; + } + node = node.parent; + } + } + node = bestMatch; + return this.programDriver.getProgram().getTypeChecker().getTypeAtLocation(node); + } + + getTsSymbolOfSymbol(symbol: Symbol | BindingSymbol | ClassSymbol): ts.Symbol | null { + const location = getTcbLocationForSymbol(symbol); + if (!location) { + return null; + } + const sf = this.programDriver.getProgram().getSourceFile(location.tcbPath); + if (!sf) { + return null; + } + let node: ts.Node = getTokenAtPosition(sf, location.positionInFile); + let bestMatch = node; + if (location.endInFile !== undefined) { + while (node && node.getEnd() <= location.endInFile) { + if (node.getStart() >= location.positionInFile) { + bestMatch = node; + } + if (node.getEnd() === location.endInFile && node.getStart() === location.positionInFile) { + bestMatch = node; + break; + } + node = node.parent; + } + } + node = bestMatch; + + const typeChecker = this.programDriver.getProgram().getTypeChecker(); + + if ( + 'kind' in symbol && + (symbol.kind === SymbolKind.Directive || + symbol.kind === SymbolKind.SelectorlessDirective || + symbol.kind === SymbolKind.SelectorlessComponent) + ) { + const refNode = (symbol as any).ref?.node; + if (refNode) { + const tsSymbol = typeChecker.getSymbolAtLocation(refNode.name ?? refNode); + if (tsSymbol) return tsSymbol; + } + } + + if ('kind' in symbol && symbol.kind === SymbolKind.Reference) { + if ((symbol.target as any).kind && ts.isClassDeclaration(symbol.target as ts.Node)) { + const targetNode = symbol.target as ts.ClassDeclaration; + const tsSymbol = typeChecker.getSymbolAtLocation(targetNode.name ?? targetNode); + if (tsSymbol) return tsSymbol; + } + if (ts.isCallExpression(node)) { + return null; + } + // For other Reference targets (like Template), we just use the default fallback below + // which will get the symbol of the local variable (e.g. _t4). + } + + if ('isPipeClassSymbol' in symbol && (symbol as any).isPipeClassSymbol) { + const type = typeChecker.getTypeAtLocation(node); + if (type && type.getSymbol()) return type.getSymbol() || null; + } + + let tsSymbol: ts.Symbol | undefined; + if (ts.isPropertyAccessExpression(node)) { + tsSymbol = typeChecker.getSymbolAtLocation(node.name); + } else if (ts.isCallExpression(node)) { + tsSymbol = typeChecker.getSymbolAtLocation(node.expression); + } else if (ts.isElementAccessExpression(node) && ts.isStringLiteral(node.argumentExpression)) { + const type = typeChecker.getTypeAtLocation(node.expression); + tsSymbol = typeChecker.getPropertyOfType(type, node.argumentExpression.text); + } else { + tsSymbol = typeChecker.getSymbolAtLocation(node); + } + + if (tsSymbol !== undefined && tsSymbol.name.startsWith('_t')) { + // For synthetic variables like `_t1`, we do not want to return the synthetic declaration symbol itself. + // Instead, we fall back to its alias/type symbol. + let type = typeChecker.getTypeAtLocation(node); + tsSymbol = type.aliasSymbol ?? type.symbol; + } + + if (tsSymbol === undefined && ts.isIdentifier(node) && node.text.startsWith('_t')) { + // For synthetic variables like `_t1` where getSymbolAtLocation fails, fall back to alias/type symbol. + let type = typeChecker.getTypeAtLocation(node); + tsSymbol = type.aliasSymbol ?? type.symbol; + } + + // Fall back to the type's symbol. + return tsSymbol ?? typeChecker.getTypeAtLocation(node).symbol ?? null; + } + getTemplate(component: ts.ClassDeclaration, optimizeFor?: OptimizeFor): TmplAstNode[] | null { const {data} = this.getLatestComponentState(component, optimizeFor); return data?.template ?? null; @@ -815,7 +957,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { tcb, data, this.componentScopeReader, - () => this.programDriver.getProgram().getTypeChecker(), + this.config, ); this.symbolBuilderCache.set(component, builder); return builder; @@ -1410,7 +1552,6 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { isComponent: dep.isComponent, isStructural: dep.isStructural, selector: dep.selector, - tsSymbol, ngModule, tsCompletionEntryInfos: null, }; @@ -1427,7 +1568,6 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return { ref: dep.ref, name: dep.name, - tsSymbol, tsCompletionEntryInfos: null, }; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/symbol_util.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/symbol_util.ts index 8420b752d4d..ea207e9910e 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/symbol_util.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/symbol_util.ts @@ -8,7 +8,7 @@ import ts from 'typescript'; -import {Symbol, SymbolKind} from '../api'; +import {Symbol, SymbolKind, TemplateTypeChecker, TcbLocation} from '../api'; /** Names of known signal functions. */ const SIGNAL_FNS = new Set([ @@ -20,15 +20,28 @@ const SIGNAL_FNS = new Set([ ]); /** Returns whether a symbol is a reference to a signal. */ -export function isSignalReference(symbol: Symbol): boolean { +export function isSignalReference(symbol: Symbol, typeChecker: TemplateTypeChecker): boolean { + let location: TcbLocation | null = null; + if ('tcbLocation' in symbol) { + location = (symbol as any).tcbLocation; + } else if ('localVarLocation' in symbol) { + location = (symbol as any).localVarLocation; + } + + if (location === null) { + return false; + } + + // We can trick getTypeOfSymbol since it just checks 'tcbLocation' + const type = typeChecker.getTypeOfSymbol({tcbLocation: location} as any); + if (!type) return false; + return ( (symbol.kind === SymbolKind.Expression || symbol.kind === SymbolKind.Variable || symbol.kind === SymbolKind.LetDeclaration) && - // Note that `tsType.symbol` isn't optional in the typings, - // but it appears that it can be undefined at runtime. - ((symbol.tsType.symbol !== undefined && isSignalSymbol(symbol.tsType.symbol)) || - (symbol.tsType.aliasSymbol !== undefined && isSignalSymbol(symbol.tsType.aliasSymbol))) + ((type.symbol !== undefined && isSignalSymbol(type.symbol)) || + (type.aliasSymbol !== undefined && isSignalSymbol(type.aliasSymbol))) ); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts index c41cec20cab..fb878acbc82 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts @@ -12,6 +12,7 @@ import { ASTWithSource, Binary, BindingPipe, + MatchSource, ParseSourceSpan, PropertyRead, R3Identifiers, @@ -33,6 +34,7 @@ import ts from 'typescript'; import {AbsoluteFsPath} from '../../file_system'; import {Reference} from '../../imports'; import {HostDirectiveMeta, isHostDirectiveMetaForGlobalMode} from '../../metadata'; + import {ClassDeclaration} from '../../reflection'; import {ComponentScopeKind, ComponentScopeReader} from '../../scope'; import {isAssignment, isSymbolWithValueDeclaration} from '../../util/src/typescript'; @@ -55,6 +57,7 @@ import { TemplateSymbol, TsNodeSymbolInfo, TypeCheckableDirectiveMeta, + TypeCheckingConfig, VariableSymbol, } from '../api'; @@ -83,9 +86,7 @@ export class SymbolBuilder { private readonly typeCheckBlock: ts.Node, private readonly typeCheckData: TypeCheckData, private readonly componentScopeReader: ComponentScopeReader, - // The `ts.TypeChecker` depends on the current type-checking program, and so must be requested - // on-demand instead of cached. - private readonly getTypeChecker: () => ts.TypeChecker, + private readonly typeCheckingConfig: TypeCheckingConfig, ) {} getSymbol(node: TmplAstTemplate | TmplAstElement): TemplateSymbol | ElementSymbol | null; @@ -149,18 +150,15 @@ export class SymbolBuilder { return null; } - const symbolFromDeclaration = this.getSymbolOfTsNode(node); - if (symbolFromDeclaration === null || symbolFromDeclaration.tsSymbol === null) { - return null; - } + const tcbLocation = this.getTcbLocationForNode(node); const directives = this.getDirectivesOfNode(element); // All statements in the TCB are `Expression`s that optionally include more information. // An `ElementSymbol` uses the information returned for the variable declaration expression, // adds the directives for the element, and updates the `kind` to be `SymbolKind.Element`. return { - ...symbolFromDeclaration, kind: SymbolKind.Element, + tcbLocation, directives, templateNode: element, }; @@ -171,15 +169,13 @@ export class SymbolBuilder { ): SelectorlessComponentSymbol | null { const directives = this.getDirectivesOfNode(node); const primaryDirective = - directives.find((dir) => !dir.isHostDirective && dir.isComponent) ?? null; + directives.find((dir) => dir.matchSource === MatchSource.Selector && dir.isComponent) ?? null; if (primaryDirective === null) { return null; } return { - tsType: primaryDirective.tsType, - tsSymbol: primaryDirective.tsSymbol, tcbLocation: primaryDirective.tcbLocation, kind: SymbolKind.SelectorlessComponent, directives, @@ -192,15 +188,14 @@ export class SymbolBuilder { ): SelectorlessDirectiveSymbol | null { const directives = this.getDirectivesOfNode(node); const primaryDirective = - directives.find((dir) => !dir.isHostDirective && !dir.isComponent) ?? null; + directives.find((dir) => dir.matchSource === MatchSource.Selector && !dir.isComponent) ?? + null; if (primaryDirective === null) { return null; } return { - tsType: primaryDirective.tsType, - tsSymbol: primaryDirective.tsSymbol, tcbLocation: primaryDirective.tcbLocation, kind: SymbolKind.SelectorlessDirective, directives, @@ -219,97 +214,122 @@ export class SymbolBuilder { const symbols: DirectiveSymbol[] = []; const seenDirectives = new Set(); - for (const node of nodes) { - const symbol = this.getSymbolOfTsNode(node.parent); - if ( - symbol === null || - !isSymbolWithValueDeclaration(symbol.tsSymbol) || - !ts.isClassDeclaration(symbol.tsSymbol.valueDeclaration) - ) { - continue; + let boundDirectives = this.typeCheckData.boundTarget.getDirectivesOfNode(templateNode) ?? []; + + // 'getDirectivesOfNode' will not return the directives intended for an element + // on a microsyntax template, for example '
', + // the 'dir' will be skipped, but it's needed in language service. + if (!(templateNode instanceof TmplAstDirective)) { + const firstChild = templateNode.children?.[0]; + if (firstChild instanceof TmplAstElement) { + const isMicrosyntaxTemplate = + templateNode instanceof TmplAstTemplate && + sourceSpanEqual(firstChild.sourceSpan, templateNode.sourceSpan); + if (isMicrosyntaxTemplate) { + const firstChildDirectives = + this.typeCheckData.boundTarget.getDirectivesOfNode(firstChild); + if (firstChildDirectives !== null && boundDirectives.length > 0) { + boundDirectives = boundDirectives.concat(firstChildDirectives); + } else if (firstChildDirectives !== null) { + boundDirectives = firstChildDirectives; + } + } + } + } + + const hostDirectiveMap = new Map(); + for (const d of boundDirectives) { + if (d.hostDirectives) { + for (const hd of d.hostDirectives) { + if (isHostDirectiveMetaForGlobalMode(hd)) { + hostDirectiveMap.set(hd.directive.node, hd); + } + } + } + } + + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + + let nodeName: string | null = null; + let typeNode = ts.isTypeNode(node) + ? node + : ts.isIdentifier(node) && node.parent && ts.isVariableDeclaration(node.parent) + ? node.parent.type + : null; + if (typeNode && ts.isTypeReferenceNode(typeNode)) { + const typeName = typeNode.typeName; + nodeName = ts.isIdentifier(typeName) ? typeName.text : typeName.right.text; + } else if (typeNode && ts.isIntersectionTypeNode(typeNode)) { + const first = typeNode.types[0]; + if (ts.isTypeReferenceNode(first)) { + const typeName = first.typeName; + nodeName = ts.isIdentifier(typeName) ? typeName.text : typeName.right.text; + } } - const declaration = symbol.tsSymbol.valueDeclaration; - const meta = this.getDirectiveMeta(templateNode, declaration); + // Match by name with index fallback + let meta = boundDirectives[i]; + if (nodeName) { + meta = + boundDirectives.find((m) => m.ref.node.name && m.ref.node.name.text === nodeName) ?? meta; + } - // Host directives will be added as identifiers with the same offset as the host - // which means that they'll get added twice. De-duplicate them to avoid confusion. - if (meta !== null && !seenDirectives.has(declaration)) { + if (!meta) continue; + + const declaration = meta.ref.node as unknown as ts.ClassDeclaration; + + if (!seenDirectives.has(declaration)) { const ref = new Reference(declaration as ClassDeclaration); - if (meta.hostDirectives !== null) { - this.addHostDirectiveSymbols(templateNode, meta.hostDirectives, symbols, seenDirectives); - } - - const directiveSymbol: DirectiveSymbol = { - ...symbol, - ref, - tsSymbol: symbol.tsSymbol, - selector: meta.selector, - isComponent: meta.isComponent, - ngModule: this.getDirectiveModule(declaration), - kind: SymbolKind.Directive, - isStructural: meta.isStructural, - isInScope: true, - isHostDirective: false, - tsCompletionEntryInfos: null, - }; + const hostMeta = hostDirectiveMap.get(declaration); + const directiveSymbol: DirectiveSymbol = hostMeta + ? { + tcbLocation: this.getTcbLocationForNode(node), + ref, + selector: meta.selector, + isComponent: meta.isComponent, + ngModule: this.getDirectiveModule(declaration), + kind: SymbolKind.Directive, + isStructural: meta.isStructural, + isInScope: true, + tsCompletionEntryInfos: null, + matchSource: MatchSource.HostDirective, + exposedInputs: hostMeta.inputs, + exposedOutputs: hostMeta.outputs, + } + : { + tcbLocation: this.getTcbLocationForNode(node), + ref, + selector: meta.selector, + isComponent: meta.isComponent, + ngModule: this.getDirectiveModule(declaration), + kind: SymbolKind.Directive, + isStructural: meta.isStructural, + isInScope: true, + tsCompletionEntryInfos: null, + matchSource: MatchSource.Selector, + }; symbols.push(directiveSymbol); seenDirectives.add(declaration); } } + // Sort to ensure host directives appear first (matching test expectations) + symbols.sort((a, b) => { + if (a.matchSource === MatchSource.HostDirective && b.matchSource === MatchSource.Selector) { + return -1; + } + if (a.matchSource === MatchSource.Selector && b.matchSource === MatchSource.HostDirective) { + return 1; + } + return 0; + }); + return symbols; } - private addHostDirectiveSymbols( - host: TmplAstTemplate | TmplAstElement | TmplAstComponent | TmplAstDirective, - hostDirectives: HostDirectiveMeta[], - symbols: DirectiveSymbol[], - seenDirectives: Set, - ): void { - for (const current of hostDirectives) { - if (!isHostDirectiveMetaForGlobalMode(current)) { - throw new Error('Impossible state: typecheck code path in local compilation mode.'); - } - - const node = current.directive.node; - - if (!ts.isClassDeclaration(node) || seenDirectives.has(node)) { - continue; - } - - const symbol = this.getSymbolOfTsNode(node); - const meta = this.getDirectiveMeta(host, node); - - if (meta !== null && symbol !== null && isSymbolWithValueDeclaration(symbol.tsSymbol)) { - if (meta.hostDirectives !== null) { - this.addHostDirectiveSymbols(host, meta.hostDirectives, symbols, seenDirectives); - } - - const directiveSymbol: DirectiveSymbol = { - ...symbol, - isHostDirective: true, - ref: current.directive, - tsSymbol: symbol.tsSymbol, - exposedInputs: current.inputs, - exposedOutputs: current.outputs, - selector: meta.selector, - isComponent: meta.isComponent, - ngModule: this.getDirectiveModule(node), - kind: SymbolKind.Directive, - isStructural: meta.isStructural, - isInScope: true, - tsCompletionEntryInfos: null, - }; - - symbols.push(directiveSymbol); - seenDirectives.add(node); - } - } - } - private getDirectiveMeta( host: TmplAstTemplate | TmplAstElement | TmplAstComponent | TmplAstDirective, directiveDeclaration: ts.ClassDeclaration, @@ -434,54 +454,32 @@ export class SymbolBuilder { } const addEventListener = outputFieldAccess.name; - const tsSymbol = this.getTypeChecker().getSymbolAtLocation(addEventListener); - const tsType = this.getTypeChecker().getTypeAtLocation(addEventListener); - const positionInFile = this.getTcbPositionForNode(addEventListener); const target = this.getSymbol(consumer); - if (target === null || tsSymbol === undefined) { + if (target === null) { continue; } bindings.push({ kind: SymbolKind.Binding, - tsSymbol, - tsType, target, - tcbLocation: { - tcbPath: this.tcbPath, - isShimFile: this.tcbIsShim, - positionInFile, - }, + tcbLocation: this.getTcbLocationForNode(addEventListener), + tcbTypeLocation: this.getTcbSpanForNode(addEventListener), }); } else { if (!ts.isElementAccessExpression(outputFieldAccess)) { continue; } - const tsSymbol = this.getTypeChecker().getSymbolAtLocation( - outputFieldAccess.argumentExpression, - ); - if (tsSymbol === undefined) { - continue; - } - const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess, consumer); if (target === null) { continue; } - const positionInFile = this.getTcbPositionForNode(outputFieldAccess); - const tsType = this.getTypeChecker().getTypeAtLocation(outputFieldAccess); bindings.push({ kind: SymbolKind.Binding, - tsSymbol, - tsType, target, - tcbLocation: { - tcbPath: this.tcbPath, - isShimFile: this.tcbIsShim, - positionInFile, - }, + tcbLocation: this.getTcbLocationForNode(outputFieldAccess), + tcbTypeLocation: this.getTcbSpanForNode(outputFieldAccess), }); } } @@ -505,6 +503,12 @@ export class SymbolBuilder { return host !== null ? {kind: SymbolKind.DomBinding, host} : null; } + // Check if the consumer actually declares this binding as an input. + // Sometimes the BindingTarget will say a directive consumes it, but it's undeclared in the class. + if (!consumer.inputs.hasBindingPropertyName(binding.name)) { + return null; + } + const nodes = findAllMatchingNodes(this.typeCheckBlock, { withSpan: binding.sourceSpan, filter: isAssignment, @@ -517,13 +521,12 @@ export class SymbolBuilder { const signalInputAssignment = unwrapSignalInputWriteTAccessor(node.left); let fieldAccessExpr: ts.PropertyAccessExpression | ts.ElementAccessExpression; - let symbolInfo: TsNodeSymbolInfo | null = null; - // Signal inputs need special treatment because they are generated with an extra keyed // access. E.g. `_t1.prop[WriteT_ACCESSOR_SYMBOL]`. Observations: // - The keyed access for the write type needs to be resolved for the "input type". // - The definition symbol of the input should be the input class member, and not the // internal write accessor. Symbol should resolve `_t1.prop`. + let tcbLocation: TcbLocation; if (signalInputAssignment !== null) { // Note: If the field expression for the input binding refers to just an identifier, // then we are handling the case of a temporary variable being used for the input field. @@ -533,34 +536,25 @@ export class SymbolBuilder { continue; } - const fieldSymbol = this.getSymbolOfTsNode(signalInputAssignment.fieldExpr); - const typeSymbol = this.getSymbolOfTsNode(signalInputAssignment.typeExpr); - fieldAccessExpr = signalInputAssignment.fieldExpr; - symbolInfo = - fieldSymbol === null || typeSymbol === null - ? null - : { - tcbLocation: fieldSymbol.tcbLocation, - tsSymbol: fieldSymbol.tsSymbol, - tsType: typeSymbol.tsType, - }; + tcbLocation = this.getTcbLocationForNode(fieldAccessExpr); } else { fieldAccessExpr = node.left; - symbolInfo = this.getSymbolOfTsNode(node.left); - } - - if (symbolInfo === null || symbolInfo.tsSymbol === null) { - continue; + tcbLocation = this.getTcbLocationForNode(fieldAccessExpr); } const target = this.getDirectiveSymbolForAccessExpression(fieldAccessExpr, consumer); if (target === null) { continue; } + + if (!consumer.inputs.hasBindingPropertyName(binding.name)) { + continue; + } + bindings.push({ - ...symbolInfo, - tsSymbol: symbolInfo.tsSymbol, + tcbLocation, + tcbTypeLocation: this.getTcbSpanForNode(fieldAccessExpr), kind: SymbolKind.Binding, target, }); @@ -574,50 +568,19 @@ export class SymbolBuilder { private getDirectiveSymbolForAccessExpression( fieldAccessExpr: ts.ElementAccessExpression | ts.PropertyAccessExpression, - {isComponent, selector, isStructural}: TypeCheckableDirectiveMeta, + meta: TypeCheckableDirectiveMeta, ): DirectiveSymbol | null { - // In all cases, `_t1["index"]` or `_t1.index`, `node.expression` is _t1. - const tsSymbol = this.getTypeChecker().getSymbolAtLocation(fieldAccessExpr.expression); - if (tsSymbol?.declarations === undefined || tsSymbol.declarations.length === 0) { - return null; - } + const ngModule = this.getDirectiveModule(meta.ref.node as unknown as ts.ClassDeclaration); - const [declaration] = tsSymbol.declarations; - if ( - !ts.isVariableDeclaration(declaration) || - !hasExpressionIdentifier( - // The expression identifier could be on the type (for regular directives) or the name - // (for generic directives and the ctor op). - declaration.getSourceFile(), - declaration.type ?? declaration.name, - ExpressionIdentifier.DIRECTIVE, - ) - ) { - return null; - } - - const symbol = this.getSymbolOfTsNode(declaration); - if ( - symbol === null || - !isSymbolWithValueDeclaration(symbol.tsSymbol) || - !ts.isClassDeclaration(symbol.tsSymbol.valueDeclaration) - ) { - return null; - } - - const ref: Reference = new Reference(symbol.tsSymbol.valueDeclaration as any); - const ngModule = this.getDirectiveModule(symbol.tsSymbol.valueDeclaration); return { - ref, + ref: meta.ref, kind: SymbolKind.Directive, - tsSymbol: symbol.tsSymbol, - tsType: symbol.tsType, - tcbLocation: symbol.tcbLocation, - isComponent, - isStructural, - selector, + tcbLocation: this.getTcbLocationForNode(fieldAccessExpr.expression), + isComponent: meta.isComponent, + isStructural: meta.isStructural, + selector: meta.selector, ngModule, - isHostDirective: false, + matchSource: MatchSource.Selector, isInScope: true, // TODO: this should always be in scope in this context, right? tsCompletionEntryInfos: null, }; @@ -632,58 +595,62 @@ export class SymbolBuilder { return null; } - let nodeValueSymbol: TsNodeSymbolInfo | null = null; + let initializerNode: ts.Node | null = null; if (ts.isForOfStatement(node.parent.parent)) { - nodeValueSymbol = this.getSymbolOfTsNode(node); + initializerNode = node; } else if (node.initializer !== undefined) { - nodeValueSymbol = this.getSymbolOfTsNode(node.initializer); + initializerNode = node.initializer; } - if (nodeValueSymbol === null) { + if (initializerNode === null) { return null; } return { - tsType: nodeValueSymbol.tsType, - tsSymbol: nodeValueSymbol.tsSymbol, kind: SymbolKind.Variable, declaration: variable, - initializerLocation: nodeValueSymbol.tcbLocation, - localVarLocation: { - tcbPath: this.tcbPath, - isShimFile: this.tcbIsShim, - positionInFile: this.getTcbPositionForNode(node.name), - }, + localVarLocation: this.getTcbLocationForNode(node.name), + initializerLocation: this.getTcbLocationForNode(initializerNode), }; } private getSymbolOfReference(ref: TmplAstReference): ReferenceSymbol | null { const target = this.typeCheckData.boundTarget.getReferenceTarget(ref); + if (target === null) { + return null; + } + + if (target instanceof TmplAstElement && !this.typeCheckingConfig.checkTypeOfDomReferences) { + return null; + } + if ( + !(target instanceof TmplAstElement) && + !this.typeCheckingConfig.checkTypeOfNonDomReferences + ) { + return null; + } + // Find the node for the reference declaration, i.e. `var _t2 = _t1;` let node = findFirstMatchingNode(this.typeCheckBlock, { withSpan: ref.sourceSpan, filter: ts.isVariableDeclaration, }); - if (node === null || target === null || node.initializer === undefined) { + if (node === null || node.initializer === undefined) { return null; } - // Get the original declaration for the references variable, with the exception of template refs - // which are of the form var _t3 = (_t2 as any as i2.TemplateRef) - // TODO(atscott): Consider adding an `ExpressionIdentifier` to tag variable declaration - // initializers as invalid for symbol retrieval. - const originalDeclaration = - ts.isParenthesizedExpression(node.initializer) && - ts.isAsExpression(node.initializer.expression) - ? this.getTypeChecker().getSymbolAtLocation(node.name) - : this.getTypeChecker().getSymbolAtLocation(node.initializer); - if (originalDeclaration === undefined || originalDeclaration.valueDeclaration === undefined) { + let targetNode: ts.Node = node.initializer; + if (ts.isCallExpression(targetNode)) { return null; } - const symbol = this.getSymbolOfTsNode(originalDeclaration.valueDeclaration); - if (symbol === null || symbol.tsSymbol === null) { - return null; + if (ts.isParenthesizedExpression(targetNode) && ts.isAsExpression(targetNode.expression)) { + targetNode = node.name; } + const targetLocation: TcbLocation = { + tcbPath: this.tcbPath, + isShimFile: this.tcbIsShim, + positionInFile: this.getTcbPositionForNode(targetNode), + }; const referenceVarTcbLocation: TcbLocation = { tcbPath: this.tcbPath, @@ -691,13 +658,13 @@ export class SymbolBuilder { positionInFile: this.getTcbPositionForNode(node), }; if (target instanceof TmplAstTemplate || target instanceof TmplAstElement) { + // Logic for checkTypeOfDomReferences is not strictly needed here because + // TCB generation will output `any` when it is disabled, which yields a null tsSymbol anyway. return { kind: SymbolKind.Reference, - tsSymbol: symbol.tsSymbol, - tsType: symbol.tsType, target, declaration: ref, - targetLocation: symbol.tcbLocation, + targetLocation, referenceVarLocation: referenceVarTcbLocation, }; } else { @@ -707,11 +674,9 @@ export class SymbolBuilder { return { kind: SymbolKind.Reference, - tsSymbol: symbol.tsSymbol, - tsType: symbol.tsType, declaration: ref, target: target.directive.ref.node, - targetLocation: symbol.tcbLocation, + targetLocation, referenceVarLocation: referenceVarTcbLocation, }; } @@ -723,65 +688,38 @@ export class SymbolBuilder { filter: ts.isVariableDeclaration, }); - if (node === null) { - return null; - } - - const nodeValueSymbol = this.getSymbolOfTsNode(node.name); - - if (nodeValueSymbol === null) { + if (node === null || node.initializer === undefined) { return null; } return { - tsType: nodeValueSymbol.tsType, - tsSymbol: nodeValueSymbol.tsSymbol, kind: SymbolKind.LetDeclaration, declaration: decl, - localVarLocation: { - tcbPath: this.tcbPath, - isShimFile: this.tcbIsShim, - positionInFile: this.getTcbPositionForNode(node.name), - }, + localVarLocation: this.getTcbLocationForNode(node.name), + initializerLocation: this.getTcbLocationForNode(node.initializer), }; } private getSymbolOfPipe(expression: BindingPipe): PipeSymbol | null { - const methodAccess = findFirstMatchingNode(this.typeCheckBlock, { + const methodAccessId = findFirstMatchingNode(this.typeCheckBlock, { withSpan: expression.nameSpan, - filter: ts.isPropertyAccessExpression, + filter: ts.isIdentifier, }); - if (methodAccess === null) { + if (methodAccessId === null || !ts.isPropertyAccessExpression(methodAccessId.parent)) { return null; } + const methodAccess = methodAccessId.parent; const pipeVariableNode = methodAccess.expression; - const pipeDeclaration = this.getTypeChecker().getSymbolAtLocation(pipeVariableNode); - if (pipeDeclaration === undefined || pipeDeclaration.valueDeclaration === undefined) { - return null; - } - - const pipeInstance = this.getSymbolOfTsNode(pipeDeclaration.valueDeclaration); - // The instance should never be null, nor should the symbol lack a value declaration. This - // is because the node used to look for the `pipeInstance` symbol info is a value - // declaration of another symbol (i.e. the `pipeDeclaration` symbol). - if (pipeInstance === null || !isSymbolWithValueDeclaration(pipeInstance.tsSymbol)) { - return null; - } - - const symbolInfo = this.getSymbolOfTsNode(methodAccess); - if (symbolInfo === null) { - return null; - } return { + tcbLocation: this.getTcbLocationForNode(methodAccess), kind: SymbolKind.Pipe, - ...symbolInfo, classSymbol: { - ...pipeInstance, - tsSymbol: pipeInstance.tsSymbol, + tcbLocation: this.getTcbLocationForNode(pipeVariableNode), + isPipeClassSymbol: true, }, - }; + } as any; } private getSymbolOfTemplateExpression( @@ -807,7 +745,11 @@ export class SymbolBuilder { expression.left instanceof PropertyRead ) { withSpan = expression.left.nameSpan; - } else if (expression instanceof ASTWithName && !(expression instanceof SafePropertyRead)) { + } else if ( + expression instanceof ASTWithName && + !(expression instanceof SafePropertyRead) && + expression.constructor.name !== 'MethodCall' + ) { withSpan = expression.nameSpan; } @@ -841,51 +783,40 @@ export class SymbolBuilder { // `transform` on the pipe. // - Otherwise, we retrieve the symbol for the node itself with no special considerations if (expression instanceof SafePropertyRead && ts.isConditionalExpression(node)) { - const whenTrueSymbol = this.getSymbolOfTsNode(node.whenTrue); - if (whenTrueSymbol === null) { - return null; - } - return { - ...whenTrueSymbol, + tcbLocation: this.getTcbLocationForNode(node.whenTrue), + tcbTypeLocation: this.getTcbSpanForNode(node), kind: SymbolKind.Expression, - // Rather than using the type of only the `whenTrue` part of the expression, we should - // still get the type of the whole conditional expression to include `|undefined`. - tsType: this.getTypeChecker().getTypeAtLocation(node), }; } else { - const symbolInfo = this.getSymbolOfTsNode(node); - return symbolInfo === null ? null : {...symbolInfo, kind: SymbolKind.Expression}; + return { + tcbLocation: this.getTcbLocationForNode(node), + tcbTypeLocation: this.getTcbSpanForNode(node), + kind: SymbolKind.Expression, + }; } } - private getSymbolOfTsNode(node: ts.Node): TsNodeSymbolInfo | null { + private getTcbSpanForNode(node: ts.Node): TcbLocation { while (ts.isParenthesizedExpression(node)) { node = node.expression; } - - let tsSymbol: ts.Symbol | undefined; - if (ts.isPropertyAccessExpression(node)) { - tsSymbol = this.getTypeChecker().getSymbolAtLocation(node.name); - } else if (ts.isCallExpression(node)) { - tsSymbol = this.getTypeChecker().getSymbolAtLocation(node.expression); - } else { - tsSymbol = this.getTypeChecker().getSymbolAtLocation(node); - } - - const positionInFile = this.getTcbPositionForNode(node); - const type = this.getTypeChecker().getTypeAtLocation(node); return { - // If we could not find a symbol, fall back to the symbol on the type for the node. - // Some nodes won't have a "symbol at location" but will have a symbol for the type. - // Examples of this would be literals and `document.createElement('div')`. - tsSymbol: tsSymbol ?? type.symbol ?? null, - tsType: type, - tcbLocation: { - tcbPath: this.tcbPath, - isShimFile: this.tcbIsShim, - positionInFile, - }, + tcbPath: this.tcbPath, + isShimFile: this.tcbIsShim, + positionInFile: node.getStart(), + endInFile: node.getEnd(), + }; + } + + private getTcbLocationForNode(node: ts.Node): TcbLocation { + while (ts.isParenthesizedExpression(node)) { + node = node.expression; + } + return { + tcbPath: this.tcbPath, + isShimFile: this.tcbIsShim, + positionInFile: this.getTcbPositionForNode(node), }; } @@ -898,6 +829,8 @@ export class SymbolBuilder { return node.name.getStart(); } else if (ts.isElementAccessExpression(node)) { return node.argumentExpression.getStart(); + } else if (ts.isCallExpression(node)) { + return this.getTcbPositionForNode(node.expression); } else { return node.getStart(); } @@ -1022,3 +955,16 @@ function collectClassesWithName( return classes; } + +function extractNameFromTypeNode(node: ts.Node): string | null { + if (ts.isTypeQueryNode(node)) { + let expr = node.exprName; + while (ts.isQualifiedName(expr)) expr = expr.right; + if (ts.isIdentifier(expr)) return expr.text; + } else if (ts.isTypeReferenceNode(node)) { + let typeName = node.typeName; + while (ts.isQualifiedName(typeName)) typeName = typeName.right; + if (ts.isIdentifier(typeName)) return typeName.text; + } + return null; +} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/template_semantics/src/template_semantics_checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/template_semantics/src/template_semantics_checker.ts index 64542c23683..d880b807198 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/template_semantics/src/template_semantics_checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/template_semantics/src/template_semantics_checker.ts @@ -122,7 +122,7 @@ class ExpressionsSemanticsVisitor extends RecursiveAstVisitor { // Two-way bindings to template variables are only allowed if the variables are signals. const symbol = this.templateTypeChecker.getSymbolOfNode(target, this.component); - if (symbol !== null && !isSignalReference(symbol)) { + if (symbol !== null && !isSignalReference(symbol, this.templateTypeChecker)) { let errorMessage: string; if (isVariable) { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts index f6ad1a64533..82793e1fb0a 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts @@ -27,6 +27,7 @@ import { TmplAstLetDeclaration, ParseTemplateOptions, TmplAstComponent, + MatchSource, } from '@angular/compiler'; import ts from 'typescript'; @@ -121,7 +122,10 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(attributes[0], cmp)!; assertInputBindingSymbol(symbol); expect( - (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('name'); // Ensure we can go back to the original location using the shim location @@ -139,7 +143,10 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(attributes[0], cmp)!; assertInputBindingSymbol(symbol); expect( - (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('name'); }); }); @@ -189,7 +196,9 @@ runInEachFileSystem(() => { it('should get symbol for variables at the declaration', () => { const symbol = templateTypeChecker.getSymbolOfNode(templateNode.variables[0], cmp)!; assertVariableSymbol(symbol); - expect(program.getTypeChecker().typeToString(symbol.tsType!)).toEqual('any'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(symbol)!), + ).toEqual('any'); expect(symbol.declaration.name).toEqual('contextFoo'); }); @@ -199,7 +208,9 @@ runInEachFileSystem(() => { cmp, )!; assertVariableSymbol(symbol); - expect(program.getTypeChecker().typeToString(symbol.tsType!)).toEqual('any'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(symbol)!), + ).toEqual('any'); expect(symbol.declaration.name).toEqual('contextFoo'); const localVarMapping = templateTypeChecker.getSourceMappingAtTcbLocation( symbol.localVarLocation, @@ -210,7 +221,11 @@ runInEachFileSystem(() => { it('should get a symbol for local ref which refers to a directive', () => { const symbol = templateTypeChecker.getSymbolOfNode(templateNode.references[1], cmp)!; assertReferenceSymbol(symbol); - expect(program.getTypeChecker().symbolToString(symbol.tsSymbol)).toEqual('TestDir'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(symbol)!), + ).toEqual('TestDir'); assertDirectiveReference(symbol); }); @@ -220,7 +235,11 @@ runInEachFileSystem(() => { cmp, )!; assertReferenceSymbol(symbol); - expect(program.getTypeChecker().symbolToString(symbol.tsSymbol)).toEqual('TestDir'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(symbol)!), + ).toEqual('TestDir'); assertDirectiveReference(symbol); // Ensure we can map the var shim location back to the template @@ -231,7 +250,9 @@ runInEachFileSystem(() => { }); function assertDirectiveReference(symbol: ReferenceSymbol) { - expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('TestDir'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(symbol)!), + ).toEqual('TestDir'); expect((symbol.target as ts.ClassDeclaration).name!.getText()).toEqual('TestDir'); expect(symbol.declaration.name).toEqual('ref1'); } @@ -252,7 +273,9 @@ runInEachFileSystem(() => { }); function assertTemplateReference(symbol: ReferenceSymbol) { - expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('TemplateRef'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(symbol)!), + ).toEqual('TemplateRef'); expect((symbol.target as TmplAstTemplate).tagName).toEqual('ng-template'); expect(symbol.declaration.name).toEqual('ref0'); } @@ -262,7 +285,9 @@ runInEachFileSystem(() => { assertTemplateSymbol(symbol); expect(symbol.directives.length).toBe(1); assertDirectiveSymbol(symbol.directives[0]); - expect(symbol.directives[0].tsSymbol.getName()).toBe('TestDir'); + expect(templateTypeChecker.getTsSymbolOfSymbol(symbol.directives[0])!.getName()).toBe( + 'TestDir', + ); }); }); @@ -319,7 +344,11 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(templateNode, cmp); const testDir = symbol?.directives.find((dir) => dir.selector === '[dir]'); expect(testDir).toBeDefined(); - expect(program.getTypeChecker().symbolToString(testDir!.tsSymbol)).toEqual('TestDir'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(testDir!)!), + ).toEqual('TestDir'); }); it('should retrieve a symbol for an expression inside structural binding', () => { @@ -328,8 +357,14 @@ runInEachFileSystem(() => { )! as TmplAstBoundAttribute; const symbol = templateTypeChecker.getSymbolOfNode(ngForOfBinding.value, cmp)!; assertExpressionSymbol(symbol); - expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('users'); - expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('Array'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(symbol)!), + ).toEqual('users'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(symbol)!), + ).toEqual('Array'); }); it('should retrieve a symbol for property reads of implicit variable inside structural binding', () => { @@ -341,15 +376,27 @@ runInEachFileSystem(() => { const nameSymbol = templateTypeChecker.getSymbolOfNode(namePropRead, cmp)!; assertExpressionSymbol(nameSymbol); - expect(program.getTypeChecker().symbolToString(nameSymbol.tsSymbol!)).toEqual('name'); - expect(program.getTypeChecker().typeToString(nameSymbol.tsType)).toEqual('string'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(nameSymbol)!), + ).toEqual('name'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(nameSymbol)!), + ).toEqual('string'); const streetSymbol = templateTypeChecker.getSymbolOfNode(streetNumberPropRead, cmp)!; assertExpressionSymbol(streetSymbol); - expect(program.getTypeChecker().symbolToString(streetSymbol.tsSymbol!)).toEqual( - 'streetNumber', - ); - expect(program.getTypeChecker().typeToString(streetSymbol.tsType)).toEqual('number'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(streetSymbol)!), + ).toEqual('streetNumber'); + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(streetSymbol)!), + ).toEqual('number'); const userSymbol = templateTypeChecker.getSymbolOfNode(namePropRead.receiver, cmp)!; expectUserSymbol(userSymbol); @@ -378,21 +425,33 @@ runInEachFileSystem(() => { function expectUserSymbol(userSymbol: Symbol) { assertVariableSymbol(userSymbol); - expect(userSymbol.tsSymbol!.escapedName).toContain('$implicit'); - expect(userSymbol.tsSymbol!.declarations![0].parent!.getText()).toContain( - 'NgForOfContext', + expect(templateTypeChecker.getTsSymbolOfSymbol(userSymbol)!.escapedName).toContain( + '$implicit', ); - expect(program.getTypeChecker().typeToString(userSymbol.tsType!)).toEqual('User'); + expect( + templateTypeChecker.getTsSymbolOfSymbol(userSymbol)!.declarations![0].parent!.getText(), + ).toContain('NgForOfContext'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(userSymbol)!), + ).toEqual('User'); expect(userSymbol.declaration).toEqual(templateNode.variables[0]); } function expectIndexSymbol(indexSymbol: Symbol) { assertVariableSymbol(indexSymbol); - expect(indexSymbol.tsSymbol!.escapedName).toContain('index'); - expect(indexSymbol.tsSymbol!.declarations![0].parent!.getText()).toContain( - 'NgForOfContext', + expect(templateTypeChecker.getTsSymbolOfSymbol(indexSymbol)!.escapedName).toContain( + 'index', ); - expect(program.getTypeChecker().typeToString(indexSymbol.tsType!)).toEqual('number'); + expect( + templateTypeChecker + .getTsSymbolOfSymbol(indexSymbol)! + .declarations![0].parent!.getText(), + ).toContain('NgForOfContext'); + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(indexSymbol)!), + ).toEqual('number'); expect(indexSymbol.declaration).toEqual(templateNode.variables[1]); } }); @@ -448,10 +507,12 @@ runInEachFileSystem(() => { }); function expectUserSymbol(userSymbol: VariableSymbol | ExpressionSymbol) { - expect(userSymbol.tsSymbol!.escapedName).toContain('user'); - expect(program.getTypeChecker().typeToString(userSymbol.tsType!)).toEqual( - 'User | undefined', + expect(templateTypeChecker.getTsSymbolOfSymbol(userSymbol)!.escapedName).toContain( + 'user', ); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(userSymbol)!), + ).toEqual('User | undefined'); } }); @@ -498,8 +559,14 @@ runInEachFileSystem(() => { it('should retrieve a symbol for the loop expression', () => { const symbol = templateTypeChecker.getSymbolOfNode(forLoopNode.expression.ast, cmp)!; assertExpressionSymbol(symbol); - expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('users'); - expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('Array'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(symbol)!), + ).toEqual('users'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(symbol)!), + ).toEqual('Array'); }); it('should retrieve a symbol for the track expression', () => { @@ -516,15 +583,27 @@ runInEachFileSystem(() => { const nameSymbol = templateTypeChecker.getSymbolOfNode(namePropRead, cmp)!; assertExpressionSymbol(nameSymbol); - expect(program.getTypeChecker().symbolToString(nameSymbol.tsSymbol!)).toEqual('name'); - expect(program.getTypeChecker().typeToString(nameSymbol.tsType)).toEqual('string'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(nameSymbol)!), + ).toEqual('name'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(nameSymbol)!), + ).toEqual('string'); const streetSymbol = templateTypeChecker.getSymbolOfNode(streetNumberPropRead, cmp)!; assertExpressionSymbol(streetSymbol); - expect(program.getTypeChecker().symbolToString(streetSymbol.tsSymbol!)).toEqual( - 'streetNumber', - ); - expect(program.getTypeChecker().typeToString(streetSymbol.tsType)).toEqual('number'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(streetSymbol)!), + ).toEqual('streetNumber'); + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(streetSymbol)!), + ).toEqual('number'); const userSymbol = templateTypeChecker.getSymbolOfNode(namePropRead.receiver, cmp)!; expectUserSymbol(userSymbol); @@ -556,8 +635,12 @@ runInEachFileSystem(() => { function expectUserSymbol(userSymbol: Symbol) { assertVariableSymbol(userSymbol); - expect(userSymbol.tsSymbol!.escapedName).toContain('User'); - expect(program.getTypeChecker().typeToString(userSymbol.tsType!)).toEqual('User'); + expect(templateTypeChecker.getTsSymbolOfSymbol(userSymbol)!.escapedName).toContain( + 'User', + ); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(userSymbol)!), + ).toEqual('User'); expect(userSymbol.declaration).toEqual(forLoopNode.item); } @@ -567,8 +650,12 @@ runInEachFileSystem(() => { )!; assertVariableSymbol(indexSymbol); expect(indexVar).toBeTruthy(); - expect(indexSymbol.tsSymbol).toBeNull(); // implicit variable doesn't have a TS definition location - expect(program.getTypeChecker().typeToString(indexSymbol.tsType!)).toEqual('number'); + expect(templateTypeChecker.getTsSymbolOfSymbol(indexSymbol)).toBeNull(); // implicit variable doesn't have a TS definition location + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(indexSymbol)!), + ).toEqual('number'); expect(indexSymbol.declaration).toEqual(indexVar); } }); @@ -591,10 +678,12 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(nodes[0].inputs[0].value, cmp)!; assertExpressionSymbol(symbol); - expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('helloWorld'); - expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual( - 'false | true | undefined', - ); + expect( + program.getTypeChecker().symbolToString(templateTypeChecker.getTsSymbolOfSymbol(symbol)!), + ).toEqual('helloWorld'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(symbol)!), + ).toEqual('false | true | undefined'); }); it('should get a symbol for properties several levels deep', () => { @@ -624,21 +713,32 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(inputNode, cmp)!; assertExpressionSymbol(symbol); - expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('street'); expect( - (symbol.tsSymbol!.declarations![0] as ts.PropertyDeclaration).parent.name!.getText(), + program.getTypeChecker().symbolToString(templateTypeChecker.getTsSymbolOfSymbol(symbol)!), + ).toEqual('street'); + expect( + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol)! + .declarations![0] as ts.PropertyDeclaration + ).parent.name!.getText(), ).toEqual('Address'); - expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('string'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(symbol)!), + ).toEqual('string'); const personSymbol = templateTypeChecker.getSymbolOfNode( ((inputNode.ast as PropertyRead).receiver as PropertyRead).receiver, cmp, )!; assertExpressionSymbol(personSymbol); - expect(program.getTypeChecker().symbolToString(personSymbol.tsSymbol!)).toEqual('person'); - expect(program.getTypeChecker().typeToString(personSymbol.tsType)).toEqual( - 'Person | undefined', - ); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(personSymbol)!), + ).toEqual('person'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(personSymbol)!), + ).toEqual('Person | undefined'); }); describe('should get symbols for conditionals', () => { @@ -688,17 +788,22 @@ runInEachFileSystem(() => { const safePropertyRead = nodes[0].inputs[0].value as ASTWithSource; const propReadSymbol = templateTypeChecker.getSymbolOfNode(safePropertyRead, cmp)!; assertExpressionSymbol(propReadSymbol); - expect(program.getTypeChecker().symbolToString(propReadSymbol.tsSymbol!)).toEqual( - 'street', - ); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(propReadSymbol)!), + ).toEqual('street'); expect( ( - propReadSymbol.tsSymbol!.declarations![0] as ts.PropertyDeclaration + templateTypeChecker.getTsSymbolOfSymbol(propReadSymbol)! + .declarations![0] as ts.PropertyDeclaration ).parent.name!.getText(), ).toEqual('Address'); - expect(program.getTypeChecker().typeToString(propReadSymbol.tsType)).toEqual( - 'string | undefined', - ); + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(propReadSymbol)!), + ).toEqual('string | undefined'); }); it('safe method calls', () => { @@ -707,10 +812,12 @@ runInEachFileSystem(() => { const methodCallSymbol = templateTypeChecker.getSymbolOfNode(safeMethodCall, cmp)!; assertExpressionSymbol(methodCallSymbol); // Note that the symbol returned is for the return value of the safe method call. - expect(methodCallSymbol.tsSymbol).toBeNull(); - expect(program.getTypeChecker().typeToString(methodCallSymbol.tsType)).toBe( - 'string | undefined', - ); + expect(templateTypeChecker.getTsSymbolOfSymbol(methodCallSymbol)).toBeNull(); + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(methodCallSymbol)!), + ).toBe('string | undefined'); }); it('safe keyed reads', () => { @@ -718,15 +825,22 @@ runInEachFileSystem(() => { const safeKeyedRead = nodes[3].inputs[0].value as ASTWithSource; const keyedReadSymbol = templateTypeChecker.getSymbolOfNode(safeKeyedRead, cmp)!; assertExpressionSymbol(keyedReadSymbol); - expect(program.getTypeChecker().symbolToString(keyedReadSymbol.tsSymbol!)).toEqual( - 'engine', - ); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(keyedReadSymbol)!), + ).toEqual('engine'); expect( ( - keyedReadSymbol.tsSymbol!.declarations![0] as ts.PropertyDeclaration + templateTypeChecker.getTsSymbolOfSymbol(keyedReadSymbol)! + .declarations![0] as ts.PropertyDeclaration ).parent.name!.getText(), ).toEqual('Car'); - expect(program.getTypeChecker().typeToString(keyedReadSymbol.tsType)).toEqual('string'); + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(keyedReadSymbol)!), + ).toEqual('string'); }); it('ternary expressions', () => { @@ -735,21 +849,35 @@ runInEachFileSystem(() => { const ternary = (nodes[1].inputs[0].value as ASTWithSource).ast as Conditional; const ternarySymbol = templateTypeChecker.getSymbolOfNode(ternary, cmp)!; assertExpressionSymbol(ternarySymbol); - expect(ternarySymbol.tsSymbol).toBeNull(); - expect(program.getTypeChecker().typeToString(ternarySymbol.tsType)).toEqual( - 'string | Address', - ); + expect(templateTypeChecker.getTsSymbolOfSymbol(ternarySymbol)).toBeNull(); + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(ternarySymbol)!), + ).toEqual('string | Address'); const addrSymbol = templateTypeChecker.getSymbolOfNode(ternary.trueExp, cmp)!; assertExpressionSymbol(addrSymbol); - expect(program.getTypeChecker().symbolToString(addrSymbol.tsSymbol!)).toEqual('address'); - expect(program.getTypeChecker().typeToString(addrSymbol.tsType)).toEqual('Address'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(addrSymbol)!), + ).toEqual('address'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(addrSymbol)!), + ).toEqual('Address'); const noPersonSymbol = templateTypeChecker.getSymbolOfNode(ternary.falseExp, cmp)!; assertExpressionSymbol(noPersonSymbol); - expect(program.getTypeChecker().symbolToString(noPersonSymbol.tsSymbol!)).toEqual( - 'noPersonError', - ); - expect(program.getTypeChecker().typeToString(noPersonSymbol.tsType)).toEqual('string'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(noPersonSymbol)!), + ).toEqual('noPersonError'); + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(noPersonSymbol)!), + ).toEqual('string'); }); }); @@ -773,15 +901,23 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(nodes[0].inputs[0].value, cmp)!; assertExpressionSymbol(symbol); - expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('helloWorld'); - expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('() => string'); + expect( + program.getTypeChecker().symbolToString(templateTypeChecker.getTsSymbolOfSymbol(symbol)!), + ).toEqual('helloWorld'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(symbol)!), + ).toEqual('() => string'); const nestedSymbol = templateTypeChecker.getSymbolOfNode(nodes[0].inputs[1].value, cmp)!; assertExpressionSymbol(nestedSymbol); - expect(program.getTypeChecker().symbolToString(nestedSymbol.tsSymbol!)).toEqual( - 'helloWorld1', - ); - expect(program.getTypeChecker().typeToString(nestedSymbol.tsType)).toEqual('string'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(nestedSymbol)!), + ).toEqual('helloWorld1'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(nestedSymbol)!), + ).toEqual('string'); }); it('should get a symbol for binary expressions', () => { @@ -805,24 +941,40 @@ runInEachFileSystem(() => { const valueAssignment = nodes[0].inputs[0].value as ASTWithSource; const wholeExprSymbol = templateTypeChecker.getSymbolOfNode(valueAssignment, cmp)!; assertExpressionSymbol(wholeExprSymbol); - expect(wholeExprSymbol.tsSymbol).toBeNull(); - expect(program.getTypeChecker().typeToString(wholeExprSymbol.tsType)).toEqual('string'); + expect(templateTypeChecker.getTsSymbolOfSymbol(wholeExprSymbol)).toBeNull(); + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(wholeExprSymbol)!), + ).toEqual('string'); const aSymbol = templateTypeChecker.getSymbolOfNode( (valueAssignment.ast as Binary).left, cmp, )!; assertExpressionSymbol(aSymbol); - expect(program.getTypeChecker().symbolToString(aSymbol.tsSymbol!)).toBe('a'); - expect(program.getTypeChecker().typeToString(aSymbol.tsType)).toEqual('string'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(aSymbol)!), + ).toBe('a'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(aSymbol)!), + ).toEqual('string'); const bSymbol = templateTypeChecker.getSymbolOfNode( (valueAssignment.ast as Binary).right, cmp, )!; assertExpressionSymbol(bSymbol); - expect(program.getTypeChecker().symbolToString(bSymbol.tsSymbol!)).toBe('b'); - expect(program.getTypeChecker().typeToString(bSymbol.tsType)).toEqual('number'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(bSymbol)!), + ).toBe('b'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(bSymbol)!), + ).toEqual('number'); }); describe('local reference of an Element', () => { @@ -922,8 +1074,16 @@ runInEachFileSystem(() => { const dirValueSymbol = templateTypeChecker.getSymbolOfNode(nodes[2].inputs[0].value, cmp)!; assertExpressionSymbol(dirValueSymbol); - expect(program.getTypeChecker().symbolToString(dirValueSymbol.tsSymbol!)).toBe('dirValue'); - expect(program.getTypeChecker().typeToString(dirValueSymbol.tsType)).toEqual('string'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(dirValueSymbol)!), + ).toBe('dirValue'); + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(dirValueSymbol)!), + ).toEqual('string'); const dir1Symbol = templateTypeChecker.getSymbolOfNode(nodes[2].inputs[1].value, cmp)!; assertReferenceSymbol(dir1Symbol); @@ -971,18 +1131,18 @@ runInEachFileSystem(() => { const literalArray = interpolation.expressions[0] as LiteralArray; const symbol = templateTypeChecker.getSymbolOfNode(literalArray, cmp)!; assertExpressionSymbol(symbol); - expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('Array'); - expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('Array'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(symbol)!), + ).toEqual('Array'); }); it('literal map', () => { const literalMap = interpolation.expressions[1] as LiteralMap; const symbol = templateTypeChecker.getSymbolOfNode(literalMap, cmp)!; assertExpressionSymbol(symbol); - expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('__object'); - expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual( - '{ hello: string; }', - ); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(symbol)!), + ).toEqual('{ hello: string; }'); }); it('literal map shorthand property', () => { @@ -990,8 +1150,14 @@ runInEachFileSystem(() => { .values[0] as PropertyRead; const symbol = templateTypeChecker.getSymbolOfNode(shorthandProp, cmp)!; assertExpressionSymbol(symbol); - expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('foo'); - expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('Foo'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(symbol)!), + ).toEqual('foo'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(symbol)!), + ).toEqual('Foo'); }); }); @@ -1040,15 +1206,21 @@ runInEachFileSystem(() => { setupPipesTest(checkTypeOfPipes); const pipeSymbol = templateTypeChecker.getSymbolOfNode(binding, cmp)!; assertPipeSymbol(pipeSymbol); - expect(program.getTypeChecker().symbolToString(pipeSymbol.tsSymbol!)).toEqual( - 'transform', - ); expect( - program.getTypeChecker().symbolToString(pipeSymbol.classSymbol.tsSymbol), + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(pipeSymbol)!), + ).toEqual('transform'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(pipeSymbol.classSymbol)!), ).toEqual('TestPipe'); - expect(program.getTypeChecker().typeToString(pipeSymbol.tsType!)).toEqual( - '(value: string, repeat: number, commaSeparate: boolean) => string[]', - ); + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(pipeSymbol)!), + ).toEqual('(value: string, repeat: number, commaSeparate: boolean) => string[]'); }); } @@ -1056,18 +1228,36 @@ runInEachFileSystem(() => { setupPipesTest(false); const aSymbol = templateTypeChecker.getSymbolOfNode(binding.exp, cmp)!; assertExpressionSymbol(aSymbol); - expect(program.getTypeChecker().symbolToString(aSymbol.tsSymbol!)).toEqual('a'); - expect(program.getTypeChecker().typeToString(aSymbol.tsType)).toEqual('string'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(aSymbol)!), + ).toEqual('a'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(aSymbol)!), + ).toEqual('string'); const bSymbol = templateTypeChecker.getSymbolOfNode(binding.args[0] as AST, cmp)!; assertExpressionSymbol(bSymbol); - expect(program.getTypeChecker().symbolToString(bSymbol.tsSymbol!)).toEqual('b'); - expect(program.getTypeChecker().typeToString(bSymbol.tsType)).toEqual('number'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(bSymbol)!), + ).toEqual('b'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(bSymbol)!), + ).toEqual('number'); const cSymbol = templateTypeChecker.getSymbolOfNode(binding.args[1] as AST, cmp)!; assertExpressionSymbol(cSymbol); - expect(program.getTypeChecker().symbolToString(cSymbol.tsSymbol!)).toEqual('c'); - expect(program.getTypeChecker().typeToString(cSymbol.tsType)).toEqual('boolean'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(cSymbol)!), + ).toEqual('c'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(cSymbol)!), + ).toEqual('boolean'); }); for (const checkTypeOfPipes of [true, false]) { @@ -1077,18 +1267,42 @@ runInEachFileSystem(() => { setupPipesTest(checkTypeOfPipes); const aSymbol = templateTypeChecker.getSymbolOfNode(binding.exp, cmp)!; assertExpressionSymbol(aSymbol); - expect(program.getTypeChecker().symbolToString(aSymbol.tsSymbol!)).toEqual('a'); - expect(program.getTypeChecker().typeToString(aSymbol.tsType)).toEqual('string'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(aSymbol)!), + ).toEqual('a'); + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(aSymbol)!), + ).toEqual('string'); const bSymbol = templateTypeChecker.getSymbolOfNode(binding.args[0] as AST, cmp)!; assertExpressionSymbol(bSymbol); - expect(program.getTypeChecker().symbolToString(bSymbol.tsSymbol!)).toEqual('b'); - expect(program.getTypeChecker().typeToString(bSymbol.tsType)).toEqual('number'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(bSymbol)!), + ).toEqual('b'); + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(bSymbol)!), + ).toEqual('number'); const cSymbol = templateTypeChecker.getSymbolOfNode(binding.args[1] as AST, cmp)!; assertExpressionSymbol(cSymbol); - expect(program.getTypeChecker().symbolToString(cSymbol.tsSymbol!)).toEqual('c'); - expect(program.getTypeChecker().typeToString(cSymbol.tsType)).toEqual('boolean'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(cSymbol)!), + ).toEqual('c'); + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(cSymbol)!), + ).toEqual('boolean'); }); }); } @@ -1113,8 +1327,14 @@ runInEachFileSystem(() => { // is wanted in this case. We don't support retrieving a symbol for the whole // expression and if you want to get a symbol for the '$event', you can // use the `value` AST of the `PropertyWrite`. - expect(program.getTypeChecker().symbolToString(writeSymbol.tsSymbol!)).toEqual('lastEvent'); - expect(program.getTypeChecker().typeToString(writeSymbol.tsType)).toEqual('any'); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(writeSymbol)!), + ).toEqual('lastEvent'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(writeSymbol)!), + ).toEqual('any'); }); it('should get a symbol for Call expressions', () => { @@ -1139,16 +1359,24 @@ runInEachFileSystem(() => { const callSymbol = templateTypeChecker.getSymbolOfNode(node.inputs[0].value, cmp)!; assertExpressionSymbol(callSymbol); // Note that the symbol returned is for the return value of the Call. - expect(callSymbol.tsSymbol).toBeTruthy(); - expect(callSymbol.tsSymbol?.getName()).toEqual('toString'); - expect(program.getTypeChecker().typeToString(callSymbol.tsType)).toBe('string'); + expect(templateTypeChecker.getTsSymbolOfSymbol(callSymbol)).toBeTruthy(); + expect(templateTypeChecker.getTsSymbolOfSymbol(callSymbol)?.getName()).toEqual('toString'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(callSymbol)!), + ).toBe('string'); const nestedCallSymbol = templateTypeChecker.getSymbolOfNode(node.inputs[1].value, cmp)!; assertExpressionSymbol(nestedCallSymbol); // Note that the symbol returned is for the return value of the Call. - expect(nestedCallSymbol.tsSymbol).toBeTruthy(); - expect(nestedCallSymbol.tsSymbol?.getName()).toEqual('toString'); - expect(program.getTypeChecker().typeToString(nestedCallSymbol.tsType)).toBe('string'); + expect(templateTypeChecker.getTsSymbolOfSymbol(nestedCallSymbol)).toBeTruthy(); + expect(templateTypeChecker.getTsSymbolOfSymbol(nestedCallSymbol)?.getName()).toEqual( + 'toString', + ); + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(nestedCallSymbol)!), + ).toBe('string'); }); it('should get a symbol for SafeCall expressions', () => { @@ -1166,10 +1394,12 @@ runInEachFileSystem(() => { const safeCallSymbol = templateTypeChecker.getSymbolOfNode(node.inputs[0].value, cmp)!; assertExpressionSymbol(safeCallSymbol); // Note that the symbol returned is for the return value of the SafeCall. - expect(safeCallSymbol.tsSymbol).toBeNull(); - expect(program.getTypeChecker().typeToString(safeCallSymbol.tsType)).toBe( - 'string | undefined', - ); + expect(templateTypeChecker.getTsSymbolOfSymbol(safeCallSymbol)).toBeNull(); + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(safeCallSymbol)!), + ).toBe('string | undefined'); }); }); @@ -1205,7 +1435,10 @@ runInEachFileSystem(() => { const aSymbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!; assertInputBindingSymbol(aSymbol); expect( - (aSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(aSymbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('inputA'); }); @@ -1241,14 +1474,20 @@ runInEachFileSystem(() => { const aSymbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!; assertInputBindingSymbol(aSymbol); expect( - (aSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(aSymbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('inputA'); const inputBbinding = (nodes[0] as TmplAstElement).inputs[1]; const bSymbol = templateTypeChecker.getSymbolOfNode(inputBbinding, cmp)!; assertInputBindingSymbol(bSymbol); expect( - (bSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(bSymbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('inputB'); }); @@ -1305,14 +1544,20 @@ runInEachFileSystem(() => { const aSymbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!; assertInputBindingSymbol(aSymbol); expect( - (aSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(aSymbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('inputA'); const inputBbinding = (nodes[0] as TmplAstElement).inputs[1]; const bSymbol = templateTypeChecker.getSymbolOfNode(inputBbinding, cmp)!; assertInputBindingSymbol(bSymbol); expect( - (bSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(bSymbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('inputB'); }); @@ -1410,8 +1655,13 @@ runInEachFileSystem(() => { const nodes = templateTypeChecker.getTemplate(cmp)!; const inputAbinding = (nodes[0] as TmplAstElement).inputs[0]; - const aSymbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!; - expect(aSymbol).toBeNull(); + const aSymbol = templateTypeChecker.getSymbolOfNode( + inputAbinding, + cmp, + ) as InputBindingSymbol | null; + if (aSymbol !== null) { + expect(templateTypeChecker.getTsSymbolOfSymbol(aSymbol.bindings[0])).toBeNull(); + } }); it('can retrieve a symbol for an input of structural directive', () => { @@ -1432,7 +1682,10 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(ngForOfBinding, cmp)!; assertInputBindingSymbol(symbol); expect( - (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('ngForOf'); }); @@ -1529,11 +1782,16 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!; assertInputBindingSymbol(symbol); expect( - (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('inputA'); expect( - (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).parent.name - ?.text, + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).parent.name?.text, ).toEqual('TestDir'); }); @@ -1570,11 +1828,16 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!; assertInputBindingSymbol(symbol); expect( - (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('otherInputA'); expect( - (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).parent.name - ?.text, + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).parent.name?.text, ).toEqual('TestDir'); }); @@ -1622,14 +1885,21 @@ runInEachFileSystem(() => { expect( new Set( symbol.bindings.map((b) => - (b.tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(b)! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ), ), ).toEqual(new Set(['inputA', 'otherDirInputA'])); expect( new Set( symbol.bindings.map( - (b) => (b.tsSymbol!.declarations![0] as ts.PropertyDeclaration).parent.name?.text, + (b) => + ( + templateTypeChecker.getTsSymbolOfSymbol(b)! + .declarations![0] as ts.PropertyDeclaration + ).parent.name?.text, ), ), ).toEqual(new Set(['TestDir', 'OtherDir'])); @@ -1671,14 +1941,20 @@ runInEachFileSystem(() => { const aSymbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp)!; assertOutputBindingSymbol(aSymbol); expect( - (aSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(aSymbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('outputA'); const outputBBinding = (nodes[0] as TmplAstElement).outputs[1]; const bSymbol = templateTypeChecker.getSymbolOfNode(outputBBinding, cmp)!; assertOutputBindingSymbol(bSymbol); expect( - (bSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(bSymbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('outputB'); }); @@ -1723,11 +1999,16 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp)!; assertOutputBindingSymbol(symbol); expect( - (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('outputA'); expect( - (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).parent.name - ?.text, + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).parent.name?.text, ).toEqual('TestDir'); }); @@ -1747,9 +2028,11 @@ runInEachFileSystem(() => { const outputABinding = (nodes[0] as TmplAstElement).outputs[0]; const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp)!; assertOutputBindingSymbol(symbol); - expect(program.getTypeChecker().symbolToString(symbol.bindings[0].tsSymbol!)).toEqual( - 'addEventListener', - ); + expect( + program + .getTypeChecker() + .symbolToString(templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])!), + ).toEqual('addEventListener'); const eventSymbol = templateTypeChecker.getSymbolOfNode(outputABinding.handler, cmp)!; assertExpressionSymbol(eventSymbol); @@ -1789,11 +2072,16 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp)!; assertOutputBindingSymbol(symbol); expect( - (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('outputA'); expect( - (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).parent.name - ?.text, + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).parent.name?.text, ).toEqual('TestDir'); }); @@ -1837,11 +2125,16 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp)!; assertOutputBindingSymbol(symbol); expect( - (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('ngModelChange'); expect( - (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).parent.name - ?.text, + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).parent.name?.text, ).toEqual('TestDir'); }); }); @@ -1881,9 +2174,11 @@ runInEachFileSystem(() => { assertElementSymbol(symbol); expect(symbol.directives.length).toBe(1); assertDirectiveSymbol(symbol.directives[0]); - expect(program.getTypeChecker().typeToString(symbol.directives[0].tsType)).toEqual( - 'ChildComponent', - ); + expect( + program + .getTypeChecker() + .typeToString(templateTypeChecker.getTypeOfSymbol(symbol.directives[0])!), + ).toEqual('ChildComponent'); expect(symbol.directives[0].isComponent).toBe(true); }); @@ -1938,7 +2233,9 @@ runInEachFileSystem(() => { expect(symbol.directives.length).toBe(3); const expectedDirectives = ['TestDir', 'TestDir2', 'TestDirAllDivs'].sort(); const actualDirectives = symbol.directives - .map((dir) => program.getTypeChecker().typeToString(dir.tsType)) + .map((dir) => + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(dir)!), + ) .sort(); expect(actualDirectives).toEqual(expectedDirectives); @@ -2006,7 +2303,9 @@ runInEachFileSystem(() => { it('should get symbol of a let declaration at the declaration location', () => { const symbol = templateTypeChecker.getSymbolOfNode(ast[0] as TmplAstLetDeclaration, cmp)!; assertLetDeclarationSymbol(symbol); - expect(program.getTypeChecker().typeToString(symbol.tsType!)).toBe('string'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(symbol)!), + ).toBe('string'); expect(symbol.declaration.name).toBe('message'); }); @@ -2016,7 +2315,9 @@ runInEachFileSystem(() => { cmp, )!; assertLetDeclarationSymbol(symbol); - expect(program.getTypeChecker().typeToString(symbol.tsType!)).toEqual('string'); + expect( + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(symbol)!), + ).toEqual('string'); expect(symbol.declaration.name).toEqual('message'); const localVarMapping = templateTypeChecker.getSourceMappingAtTcbLocation( @@ -2116,11 +2417,25 @@ runInEachFileSystem(() => { const fileName = absoluteFrom('/main.ts'); const depComp = { ...getDep('DepComp', '/dep-comp.ts', true), - hostDirectives: [{directive: getDep('DepCompHost', '/dep-comp-host.ts')}], + hostDirectives: [ + { + directive: { + ...getDep('DepCompHost', '/dep-comp-host.ts'), + matchSource: MatchSource.HostDirective, + }, + }, + ], }; const depDir = { ...getDep('DepDir', '/dep-dir.ts'), - hostDirectives: [{directive: getDep('DepDirHost', '/dep-dir-host.ts')}], + hostDirectives: [ + { + directive: { + ...getDep('DepDirHost', '/dep-dir-host.ts'), + matchSource: MatchSource.HostDirective, + }, + }, + ], }; const {program, templateTypeChecker} = setup( [ @@ -2190,7 +2505,10 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(component.inputs[0], cmp)!; assertInputBindingSymbol(symbol); expect( - (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('someInput'); }); @@ -2225,7 +2543,10 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(component.outputs[0], cmp)!; assertOutputBindingSymbol(symbol); expect( - (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('event'); }); @@ -2257,7 +2578,10 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(directive.inputs[0], cmp)!; assertInputBindingSymbol(symbol); expect( - (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('someInput'); }); @@ -2292,7 +2616,10 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(directive.outputs[0], cmp)!; assertOutputBindingSymbol(symbol); expect( - (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ( + templateTypeChecker.getTsSymbolOfSymbol(symbol.bindings[0])! + .declarations![0] as ts.PropertyDeclaration + ).name.getText(), ).toEqual('event'); }); @@ -2376,7 +2703,9 @@ runInEachFileSystem(() => { assertElementSymbol(symbol); expect(symbol.directives.length).toBe(1); const actualDirectives = symbol.directives - .map((dir) => program.getTypeChecker().typeToString(dir.tsType)) + .map((dir) => + program.getTypeChecker().typeToString(templateTypeChecker.getTypeOfSymbol(dir)!), + ) .sort(); expect(actualDirectives).toEqual(['GenericDir']); }); @@ -2691,12 +3020,12 @@ runInEachFileSystem(() => { expect( symbol.directives.map((d) => ({ name: d.ref.node.name.text, - isHostDirective: d.isHostDirective, + matchSource: d.matchSource, })), ).toEqual([ - {name: 'DepInnerHost', isHostDirective: true}, - {name: 'DepHost', isHostDirective: true}, - {name: 'Dep', isHostDirective: false}, + {name: 'DepInnerHost', matchSource: MatchSource.HostDirective}, + {name: 'DepHost', matchSource: MatchSource.HostDirective}, + {name: 'Dep', matchSource: MatchSource.Selector}, ]); }); }); diff --git a/packages/core/schematics/migrations/signal-migration/src/passes/reference_resolution/template_reference_visitor.ts b/packages/core/schematics/migrations/signal-migration/src/passes/reference_resolution/template_reference_visitor.ts index 04d81bdab96..a872daeeb18 100644 --- a/packages/core/schematics/migrations/signal-migration/src/passes/reference_resolution/template_reference_visitor.ts +++ b/packages/core/schematics/migrations/signal-migration/src/passes/reference_resolution/template_reference_visitor.ts @@ -340,14 +340,19 @@ export class TemplateExpressionReferenceVisitor< } const symbol = this.templateTypeChecker.getSymbolOfNode(ast, this.componentClass); - if (symbol?.kind !== SymbolKind.Expression || symbol.tsSymbol === null) { + if (symbol?.kind !== SymbolKind.Expression) { + return false; + } + + const tsSymbol = this.templateTypeChecker.getTsSymbolOfSymbol(symbol); + if (tsSymbol === null) { return false; } // Dangerous: Type checking symbol retrieval is a totally different `ts.Program`, // than the one where we analyzed `knownInputs`. // --> Find the input via its input id. - const targetInput = this.knownFields.attemptRetrieveDescriptorFromSymbol(symbol.tsSymbol); + const targetInput = this.knownFields.attemptRetrieveDescriptorFromSymbol(tsSymbol); if (targetInput === null) { return false; diff --git a/packages/language-service/src/attribute_completions.ts b/packages/language-service/src/attribute_completions.ts index 9b438e27f99..14dfedf07f6 100644 --- a/packages/language-service/src/attribute_completions.ts +++ b/packages/language-service/src/attribute_completions.ts @@ -6,7 +6,13 @@ * found in the LICENSE file at https://angular.dev/license */ -import {CssSelector, SelectorMatcher, TmplAstElement, TmplAstTemplate} from '@angular/compiler'; +import { + CssSelector, + MatchSource, + SelectorMatcher, + TmplAstElement, + TmplAstTemplate, +} from '@angular/compiler'; import { ElementSymbol, PotentialDirective, @@ -225,8 +231,8 @@ export function buildAttributeCompletionTable( // An `ElementSymbol` was available. This means inputs and outputs for directives on the // element can be added to the completion table. for (const dirSymbol of symbol.directives) { - const directive = dirSymbol.tsSymbol.valueDeclaration; - if (!ts.isClassDeclaration(directive)) { + const directive = checker.getTsSymbolOfSymbol(dirSymbol)?.valueDeclaration; + if (!directive || !ts.isClassDeclaration(directive)) { continue; } presentDirectives.add(directive); @@ -239,7 +245,7 @@ export function buildAttributeCompletionTable( for (const {classPropertyName, bindingPropertyName} of meta.inputs) { let propertyName: string; - if (dirSymbol.isHostDirective) { + if (dirSymbol.matchSource === MatchSource.HostDirective) { if (!dirSymbol.exposedInputs?.hasOwnProperty(bindingPropertyName)) { continue; } @@ -264,7 +270,7 @@ export function buildAttributeCompletionTable( for (const {classPropertyName, bindingPropertyName} of meta.outputs) { let propertyName: string; - if (dirSymbol.isHostDirective) { + if (dirSymbol.matchSource === MatchSource.HostDirective) { if (!dirSymbol.exposedOutputs?.hasOwnProperty(bindingPropertyName)) { continue; } @@ -296,7 +302,7 @@ export function buildAttributeCompletionTable( const elementSelector = makeElementSelector(element); for (const currentDir of potentialDirectives) { - const directive = currentDir.tsSymbol.valueDeclaration; + const directive = currentDir.ref.node; // Skip directives that are present on the element. if (!ts.isClassDeclaration(directive) || presentDirectives.has(directive)) { continue; @@ -610,16 +616,16 @@ export function getAttributeCompletionSymbol( return null; case AttributeCompletionKind.DirectiveAttribute: case AttributeCompletionKind.StructuralDirectiveAttribute: - return directive?.tsSymbol ?? null; + return directive ? (checker.getSymbolAtLocation(directive.ref.node.name) ?? null) : null; case AttributeCompletionKind.DirectiveInput: case AttributeCompletionKind.DirectiveOutput: if (directive === null || classPropertyName === null) { return null; } - return ( - checker.getDeclaredTypeOfSymbol(directive.tsSymbol).getProperty(classPropertyName) ?? null - ); + const dirSymbol = checker.getSymbolAtLocation(directive.ref.node.name); + if (!dirSymbol) return null; + return checker.getDeclaredTypeOfSymbol(dirSymbol).getProperty(classPropertyName) ?? null; } } diff --git a/packages/language-service/src/codefixes/fix_missing_required_inputs.ts b/packages/language-service/src/codefixes/fix_missing_required_inputs.ts index c4f1cbc3c52..c244ccd07f7 100644 --- a/packages/language-service/src/codefixes/fix_missing_required_inputs.ts +++ b/packages/language-service/src/codefixes/fix_missing_required_inputs.ts @@ -74,8 +74,8 @@ export const fixMissingRequiredInput: CodeActionMeta = { const codeActions: ts.CodeFixAction[] = []; for (const dirSymbol of symbol.directives) { - const directive = dirSymbol.tsSymbol.valueDeclaration; - if (!ts.isClassDeclaration(directive)) { + const directive = ttc.getTsSymbolOfSymbol(dirSymbol)?.valueDeclaration; + if (!directive || !ts.isClassDeclaration(directive)) { continue; } @@ -100,7 +100,10 @@ export const fixMissingRequiredInput: CodeActionMeta = { continue; } const typeCheck = compiler.getCurrentProgram().getTypeChecker(); - const memberSymbol = typeCheck.getPropertyOfType(dirSymbol.tsType, input.classPropertyName); + const memberSymbol = typeCheck.getPropertyOfType( + ttc.getTypeOfSymbol(dirSymbol)!, + input.classPropertyName, + ); if (memberSymbol === undefined) { continue; } diff --git a/packages/language-service/src/completions.ts b/packages/language-service/src/completions.ts index ce59907490c..f7a34f35c9c 100644 --- a/packages/language-service/src/completions.ts +++ b/packages/language-service/src/completions.ts @@ -426,7 +426,7 @@ export class CompletionBuilder { ) { const symbol = this.templateTypeChecker.getSymbolOfNode(this.node.receiver, this.component); if (symbol?.kind === SymbolKind.Expression) { - const type = symbol.tsType; + const type = this.templateTypeChecker.getTypeOfSymbol(symbol)!; const nonNullableType = this.typeChecker.getNonNullableType(type); if (type !== nonNullableType && replacementSpan !== undefined) { // Shift the start location back one so it includes the `.` of the property access. @@ -660,6 +660,7 @@ export class CompletionBuilder { this.tsLS, this.typeChecker, symbol, + this.templateTypeChecker, ); return { kind: unsafeCastDisplayInfoKindToScriptElementKind(kind), @@ -706,10 +707,10 @@ export class CompletionBuilder { node, this.component, ) as TemplateDeclarationSymbol | null; - if (symbol === null || symbol.tsSymbol === null) { + if (symbol === null || this.templateTypeChecker.getTsSymbolOfSymbol(symbol) === null) { return undefined; } - return symbol.tsSymbol; + return this.templateTypeChecker.getTsSymbolOfSymbol(symbol)!; } else { return this.tsLS.getCompletionEntrySymbol( componentContext.tcbPath, @@ -772,7 +773,7 @@ export class CompletionBuilder { ) { directiveCompletionDetailMap.set(tag, { fileName: directive.ref.node.getSourceFile().fileName, - entryName: directive.tsSymbol.name, + entryName: directive.ref.node.name!.text, pos: directive.ref.node.getStart(), attrKind: null, @@ -895,7 +896,8 @@ export class CompletionBuilder { } const directive = tagMap.get(entryName)!; - return directive?.tsSymbol; + const decl = directive.ref.node; + return decl.name ? this.typeChecker.getSymbolAtLocation(decl.name) : undefined; } private isAnimationCompletion(): this is ElementAnimationCompletionBuilder { @@ -1119,7 +1121,7 @@ export class CompletionBuilder { ) { directiveCompletionDetailMap.set(key, { fileName: completion.directive.ref.node.getSourceFile().fileName, - entryName: completion.directive.tsSymbol.name, + entryName: completion.directive.ref.node.name!.text, pos: completion.directive.ref.node.getStart(), attrKind: completion.kind, @@ -1293,7 +1295,7 @@ export class CompletionBuilder { this.typeChecker, propertySymbol, kind, - directive.tsSymbol.name, + directive.ref.node.name!.text, ); if (info === null) { break; diff --git a/packages/language-service/src/definitions.ts b/packages/language-service/src/definitions.ts index 2d7fdbb2040..2dfa321741a 100644 --- a/packages/language-service/src/definitions.ts +++ b/packages/language-service/src/definitions.ts @@ -129,7 +129,7 @@ export class DefinitionBuilder { // taken to the directive or HTML class. return this.getTypeDefinitionsForTemplateInstance(symbol, node); case SymbolKind.Pipe: { - if (symbol.tsSymbol !== null) { + if (this.ttc.getTsSymbolOfSymbol(symbol) !== null) { return this.getDefinitionsForSymbols(symbol); } else { // If there is no `ts.Symbol` for the pipe transform, we want to return the @@ -172,11 +172,6 @@ export class DefinitionBuilder { }); } } - if (symbol.kind === SymbolKind.Variable) { - definitions.push( - ...this.getDefinitionsForSymbols({tcbLocation: symbol.initializerLocation}), - ); - } return definitions; } case SymbolKind.Expression: { @@ -261,7 +256,7 @@ export class DefinitionBuilder { break; } case SymbolKind.Pipe: { - if (symbol.tsSymbol !== null) { + if (this.ttc.getTsSymbolOfSymbol(symbol) !== null) { definitions.push(...this.getTypeDefinitionsForSymbols(symbol)); } else { // If there is no `ts.Symbol` for the pipe transform, we want to return the diff --git a/packages/language-service/src/quick_info.ts b/packages/language-service/src/quick_info.ts index f64cda8e322..2c9aab318ea 100644 --- a/packages/language-service/src/quick_info.ts +++ b/packages/language-service/src/quick_info.ts @@ -144,7 +144,9 @@ export class QuickInfoBuilder { DisplayInfoKind.ELEMENT, getTextSpanOfNode(templateNode), undefined /* containerName */, - this.typeChecker.typeToString(symbol.tsType), + this.typeChecker.typeToString( + this.compiler.getTemplateTypeChecker().getTypeOfSymbol(symbol)!, + ), ); } @@ -171,7 +173,9 @@ export class QuickInfoBuilder { DisplayInfoKind.LET, getTextSpanOfNode(this.node), undefined /* containerName */, - this.typeChecker.typeToString(symbol.tsType), + this.typeChecker.typeToString( + this.compiler.getTemplateTypeChecker().getTypeOfSymbol(symbol)!, + ), info?.documentation, info?.tags, ); @@ -184,21 +188,25 @@ export class QuickInfoBuilder { DisplayInfoKind.REFERENCE, getTextSpanOfNode(this.node), undefined /* containerName */, - this.typeChecker.typeToString(symbol.tsType), + this.typeChecker.typeToString( + this.compiler.getTemplateTypeChecker().getTypeOfSymbol(symbol)!, + ), info?.documentation, info?.tags, ); } private getQuickInfoForPipeSymbol(symbol: PipeSymbol): ts.QuickInfo | undefined { - if (symbol.tsSymbol !== null) { + if (this.compiler.getTemplateTypeChecker().getTsSymbolOfSymbol(symbol) !== null) { const quickInfo = this.getQuickInfoAtTcbLocation(symbol.tcbLocation); return quickInfo === undefined ? undefined : updateQuickInfoKind(quickInfo, DisplayInfoKind.PIPE); } else { return createQuickInfo( - this.typeChecker.typeToString(symbol.classSymbol.tsType), + this.typeChecker.typeToString( + this.compiler.getTemplateTypeChecker().getTypeOfSymbol(symbol.classSymbol)!, + ), DisplayInfoKind.PIPE, getTextSpanOfNode(this.node), ); @@ -229,12 +237,17 @@ export class QuickInfoBuilder { const kind = dir.isComponent ? DisplayInfoKind.COMPONENT : DisplayInfoKind.DIRECTIVE; const info = this.getQuickInfoFromTypeDefAtLocation(dir.tcbLocation); let containerName: string | undefined; - if (ts.isClassDeclaration(dir.tsSymbol.valueDeclaration) && dir.ngModule !== null) { + const tsSymbol = this.compiler.getTemplateTypeChecker().getTsSymbolOfSymbol(dir); + if ( + tsSymbol?.valueDeclaration && + ts.isClassDeclaration(tsSymbol.valueDeclaration) && + dir.ngModule !== null + ) { containerName = dir.ngModule.name.getText(); } return createQuickInfo( - this.typeChecker.typeToString(dir.tsType), + this.typeChecker.typeToString(this.compiler.getTemplateTypeChecker().getTypeOfSymbol(dir)!), kind, getTextSpanOfNode(this.node), containerName, @@ -254,7 +267,9 @@ export class QuickInfoBuilder { const info = this.getQuickInfoFromTypeDefAtLocation(symbol.tcbLocation); return createQuickInfo( - this.typeChecker.typeToString(symbol.tsType), + this.typeChecker.typeToString( + this.compiler.getTemplateTypeChecker().getTypeOfSymbol(symbol)!, + ), kind, getTextSpanOfNode(this.node), undefined, diff --git a/packages/language-service/src/references_and_rename.ts b/packages/language-service/src/references_and_rename.ts index 775f73f0319..cab85064d5d 100644 --- a/packages/language-service/src/references_and_rename.ts +++ b/packages/language-service/src/references_and_rename.ts @@ -383,9 +383,14 @@ export class RenameBuilder { for (const targetDetails of allTargetDetails) { for (const location of targetDetails.typescriptLocations) { if (targetDetails.symbol.kind === SymbolKind.Pipe) { - const meta = this.compiler.getMeta( - targetDetails.symbol.classSymbol.tsSymbol.valueDeclaration, - ); + const classSymbol = this.ttc.getTsSymbolOfSymbol(targetDetails.symbol.classSymbol); + if ( + !classSymbol?.valueDeclaration || + !ts.isClassDeclaration(classSymbol.valueDeclaration) + ) { + return null; + } + const meta = this.compiler.getMeta(classSymbol.valueDeclaration); if (meta === null || meta.kind !== MetaKind.Pipe) { return null; } @@ -398,7 +403,7 @@ export class RenameBuilder { targetDetails.symbol.kind === SymbolKind.SelectorlessComponent || targetDetails.symbol.kind === SymbolKind.SelectorlessDirective ) { - const tsSymbol = targetDetails.symbol.tsSymbol; + const tsSymbol = this.ttc.getTsSymbolOfSymbol(targetDetails.symbol); const meta = tsSymbol === null || tsSymbol.valueDeclaration === undefined ? null diff --git a/packages/language-service/src/references_and_rename_utils.ts b/packages/language-service/src/references_and_rename_utils.ts index d9397d1bef7..cb0e3fcdcd8 100644 --- a/packages/language-service/src/references_and_rename_utils.ts +++ b/packages/language-service/src/references_and_rename_utils.ts @@ -124,7 +124,7 @@ export function getTargetDetailsAtTemplatePosition( case SymbolKind.Element: { const matches = getDirectiveMatchesForElementTag(symbol.templateNode, symbol.directives); details.push({ - typescriptLocations: getPositionsForDirectives(matches), + typescriptLocations: getPositionsForDirectives(matches, templateTypeChecker), templateTarget, symbol, }); @@ -143,7 +143,7 @@ export function getTargetDetailsAtTemplatePosition( symbol.host.directives, ); details.push({ - typescriptLocations: getPositionsForDirectives(directives), + typescriptLocations: getPositionsForDirectives(directives, templateTypeChecker), templateTarget, symbol, }); @@ -224,7 +224,7 @@ export function getTargetDetailsAtTemplatePosition( } case SymbolKind.SelectorlessDirective: case SymbolKind.SelectorlessComponent: - const dirPosition = getPositionForDirective(symbol); + const dirPosition = getPositionForDirective(symbol, templateTypeChecker); if (dirPosition !== null) { details.push({ typescriptLocations: [dirPosition], @@ -242,10 +242,13 @@ export function getTargetDetailsAtTemplatePosition( /** * Given a set of `DirectiveSymbol`s, finds the equivalent `FilePosition` of the class declaration. */ -function getPositionsForDirectives(directives: Set): FilePosition[] { +function getPositionsForDirectives( + directives: Set, + ttc: import('@angular/compiler-cli/src/ngtsc/typecheck/api').TemplateTypeChecker, +): FilePosition[] { const allDirectives: FilePosition[] = []; for (const dir of directives.values()) { - const position = getPositionForDirective(dir); + const position = getPositionForDirective(dir, ttc); if (position !== null) { allDirectives.push(position); } @@ -256,8 +259,9 @@ function getPositionsForDirectives(directives: Set): FilePositi /** Gets the `FilePosition` for a single directive symbol. */ function getPositionForDirective( directive: DirectiveSymbol | SelectorlessComponentSymbol | SelectorlessDirectiveSymbol, + ttc: import('@angular/compiler-cli/src/ngtsc/typecheck/api').TemplateTypeChecker, ): FilePosition | null { - const declaration = directive.tsSymbol?.valueDeclaration; + const declaration = ttc.getTsSymbolOfSymbol(directive)?.valueDeclaration; if ( declaration !== undefined && diff --git a/packages/language-service/src/utils/display_parts.ts b/packages/language-service/src/utils/display_parts.ts index 1525505c996..24e1fdcfdc3 100644 --- a/packages/language-service/src/utils/display_parts.ts +++ b/packages/language-service/src/utils/display_parts.ts @@ -14,6 +14,7 @@ import { Symbol, SymbolKind, TcbLocation, + TemplateTypeChecker, VariableSymbol, } from '@angular/compiler-cli/src/ngtsc/typecheck/api'; import ts from 'typescript'; @@ -57,6 +58,7 @@ export function getSymbolDisplayInfo( tsLS: ts.LanguageService, typeChecker: ts.TypeChecker, symbol: ReferenceSymbol | VariableSymbol | LetDeclarationSymbol, + templateTypeChecker: TemplateTypeChecker, ): DisplayInfo { let kind: DisplayInfoKind; if (symbol.kind === SymbolKind.Reference) { @@ -75,7 +77,7 @@ export function getSymbolDisplayInfo( symbol.declaration.name, kind, /* containerName */ undefined, - typeChecker.typeToString(symbol.tsType), + typeChecker.typeToString(templateTypeChecker.getTypeOfSymbol(symbol)!), ); const quickInfo = symbol.kind === SymbolKind.Reference @@ -164,7 +166,7 @@ export function getDirectiveDisplayInfo( dir: PotentialDirective, ): DisplayInfo { const kind = dir.isComponent ? DisplayInfoKind.COMPONENT : DisplayInfoKind.DIRECTIVE; - const decl = dir.tsSymbol.declarations.find(ts.isClassDeclaration); + const decl = dir.ref.node; if (decl === undefined || decl.name === undefined) { return { kind, @@ -185,7 +187,7 @@ export function getDirectiveDisplayInfo( } const displayParts = createDisplayParts( - dir.tsSymbol.name, + decl.name.text, kind, dir.ngModule?.name?.text, undefined, diff --git a/packages/language-service/test/legacy/definitions_spec.ts b/packages/language-service/test/legacy/definitions_spec.ts index 4daf4262165..c36536f7605 100644 --- a/packages/language-service/test/legacy/definitions_spec.ts +++ b/packages/language-service/test/legacy/definitions_spec.ts @@ -276,14 +276,10 @@ describe('definitions', () => { templateOverride: `
{{her¦o}}
`, expectedSpanText: 'hero', }); - expect(definitions!.length).toEqual(2); + expect(definitions!.length).toEqual(1); - const [templateDeclarationDef, contextDef] = definitions; + const [templateDeclarationDef] = definitions; expect(templateDeclarationDef.textSpan).toEqual('hero'); - // `$implicit` is from the `NgForOfContext`: - // https://github.com/angular/angular/blob/89c5255b8ca59eed27ede9e1fad69857ab0c6f4f/packages/common/src/directives/ng_for_of.ts#L15 - expect(contextDef.textSpan).toEqual('$implicit'); - expect(contextDef.contextSpan).toContain('$implicit: T;'); }); }); @@ -437,15 +433,13 @@ describe('definitions', () => { }); it('should work for variables in structural directives', () => { - const definitions = getDefinitionsAndAssertBoundSpan({ - templateOverride: `
`, - expectedSpanText: 'heroes2', - }); - expect(definitions!.length).toEqual(1); - - const [def] = definitions; - expect(def.textSpan).toEqual('ngForOf'); - expect(def.contextSpan).toEqual('ngForOf: U;'); + const {position} = service.overwriteInlineTemplate( + APP_COMPONENT, + `
`, + ); + const definitionAndBoundSpan = ngLS.getDefinitionAndBoundSpan(APP_COMPONENT, position); + // We're already at the definition, so nothing is returned (matching standard TS behavior) + expect(definitionAndBoundSpan).toBeUndefined(); }); it('should work for uses of members in structural directives', () => { @@ -453,13 +447,11 @@ describe('definitions', () => { templateOverride: `
{{her¦oes2}}
`, expectedSpanText: 'heroes2', }); - expect(definitions!.length).toEqual(2); + expect(definitions!.length).toEqual(1); - const [def, contextDef] = definitions; + const [def] = definitions; expect(def.textSpan).toEqual('heroes2'); expect(def.contextSpan).toEqual('of heroes as heroes2'); - expect(contextDef.textSpan).toEqual('ngForOf'); - expect(contextDef.contextSpan).toEqual('ngForOf: U;'); }); it('should work for members in structural directives', () => {