diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts index 04981431ee4..1161f5daf95 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts @@ -55,11 +55,21 @@ export interface TsCompletionEntryInfo { tsCompletionEntryData?: ts.CompletionEntryData; } +/** + * A reference to a symbol in a source file, without holding heavy AST nodes. + */ +export interface SymbolReference { + filePath: string; + position: number; + name: string; + moduleSpecifier?: string; +} + /** * Metadata on a directive which is available in a template. */ export interface PotentialDirective { - ref: Reference; + ref: SymbolReference; /** * The module which declares the directive. @@ -99,7 +109,7 @@ export interface PotentialDirective { * Metadata for a pipe which is available in a template. */ export interface PotentialPipe { - ref: Reference; + ref: SymbolReference; /** * 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 51d0fe1d1f8..385725f4a52 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts @@ -333,4 +333,7 @@ export interface PipeSymbol { export interface ClassSymbol { /** The position for the variable declaration for the class instance. */ tcbLocation: TcbLocation; + + /** Whether this class symbol represents a pipe. */ + isPipeClassSymbol?: boolean; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts index 5d3fc8a4017..62ac1e04f1b 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts @@ -8,19 +8,26 @@ import { AST, + BoundTarget, CssSelector, DomElementSchemaRegistry, ExternalExpr, LiteralPrimitive, ParseSourceSpan, PropertyRead, + ReferenceTarget, SafePropertyRead, + ScopedNode, + Target, TemplateEntity, + TmplAstBoundAttribute, + TmplAstBoundEvent, TmplAstComponent, TmplAstDirective, TmplAstElement, TmplAstHostElement, TmplAstNode, + TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, WrappedNodeExpr, @@ -86,6 +93,7 @@ import { SelectorlessDirectiveSymbol, Symbol, SymbolKind, + SymbolReference, TcbLocation, TemplateDiagnostic, TemplateSymbol, @@ -108,9 +116,109 @@ import {shouldReportDiagnostic, translateDiagnostic} from './diagnostics'; import {TypeCheckShimGenerator} from './shim'; import {DirectiveSourceManager} from './source'; import {findTypeCheckBlock, getSourceMapping, TypeCheckSourceResolver} from './tcb_util'; -import {SymbolBuilder} from './template_symbol_builder'; +import {SymbolBuilder, SymbolDirectiveMeta, SymbolBoundTarget} from './template_symbol_builder'; import {findAllMatchingNodes} from './comments'; +export class TypeCheckableDirectiveMetaAdapter implements SymbolDirectiveMeta { + constructor( + private meta: TypeCheckableDirectiveMeta, + private componentScopeReader: ComponentScopeReader, + ) {} + + getSymbolReference(): SymbolReference { + return { + filePath: this.meta.ref.node.getSourceFile().fileName, + position: this.meta.ref.node.name.getStart(), + name: this.meta.ref.node.name.text, + moduleSpecifier: this.meta.ref.bestGuessOwningModule?.specifier, + }; + } + + getNgModule(): ClassDeclaration | null { + if (ts.isClassDeclaration(this.meta.ref.node)) { + const scope = this.componentScopeReader.getScopeForComponent(this.meta.ref.node); + if (scope === null || scope.kind !== ComponentScopeKind.NgModule) { + return null; + } + return scope.ngModule; + } + return null; + } + + getReferenceTargetNode(): ts.ClassDeclaration | null { + return ts.isClassDeclaration(this.meta.ref.node) ? this.meta.ref.node : null; + } + + get selector() { + return this.meta.selector; + } + get isComponent() { + return this.meta.isComponent; + } + get inputs() { + return this.meta.inputs; + } + get outputs() { + return this.meta.outputs; + } + get isStructural() { + return this.meta.isStructural; + } + get hostDirectives() { + return this.meta.hostDirectives; + } + get matchSource() { + return this.meta.matchSource; + } +} + +export class BoundTargetAdapter implements SymbolBoundTarget { + constructor( + private delegate: BoundTarget, + private componentScopeReader: ComponentScopeReader, + ) {} + + getDirectivesOfNode(node: TmplAstNode): SymbolDirectiveMeta[] | null { + const dirs = this.delegate.getDirectivesOfNode(node as TmplAstElement | TmplAstTemplate); + return dirs + ? dirs.map((d) => new TypeCheckableDirectiveMetaAdapter(d, this.componentScopeReader)) + : null; + } + + getReferenceTarget(ref: TmplAstReference): ReferenceTarget | null { + const target = this.delegate.getReferenceTarget(ref); + if (target === null) return null; + if ('directive' in target) { + return { + directive: new TypeCheckableDirectiveMetaAdapter( + target.directive, + this.componentScopeReader, + ), + node: target.node, + }; + } + return target; + } + + getConsumerOfBinding( + binding: TmplAstBoundAttribute | TmplAstBoundEvent | TmplAstTextAttribute, + ): SymbolDirectiveMeta | TmplAstElement | TmplAstTemplate | null { + const consumer = this.delegate.getConsumerOfBinding(binding); + if (consumer === null) return null; + if (typeof consumer === 'object' && 'ref' in consumer) { + return new TypeCheckableDirectiveMetaAdapter( + consumer as TypeCheckableDirectiveMeta, + this.componentScopeReader, + ); + } + return consumer; + } + + getExpressionTarget(expr: AST) { + return this.delegate.getExpressionTarget(expr); + } +} + function getTcbLocationForSymbol(symbol: Symbol | BindingSymbol | ClassSymbol): TcbLocation | null { if ('tcbLocation' in symbol && symbol.tcbLocation !== undefined) { return symbol.tcbLocation as TcbLocation; @@ -255,21 +363,8 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { 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)) { + if (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; @@ -281,7 +376,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { // which will get the symbol of the local variable (e.g. _t4). } - if ('isPipeClassSymbol' in symbol && (symbol as any).isPipeClassSymbol) { + if ('isPipeClassSymbol' in symbol && symbol.isPipeClassSymbol) { const type = typeChecker.getTypeAtLocation(node); if (type && type.getSymbol()) return type.getSymbol() || null; } @@ -955,8 +1050,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { tcbPath, tcbIsShim, tcb, - data, - this.componentScopeReader, + new BoundTargetAdapter(data.boundTarget, this.componentScopeReader), this.config, ); this.symbolBuilderCache.set(component, builder); @@ -971,6 +1065,14 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return engine.getGlobalTsContext(); } + private getRefKey(ref: Reference | SymbolReference): string { + if ('filePath' in ref) { + return `${ref.filePath}#${ref.position}`; + } else { + return `${ref.node.getSourceFile().fileName}#${ref.node.name!.getStart()}`; + } + } + getPotentialTemplateDirectives( component: ts.ClassDeclaration, tsLs: ts.LanguageService, @@ -983,14 +1085,15 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return []; } - const resultingDirectives = new Map, PotentialDirective>(); + const resultingDirectives = new Map(); const directivesInScope = this.getTemplateDirectiveInScope(component); const directiveInGlobal = this.getElementsInGlobal(component, tsLs, options); for (const directive of [...directivesInScope, ...directiveInGlobal]) { - if (resultingDirectives.has(directive.ref.node)) { + const key = this.getRefKey(directive.ref); + if (resultingDirectives.has(key)) { continue; } - resultingDirectives.set(directive.ref.node, directive); + resultingDirectives.set(key, directive); } return Array.from(resultingDirectives.values()); } @@ -1005,20 +1108,20 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { // Very similar to the above `getPotentialTemplateDirectives`, but on pipes. const typeChecker = this.programDriver.getProgram().getTypeChecker(); - const resultingPipes = new Map, PotentialPipe>(); + const resultingPipes = new Map(); if (scope !== null) { const inScopePipes = this.getScopeData(component, scope)?.pipes ?? []; for (const p of inScopePipes) { - resultingPipes.set(p.ref.node, p); + resultingPipes.set(this.getRefKey(p.ref), p); } } for (const pipeClass of this.localMetaReader.getKnown(MetaKind.Pipe)) { const pipeMeta = this.metaReader.getPipeMetadata(new Reference(pipeClass)); if (pipeMeta === null) continue; - if (resultingPipes.has(pipeClass)) continue; + if (resultingPipes.has(this.getRefKey(new Reference(pipeClass)))) continue; const withScope = this.scopeDataOfPipeMeta(typeChecker, pipeMeta); if (withScope === null) continue; - resultingPipes.set(pipeClass, {...withScope, isInScope: false}); + resultingPipes.set(this.getRefKey(withScope.ref), {...withScope, isInScope: false}); } return Array.from(resultingPipes.values()); } @@ -1045,7 +1148,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { } getTemplateDirectiveInScope(component: ts.ClassDeclaration): PotentialDirective[] { - const resultingDirectives = new Map, PotentialDirective>(); + const resultingDirectives = new Map(); const scope = this.getComponentScope(component); @@ -1058,7 +1161,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { const inScopeDirectives = this.getScopeData(component, scope)?.directives ?? []; // First, all in scope directives can be used. for (const d of inScopeDirectives) { - resultingDirectives.set(d.ref.node, d); + resultingDirectives.set(this.getRefKey(d.ref), d); } } @@ -1073,12 +1176,14 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { if (directiveClass.getSourceFile().fileName !== currentComponentFileName) { continue; } - const directiveMeta = this.metaReader.getDirectiveMetadata(new Reference(directiveClass)); + const ref = new Reference(directiveClass); + const directiveMeta = this.metaReader.getDirectiveMetadata(ref); if (directiveMeta === null) continue; - if (resultingDirectives.has(directiveClass)) continue; + const key = this.getRefKey(ref); + if (resultingDirectives.has(key)) continue; const withScope = this.scopeDataOfDirectiveMeta(typeChecker, directiveMeta); if (withScope === null) continue; - resultingDirectives.set(directiveClass, {...withScope, isInScope: false}); + resultingDirectives.set(key, {...withScope, isInScope: false}); } return Array.from(resultingDirectives.values()); @@ -1548,7 +1653,12 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { } return { - ref: dep.ref, + ref: { + filePath: dep.ref.node.getSourceFile().fileName, + position: dep.ref.node.name!.getStart(), + name: dep.ref.node.name!.text, + moduleSpecifier: dep.ref.bestGuessOwningModule?.specifier, + }, isComponent: dep.isComponent, isStructural: dep.isStructural, selector: dep.selector, @@ -1561,12 +1671,16 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { typeChecker: ts.TypeChecker, dep: PipeMeta, ): Omit | null { - const tsSymbol = typeChecker.getSymbolAtLocation(dep.ref.node.name); - if (tsSymbol === undefined) { + if (!dep.ref.node.name) { return null; } return { - ref: dep.ref, + ref: { + filePath: dep.ref.node.getSourceFile().fileName, + position: dep.ref.node.name!.getStart(), + name: dep.ref.node.name!.text, + moduleSpecifier: dep.ref.bestGuessOwningModule?.specifier, + }, name: dep.name, tsCompletionEntryInfos: null, }; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts index abc7f65f477..20245849cef 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts @@ -8,6 +8,7 @@ import { BoundTarget, + DirectiveMeta, ParseError, ParseSourceFile, R3TargetBinder, @@ -80,7 +81,7 @@ export interface ShimTypeCheckingData { /** * Data tracked for each class processed by the type-checking system. */ -export interface TypeCheckData { +export interface TypeCheckData { /** * Template nodes for which the TCB was generated. */ @@ -90,7 +91,7 @@ export interface TypeCheckData { * `BoundTarget` which was used to generate the TCB, and contains bindings for the associated * template nodes. */ - boundTarget: BoundTarget; + boundTarget: BoundTarget; /** * Errors found while parsing the template, which have been converted to diagnostics. 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 40f7cf986d2..09ebf00606d 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,11 +12,15 @@ import { ASTWithSource, Binary, BindingPipe, + BoundTarget, + ClassPropertyMapping, MatchSource, ParseSourceSpan, PropertyRead, R3Identifiers, + ReferenceTarget, SafePropertyRead, + TemplateEntity, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstComponent, @@ -32,12 +36,10 @@ import { 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'; +import {isAssignment} from '../../util/src/typescript'; import { BindingSymbol, DirectiveSymbol, @@ -51,16 +53,14 @@ import { ReferenceSymbol, SelectorlessComponentSymbol, SelectorlessDirectiveSymbol, + SymbolReference, Symbol, SymbolKind, TcbLocation, TemplateSymbol, - TsNodeSymbolInfo, - TypeCheckableDirectiveMeta, TypeCheckingConfig, VariableSymbol, } from '../api'; - import { ExpressionIdentifier, findAllMatchingNodes, @@ -68,10 +68,31 @@ import { hasExpressionIdentifier, readDirectiveIdFromComment, } from './comments'; -import {TypeCheckData} from './context'; import {isAccessExpression, isDirectiveDeclaration} from './ts_util'; import {MaybeSourceFileWithOriginalFile, NgOriginalFile} from '../../program_driver'; +export interface SymbolDirectiveMeta { + getSymbolReference(): SymbolReference; + getNgModule(): ClassDeclaration | null; + getReferenceTargetNode(): ts.ClassDeclaration | null; + matchSource: MatchSource; + isComponent: boolean; + selector: string | null; + isStructural: boolean; + inputs: ClassPropertyMapping; + outputs: ClassPropertyMapping; + hostDirectives?: HostDirectiveMeta[] | null; +} + +export interface SymbolBoundTarget { + getDirectivesOfNode(node: TmplAstNode): SymbolDirectiveMeta[] | null; + getConsumerOfBinding( + binding: TmplAstBoundAttribute | TmplAstBoundEvent | TmplAstTextAttribute, + ): SymbolDirectiveMeta | TmplAstElement | TmplAstTemplate | null; + getReferenceTarget(ref: TmplAstReference): ReferenceTarget | null; + getExpressionTarget(expr: AST): TemplateEntity | null; +} + /** * Generates and caches `Symbol`s for various template structures for a given component. * @@ -85,8 +106,7 @@ export class SymbolBuilder { private readonly tcbPath: AbsoluteFsPath, private readonly tcbIsShim: boolean, private readonly typeCheckBlock: ts.Node, - private readonly typeCheckData: TypeCheckData, - private readonly componentScopeReader: ComponentScopeReader, + private readonly boundTarget: SymbolBoundTarget, private readonly typeCheckingConfig: TypeCheckingConfig, ) {} @@ -208,8 +228,7 @@ export class SymbolBuilder { templateNode: TmplAstElement | TmplAstTemplate | TmplAstComponent | TmplAstDirective, ): DirectiveSymbol[] { const elementSourceSpan = templateNode.startSourceSpan ?? templateNode.sourceSpan; - const boundDirectives = this.typeCheckData.boundTarget.getDirectivesOfNode(templateNode) ?? []; - + const boundDirectives = this.boundTarget.getDirectivesOfNode(templateNode) ?? []; let symbols = this.getDirectiveSymbolsForDirectives(boundDirectives, elementSourceSpan); // 'getDirectivesOfNode' will not return the directives intended for an element @@ -224,8 +243,7 @@ export class SymbolBuilder { templateNode instanceof TmplAstTemplate && sourceSpanEqual(firstChild.sourceSpan, templateNode.sourceSpan); if (isMicrosyntaxTemplate) { - const firstChildDirectives = - this.typeCheckData.boundTarget.getDirectivesOfNode(firstChild); + const firstChildDirectives = this.boundTarget.getDirectivesOfNode(firstChild); if (firstChildDirectives !== null) { const childSymbols = this.getDirectiveSymbolsForDirectives( firstChildDirectives, @@ -233,7 +251,11 @@ export class SymbolBuilder { ); // Merge symbols, avoiding duplicates for (const symbol of childSymbols) { - if (!symbols.some((s) => s.ref.node === symbol.ref.node)) { + if ( + !symbols.some( + (s) => s.ref.name === symbol.ref.name && s.ref.filePath === symbol.ref.filePath, + ) + ) { symbols.push(symbol); } } @@ -246,7 +268,7 @@ export class SymbolBuilder { } private getDirectiveSymbolsForDirectives( - boundDirectives: TypeCheckableDirectiveMeta[], + boundDirectives: SymbolDirectiveMeta[], span: ParseSourceSpan, ): DirectiveSymbol[] { const nodes = findAllMatchingNodes(this.typeCheckBlock, { @@ -254,19 +276,20 @@ export class SymbolBuilder { filter: isDirectiveDeclaration, }); - const hostDirectiveMap = new Map(); + 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); + const key = `${hd.directive.node.getSourceFile().fileName}#${hd.directive.node.name.text}`; + hostDirectiveMap.set(key, hd); } } } } const symbols: DirectiveSymbol[] = []; - const seenDirectives = new Set(); + const seenDirectives = new Set(); const sf = this.typeCheckBlock.getSourceFile(); for (const node of nodes) { @@ -275,19 +298,20 @@ export class SymbolBuilder { const meta = boundDirectives[id]; if (!meta) continue; - const declaration = meta.ref.node as unknown as ts.ClassDeclaration; + const ref = meta.getSymbolReference(); + const refKey = `${ref.filePath}#${ref.name}`; - if (!seenDirectives.has(declaration)) { - const ref = new Reference(declaration as ClassDeclaration); - - const hostMeta = hostDirectiveMap.get(declaration); + if (!seenDirectives.has(refKey)) { + const ref = meta.getSymbolReference(); + const key = `${ref.filePath}#${ref.name}`; + const hostMeta = hostDirectiveMap.get(key) || null; const directiveSymbol: DirectiveSymbol = hostMeta ? { tcbLocation: this.getTcbLocationForNode(node), ref, selector: meta.selector, isComponent: meta.isComponent, - ngModule: this.getDirectiveModule(declaration), + ngModule: meta.getNgModule(), kind: SymbolKind.Directive, isStructural: meta.isStructural, isInScope: true, @@ -301,7 +325,7 @@ export class SymbolBuilder { ref, selector: meta.selector, isComponent: meta.isComponent, - ngModule: this.getDirectiveModule(declaration), + ngModule: meta.getNgModule(), kind: SymbolKind.Directive, isStructural: meta.isStructural, isInScope: true, @@ -310,23 +334,15 @@ export class SymbolBuilder { }; symbols.push(directiveSymbol); - seenDirectives.add(declaration); + seenDirectives.add(refKey); } } return symbols; } - private getDirectiveModule(declaration: ts.ClassDeclaration): ClassDeclaration | null { - const scope = this.componentScopeReader.getScopeForComponent(declaration as ClassDeclaration); - if (scope === null || scope.kind !== ComponentScopeKind.NgModule) { - return null; - } - return scope.ngModule; - } - private getSymbolOfBoundEvent(eventBinding: TmplAstBoundEvent): OutputBindingSymbol | null { - const consumer = this.typeCheckData.boundTarget.getConsumerOfBinding(eventBinding); + const consumer = this.boundTarget.getConsumerOfBinding(eventBinding); if (consumer === null) { return null; } @@ -415,7 +431,7 @@ export class SymbolBuilder { private getSymbolOfInputBinding( binding: TmplAstBoundAttribute | TmplAstTextAttribute, ): InputBindingSymbol | DomBindingSymbol | null { - const consumer = this.typeCheckData.boundTarget.getConsumerOfBinding(binding); + const consumer = this.boundTarget.getConsumerOfBinding(binding); if (consumer === null) { return null; } @@ -490,18 +506,16 @@ export class SymbolBuilder { private getDirectiveSymbolForAccessExpression( fieldAccessExpr: ts.ElementAccessExpression | ts.PropertyAccessExpression, - meta: TypeCheckableDirectiveMeta, + meta: SymbolDirectiveMeta, ): DirectiveSymbol | null { - const ngModule = this.getDirectiveModule(meta.ref.node as unknown as ts.ClassDeclaration); - return { - ref: meta.ref, + ref: meta.getSymbolReference(), kind: SymbolKind.Directive, tcbLocation: this.getTcbLocationForNode(fieldAccessExpr.expression), isComponent: meta.isComponent, isStructural: meta.isStructural, selector: meta.selector, - ngModule, + ngModule: meta.getNgModule(), matchSource: MatchSource.Selector, isInScope: true, // TODO: this should always be in scope in this context, right? tsCompletionEntryInfos: null, @@ -537,7 +551,7 @@ export class SymbolBuilder { } private getSymbolOfReference(ref: TmplAstReference): ReferenceSymbol | null { - const target = this.typeCheckData.boundTarget.getReferenceTarget(ref); + const target = this.boundTarget.getReferenceTarget(ref); if (target === null) { return null; } @@ -590,14 +604,15 @@ export class SymbolBuilder { referenceVarLocation: referenceVarTcbLocation, }; } else { - if (!ts.isClassDeclaration(target.directive.ref.node)) { + const targetNode = target.directive.getReferenceTargetNode(); + if (targetNode === null) { return null; } return { kind: SymbolKind.Reference, declaration: ref, - target: target.directive.ref.node, + target: targetNode, targetLocation, referenceVarLocation: referenceVarTcbLocation, }; @@ -641,7 +656,7 @@ export class SymbolBuilder { tcbLocation: this.getTcbLocationForNode(pipeVariableNode), isPipeClassSymbol: true, }, - } as any; + }; } private getSymbolOfTemplateExpression( @@ -651,9 +666,14 @@ export class SymbolBuilder { expression = expression.ast; } - const expressionTarget = this.typeCheckData.boundTarget.getExpressionTarget(expression); + const expressionTarget = this.boundTarget.getExpressionTarget(expression); if (expressionTarget !== null) { - return this.getSymbol(expressionTarget); + return this.getSymbol(expressionTarget) as + | VariableSymbol + | ReferenceSymbol + | ExpressionSymbol + | LetDeclarationSymbol + | null; } let withSpan = expression.sourceSpan; 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 2dcc8cd02cf..f8ef1df7bf6 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 @@ -2359,7 +2359,7 @@ runInEachFileSystem(() => { const nodes = templateTypeChecker.getTemplate(cmp)!; const symbol = templateTypeChecker.getSymbolOfNode(nodes[0] as TmplAstComponent, cmp)!; assertSelectorlessComponentSymbol(symbol); - expect(symbol.directives.map((d) => d.ref.node.name.text)).toEqual(['Dep']); + expect(symbol.directives.map((d) => d.ref.name)).toEqual(['Dep']); }); it('should get symbol for a selector attribute when there are multiple directives', () => { @@ -2397,7 +2397,7 @@ runInEachFileSystem(() => { expect(symbol).toBeTruthy(); assertDomBindingSymbol(symbol!); assertElementSymbol(symbol!.host); - expect(symbol!.host.directives.map((d) => d.ref.node.name.text)).toContain('MatListItem'); + expect(symbol!.host.directives.map((d) => d.ref.name)).toContain('MatListItem'); }); it('should get symbol of a selectorless directive', () => { const fileName = absoluteFrom('/main.ts'); @@ -2418,7 +2418,7 @@ runInEachFileSystem(() => { const element = nodes[0] as TmplAstElement; const symbol = templateTypeChecker.getSymbolOfNode(element.directives[0], cmp)!; assertSelectorlessDirectiveSymbol(symbol); - expect(symbol.directives.map((d) => d.ref.node.name.text)).toEqual(['Dep']); + expect(symbol.directives.map((d) => d.ref.name)).toEqual(['Dep']); }); it('should get symbol on a node that has both selectorless components and directives', () => { @@ -2444,10 +2444,10 @@ runInEachFileSystem(() => { const directiveSymbol = templateTypeChecker.getSymbolOfNode(component.directives[0], cmp)!; assertSelectorlessComponentSymbol(componentSymbol); - expect(componentSymbol.directives.map((d) => d.ref.node.name.text)).toEqual(['DepComp']); + expect(componentSymbol.directives.map((d) => d.ref.name)).toEqual(['DepComp']); assertSelectorlessDirectiveSymbol(directiveSymbol); - expect(directiveSymbol.directives.map((d) => d.ref.node.name.text)).toEqual(['DepDir']); + expect(directiveSymbol.directives.map((d) => d.ref.name)).toEqual(['DepDir']); }); it('should get symbol of selectorless directives with host directives', () => { @@ -2502,16 +2502,13 @@ runInEachFileSystem(() => { const directiveSymbol = templateTypeChecker.getSymbolOfNode(component.directives[0], cmp)!; assertSelectorlessComponentSymbol(componentSymbol); - expect(componentSymbol.directives.map((d) => d.ref.node.name.text)).toEqual([ + expect(componentSymbol.directives.map((d) => d.ref.name)).toEqual([ 'DepCompHost', 'DepComp', ]); assertSelectorlessDirectiveSymbol(directiveSymbol); - expect(directiveSymbol.directives.map((d) => d.ref.node.name.text)).toEqual([ - 'DepDirHost', - 'DepDir', - ]); + expect(directiveSymbol.directives.map((d) => d.ref.name)).toEqual(['DepDirHost', 'DepDir']); }); it('should get symbol of a selectorless component input', () => { @@ -3055,7 +3052,7 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(element, cmp)!; assertElementSymbol(symbol); const actual = symbol.directives.map((d) => ({ - name: d.ref.node.name.text, + name: d.ref.name, matchSource: d.matchSource, })); actual.sort((a, b) => a.name.localeCompare(b.name)); diff --git a/packages/core/schematics/ng-generate/standalone-migration/prune-modules.ts b/packages/core/schematics/ng-generate/standalone-migration/prune-modules.ts index 311b976ca20..462385369eb 100644 --- a/packages/core/schematics/ng-generate/standalone-migration/prune-modules.ts +++ b/packages/core/schematics/ng-generate/standalone-migration/prune-modules.ts @@ -117,6 +117,7 @@ export function pruneNgModules( tracker, typeChecker, templateTypeChecker, + tsProgram, declarationImportRemapper, ); @@ -271,6 +272,7 @@ function replaceInComponentImportsArray( tracker: ChangeTracker, typeChecker: ts.TypeChecker, templateTypeChecker: TemplateTypeChecker, + program: ts.Program, importRemapper?: DeclarationImportsRemapper, ) { for (const [array, toReplace] of componentImportArrays.getEntries()) { @@ -282,7 +284,7 @@ function replaceInComponentImportsArray( const replacements = new UniqueItemTracker>(); const usedImports = new Set( - findTemplateDependencies(closestClass, templateTypeChecker).map((ref) => ref.node), + findTemplateDependencies(closestClass, templateTypeChecker, program).map((ref) => ref.node), ); const nodesToRemove = new Set(); diff --git a/packages/core/schematics/ng-generate/standalone-migration/standalone-bootstrap.ts b/packages/core/schematics/ng-generate/standalone-migration/standalone-bootstrap.ts index 0700cbdc5c1..00596aee972 100644 --- a/packages/core/schematics/ng-generate/standalone-migration/standalone-bootstrap.ts +++ b/packages/core/schematics/ng-generate/standalone-migration/standalone-bootstrap.ts @@ -121,6 +121,7 @@ export function toStandaloneBootstrap( allDeclarations, tracker, templateTypeChecker, + program.getTsProgram(), declarationImportRemapper, ); } diff --git a/packages/core/schematics/ng-generate/standalone-migration/to-standalone.ts b/packages/core/schematics/ng-generate/standalone-migration/to-standalone.ts index 3af575d32f2..2c5e8c1ef4f 100644 --- a/packages/core/schematics/ng-generate/standalone-migration/to-standalone.ts +++ b/packages/core/schematics/ng-generate/standalone-migration/to-standalone.ts @@ -20,7 +20,6 @@ import {ChangesByFile, ChangeTracker, ImportRemapper} from '../../utils/change_t import {getAngularDecorators, NgDecorator} from '../../utils/ng_decorators'; import {getImportSpecifier} from '../../utils/typescript/imports'; import {closestNode} from '../../utils/typescript/nodes'; -import {isReferenceToImport} from '../../utils/typescript/symbol'; import { findClassDeclaration, @@ -88,6 +87,7 @@ export function toStandalone( declarations, tracker, templateTypeChecker, + program.getTsProgram(), declarationImportRemapper, ); } @@ -119,6 +119,7 @@ export function convertNgModuleDeclarationToStandalone( allDeclarations: Set, tracker: ChangeTracker, typeChecker: TemplateTypeChecker, + program: ts.Program, importRemapper?: DeclarationImportsRemapper, ): void { const directiveMeta = typeChecker.getDirectiveMetadata(decl); @@ -132,6 +133,7 @@ export function convertNgModuleDeclarationToStandalone( allDeclarations, tracker, typeChecker, + program, importRemapper, ); @@ -175,9 +177,10 @@ function getComponentImportExpressions( allDeclarations: Set, tracker: ChangeTracker, typeChecker: TemplateTypeChecker, + program: ts.Program, importRemapper?: DeclarationImportsRemapper, ): ts.Expression[] { - const templateDependencies = findTemplateDependencies(decl, typeChecker); + const templateDependencies = findTemplateDependencies(decl, typeChecker, program); const usedDependenciesInMigration = new Set( templateDependencies.filter((dep) => allDeclarations.has(dep.node)), ); @@ -656,6 +659,7 @@ export function findTestObjectsToMigrate(sourceFile: ts.SourceFile, typeChecker: export function findTemplateDependencies( decl: ts.ClassDeclaration, typeChecker: TemplateTypeChecker, + program: ts.Program, ): Reference[] { const results: Reference[] = []; const usedDirectives = typeChecker.getUsedDirectives(decl); @@ -663,9 +667,7 @@ export function findTemplateDependencies( if (usedDirectives !== null) { for (const dir of usedDirectives) { - if (ts.isClassDeclaration(dir.ref.node)) { - results.push(dir.ref as Reference); - } + results.push(dir.ref as Reference); } } @@ -673,11 +675,17 @@ export function findTemplateDependencies( const potentialPipes = typeChecker.getPotentialPipes(decl); for (const pipe of potentialPipes) { - if ( - ts.isClassDeclaration(pipe.ref.node) && - usedPipes.some((current) => pipe.name === current) - ) { - results.push(pipe.ref as Reference); + const sourceFile = program.getSourceFile(pipe.ref.filePath); + const node = sourceFile ? findTightestNode(sourceFile, pipe.ref.position) : null; + const classDecl = node ? closestNode(node, ts.isClassDeclaration) : null; + if (classDecl && usedPipes.some((current) => pipe.name === current)) { + const owningModule = pipe.ref.moduleSpecifier + ? { + specifier: pipe.ref.moduleSpecifier, + resolutionContext: decl.getSourceFile().fileName, + } + : null; + results.push(new Reference(classDecl as NamedClassDeclaration, owningModule)); } } } @@ -947,3 +955,10 @@ function isStandaloneDeclaration( templateTypeChecker.getDirectiveMetadata(node) || templateTypeChecker.getPipeMetadata(node); return metadata != null && metadata.isStandalone; } + +function findTightestNode(node: ts.Node, position: number): ts.Node | undefined { + if (position < node.getStart() || position > node.getEnd()) { + return undefined; + } + return node.forEachChild((c) => findTightestNode(c, position)) ?? node; +} diff --git a/packages/language-service/src/attribute_completions.ts b/packages/language-service/src/attribute_completions.ts index 14dfedf07f6..3888b609aaa 100644 --- a/packages/language-service/src/attribute_completions.ts +++ b/packages/language-service/src/attribute_completions.ts @@ -24,6 +24,7 @@ import ts from 'typescript'; import {DisplayInfoKind, unsafeCastDisplayInfoKindToScriptElementKind} from './utils/display_parts'; import {makeElementSelector} from './utils'; +import {getClassDeclarationFromSymbolReference} from './utils/ts_utils'; /** * Differentiates different kinds of `AttributeCompletion`s. @@ -231,7 +232,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 = checker.getTsSymbolOfSymbol(dirSymbol)?.valueDeclaration; + const directive = getClassDeclarationFromSymbolReference(ls, dirSymbol.ref); + if (!directive || !ts.isClassDeclaration(directive)) { continue; } @@ -302,9 +304,10 @@ export function buildAttributeCompletionTable( const elementSelector = makeElementSelector(element); for (const currentDir of potentialDirectives) { - const directive = currentDir.ref.node; + const directive = getClassDeclarationFromSymbolReference(ls, currentDir.ref); + // Skip directives that are present on the element. - if (!ts.isClassDeclaration(directive) || presentDirectives.has(directive)) { + if (!directive || !ts.isClassDeclaration(directive) || presentDirectives.has(directive)) { continue; } @@ -603,11 +606,23 @@ export function addAttributeCompletionEntries( } } +function getDirectiveSymbol( + directive: PotentialDirective, + checker: ts.TypeChecker, + ls?: ts.LanguageService, +): ts.Symbol | null { + if (!ls) return null; + const classDecl = getClassDeclarationFromSymbolReference(ls, directive.ref); + if (!classDecl || !classDecl.name) return null; + return checker.getSymbolAtLocation(classDecl.name) ?? null; +} + export function getAttributeCompletionSymbol( attrKind: AttributeCompletionKind, directive: PotentialDirective | null, classPropertyName: string | null, checker: ts.TypeChecker, + ls?: ts.LanguageService, ): ts.Symbol | null { switch (attrKind) { case AttributeCompletionKind.DomAttribute: @@ -616,14 +631,14 @@ export function getAttributeCompletionSymbol( return null; case AttributeCompletionKind.DirectiveAttribute: case AttributeCompletionKind.StructuralDirectiveAttribute: - return directive ? (checker.getSymbolAtLocation(directive.ref.node.name) ?? null) : null; + return directive ? getDirectiveSymbol(directive, checker, ls) : null; case AttributeCompletionKind.DirectiveInput: case AttributeCompletionKind.DirectiveOutput: if (directive === null || classPropertyName === null) { return null; } - const dirSymbol = checker.getSymbolAtLocation(directive.ref.node.name); + const dirSymbol = getDirectiveSymbol(directive, checker, ls); if (!dirSymbol) return null; return checker.getDeclaredTypeOfSymbol(dirSymbol).getProperty(classPropertyName) ?? null; } diff --git a/packages/language-service/src/completions.ts b/packages/language-service/src/completions.ts index f7a34f35c9c..534c80defd9 100644 --- a/packages/language-service/src/completions.ts +++ b/packages/language-service/src/completions.ts @@ -65,6 +65,7 @@ import { findTightestNode, getCodeActionToImportTheDirectiveDeclaration, standaloneTraitOrNgModule, + getClassDeclarationFromSymbolReference, } from './utils/ts_utils'; import {filterAliasImports, isBoundEventWithSyntheticHandler, isWithin} from './utils'; @@ -772,9 +773,9 @@ export class CompletionBuilder { directive.tsCompletionEntryInfos.length > 0 ) { directiveCompletionDetailMap.set(tag, { - fileName: directive.ref.node.getSourceFile().fileName, - entryName: directive.ref.node.name!.text, - pos: directive.ref.node.getStart(), + fileName: directive.ref.filePath, + entryName: directive.ref.name, + pos: directive.ref.position, attrKind: null, // The Angular LS only supports displaying one directive at a time when @@ -896,7 +897,9 @@ export class CompletionBuilder { } const directive = tagMap.get(entryName)!; - const decl = directive.ref.node; + const decl = getClassDeclarationFromSymbolReference(this.tsLS, directive.ref); + + if (!decl || !ts.isClassDeclaration(decl)) return undefined; return decl.name ? this.typeChecker.getSymbolAtLocation(decl.name) : undefined; } @@ -1120,9 +1123,9 @@ export class CompletionBuilder { completion.directive.tsCompletionEntryInfos.length > 0 ) { directiveCompletionDetailMap.set(key, { - fileName: completion.directive.ref.node.getSourceFile().fileName, - entryName: completion.directive.ref.node.name!.text, - pos: completion.directive.ref.node.getStart(), + fileName: completion.directive.ref.filePath, + entryName: completion.directive.ref.name, + pos: completion.directive.ref.position, attrKind: completion.kind, // The Angular LS only supports displaying one directive at a time when @@ -1235,6 +1238,7 @@ export class CompletionBuilder { name, directive, templateTypeChecker, + this.tsLS, ); } } @@ -1276,6 +1280,7 @@ export class CompletionBuilder { directive, classPropertyName, this.typeChecker, + this.tsLS, ); if (propertySymbol === null) { break; @@ -1295,7 +1300,7 @@ export class CompletionBuilder { this.typeChecker, propertySymbol, kind, - directive.ref.node.name!.text, + directive.ref.name, ); if (info === null) { break; @@ -1376,6 +1381,7 @@ export class CompletionBuilder { 'directive' in completion ? completion.directive : null, 'classPropertyName' in completion ? completion.classPropertyName : null, this.typeChecker, + this.tsLS, ) ?? undefined ); } @@ -1536,12 +1542,14 @@ function getClassPropertyNameFromDirective( attrName: string, directive: PotentialDirective | null, templateTypeChecker: TemplateTypeChecker, + ls?: ts.LanguageService, ): string | null { if (directive === null || attrKind === null) { return null; } - const dirNode = directive.ref.node; - if (!ts.isClassDeclaration(dirNode)) { + const dirNode = ls ? getClassDeclarationFromSymbolReference(ls, directive.ref) : null; + + if (!dirNode || !ts.isClassDeclaration(dirNode)) { return null; } const meta = templateTypeChecker.getDirectiveMetadata(dirNode); diff --git a/packages/language-service/src/utils/display_parts.ts b/packages/language-service/src/utils/display_parts.ts index 24e1fdcfdc3..817bf3e9f37 100644 --- a/packages/language-service/src/utils/display_parts.ts +++ b/packages/language-service/src/utils/display_parts.ts @@ -166,17 +166,11 @@ export function getDirectiveDisplayInfo( dir: PotentialDirective, ): DisplayInfo { const kind = dir.isComponent ? DisplayInfoKind.COMPONENT : DisplayInfoKind.DIRECTIVE; - const decl = dir.ref.node; - if (decl === undefined || decl.name === undefined) { - return { - kind, - displayParts: [], - documentation: [], - tags: undefined, - }; - } + const filePath = dir.ref.filePath; + const position = dir.ref.position; + const name = dir.ref.name; - const res = tsLS.getQuickInfoAtPosition(decl.getSourceFile().fileName, decl.name.getStart()); + const res = tsLS.getQuickInfoAtPosition(filePath, position); if (res === undefined) { return { kind, @@ -186,12 +180,7 @@ export function getDirectiveDisplayInfo( }; } - const displayParts = createDisplayParts( - decl.name.text, - kind, - dir.ngModule?.name?.text, - undefined, - ); + const displayParts = createDisplayParts(name, kind, dir.ngModule?.name?.text, undefined); return { kind, diff --git a/packages/language-service/src/utils/ts_utils.ts b/packages/language-service/src/utils/ts_utils.ts index 6a58a267d2a..9ce40ef0e14 100644 --- a/packages/language-service/src/utils/ts_utils.ts +++ b/packages/language-service/src/utils/ts_utils.ts @@ -14,6 +14,7 @@ import { PotentialPipe, TemplateTypeChecker, TsCompletionEntryInfo, + SymbolReference, } from '@angular/compiler-cli/src/ngtsc/typecheck/api'; import ts from 'typescript'; import {guessIndentationInSingleLine} from './format'; @@ -35,6 +36,7 @@ export function findTightestNode(node: ts.Node, position: number): ts.Node | und export interface FindOptions { filter: (node: ts.Node) => node is T; + position?: number; } /** @@ -64,6 +66,11 @@ export function findFirstMatchingNode( if (match !== null) { return; } + if (opts.position !== undefined) { + if (currNode.getStart() > opts.position || opts.position >= currNode.getEnd()) { + return; + } + } if (opts.filter(currNode)) { match = currNode; return; @@ -74,6 +81,28 @@ export function findFirstMatchingNode( return match; } +/** + * Resolves a ClassDeclaration from a SymbolReference. + */ +export function getClassDeclarationFromSymbolReference( + ls: ts.LanguageService, + ref: SymbolReference, +): ts.ClassDeclaration | null { + const program = ls.getProgram(); + if (!program) { + return null; + } + const sf = program.getSourceFile(ref.filePath); + if (!sf) { + return null; + } + return findFirstMatchingNode(sf, { + position: ref.position, + filter: (node): node is ts.ClassDeclaration => + ts.isClassDeclaration(node) && node.name?.getStart() === ref.position, + }); +} + export function getParentClassDeclaration(startNode: ts.Node): ts.ClassDeclaration | undefined { while (startNode) { if (ts.isClassDeclaration(startNode)) { @@ -674,15 +703,31 @@ export function getCodeActionToImportTheDirectiveDeclaration( tsLs, includeCompletionsForModuleExports, ); + let ref: Reference | null = null; + const node = getClassDeclarationFromSymbolReference(tsLs, directive.ref); + if (node && node.name) { + const owningModule = directive.ref.moduleSpecifier + ? { + specifier: directive.ref.moduleSpecifier, + resolutionContext: directive.ref.filePath, + } + : null; + ref = new Reference(node as unknown as ClassDeclaration, owningModule); + } + + if (ref === null) { + return undefined; + } + const potentialImports = compiler .getTemplateTypeChecker() .getPotentialImportsFor( - directive.ref, + ref, importOn, PotentialImportMode.Normal, potentialDirectiveModuleSpecifierResolver, ); - const declarationName = directive.ref.node.name.getText(); + const declarationName = directive.ref.name; for (const potentialImport of potentialImports) { const fileImportChanges: ts.TextChange[] = []; @@ -810,9 +855,7 @@ function getStringLiteralText(moduleSpecifier: ts.Expression): string | undefine * The developer should export the `FooComponent` in the `AppModule`. * */ -class PotentialDirectiveModuleSpecifierResolverImpl - implements PotentialDirectiveModuleSpecifierResolver -{ +class PotentialDirectiveModuleSpecifierResolverImpl implements PotentialDirectiveModuleSpecifierResolver { constructor( private readonly compiler: NgCompiler, private readonly directive: PotentialDirective | PotentialPipe,