mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(compiler-cli): decouple SymbolBuilder from BoundTarget and minimize adapter surface
Decouple `SymbolBuilder` from the full `BoundTarget` interface by introducing a purpose-built `SymbolBoundTarget` interface containing only the 4 methods required for symbol resolution. This eliminates the need for the large, pass-through `BoundTargetAdapter` and further isolates `SymbolBuilder` from compiler-internal implementation details. Also minimize `TypeCheckableDirectiveMetaAdapter` by redefining `SymbolDirectiveMeta` to not extend `DirectiveMeta`, exposing only the properties actually used by `SymbolBuilder`. Removed dead code `getDirectiveMeta` in `template_symbol_builder.ts` which was unused. These changes improve maintainability and ensure a cleaner architecture by strictly defining the boundaries of what `SymbolBuilder` needs from the rest of the system. By limiting the required inputs to only what's necessary for the implementation, we make it easier to re-use the implementation between different compiler architectures
This commit is contained in:
parent
057cc6d09d
commit
d4c8a9a887
13 changed files with 361 additions and 143 deletions
|
|
@ -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<ClassDeclaration>;
|
||||
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<ClassDeclaration>;
|
||||
ref: SymbolReference;
|
||||
|
||||
/**
|
||||
* Name of the pipe.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TypeCheckableDirectiveMeta>,
|
||||
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<SymbolDirectiveMeta> | 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<ClassDeclaration> | 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<ClassDeclaration<DeclarationNode>, PotentialDirective>();
|
||||
const resultingDirectives = new Map<string, PotentialDirective>();
|
||||
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<ClassDeclaration<DeclarationNode>, PotentialPipe>();
|
||||
const resultingPipes = new Map<string, PotentialPipe>();
|
||||
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<ClassDeclaration<DeclarationNode>, PotentialDirective>();
|
||||
const resultingDirectives = new Map<string, PotentialDirective>();
|
||||
|
||||
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<PotentialPipe, 'isInScope'> | 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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<D extends DirectiveMeta = TypeCheckableDirectiveMeta> {
|
||||
/**
|
||||
* 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<TypeCheckableDirectiveMeta>;
|
||||
boundTarget: BoundTarget<D>;
|
||||
|
||||
/**
|
||||
* Errors found while parsing the template, which have been converted to diagnostics.
|
||||
|
|
|
|||
|
|
@ -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<SymbolDirectiveMeta> | 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<ts.Node, HostDirectiveMeta>();
|
||||
const hostDirectiveMap = new Map<string, HostDirectiveMeta>();
|
||||
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<ts.ClassDeclaration>();
|
||||
const seenDirectives = new Set<string>();
|
||||
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<ClassDeclaration>(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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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<ts.Node, Reference<NamedClassDeclaration>>();
|
||||
const usedImports = new Set(
|
||||
findTemplateDependencies(closestClass, templateTypeChecker).map((ref) => ref.node),
|
||||
findTemplateDependencies(closestClass, templateTypeChecker, program).map((ref) => ref.node),
|
||||
);
|
||||
const nodesToRemove = new Set<ts.Node>();
|
||||
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ export function toStandaloneBootstrap(
|
|||
allDeclarations,
|
||||
tracker,
|
||||
templateTypeChecker,
|
||||
program.getTsProgram(),
|
||||
declarationImportRemapper,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ts.ClassDeclaration>,
|
||||
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<ts.ClassDeclaration>,
|
||||
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<NamedClassDeclaration>[] {
|
||||
const results: Reference<NamedClassDeclaration>[] = [];
|
||||
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<NamedClassDeclaration>);
|
||||
}
|
||||
results.push(dir.ref as Reference<NamedClassDeclaration>);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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<NamedClassDeclaration>);
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<N extends TmplAstNode | AST> {
|
|||
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<N extends TmplAstNode | AST> {
|
|||
}
|
||||
|
||||
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<N extends TmplAstNode | AST> {
|
|||
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<N extends TmplAstNode | AST> {
|
|||
name,
|
||||
directive,
|
||||
templateTypeChecker,
|
||||
this.tsLS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1276,6 +1280,7 @@ export class CompletionBuilder<N extends TmplAstNode | AST> {
|
|||
directive,
|
||||
classPropertyName,
|
||||
this.typeChecker,
|
||||
this.tsLS,
|
||||
);
|
||||
if (propertySymbol === null) {
|
||||
break;
|
||||
|
|
@ -1295,7 +1300,7 @@ export class CompletionBuilder<N extends TmplAstNode | AST> {
|
|||
this.typeChecker,
|
||||
propertySymbol,
|
||||
kind,
|
||||
directive.ref.node.name!.text,
|
||||
directive.ref.name,
|
||||
);
|
||||
if (info === null) {
|
||||
break;
|
||||
|
|
@ -1376,6 +1381,7 @@ export class CompletionBuilder<N extends TmplAstNode | AST> {
|
|||
'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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<T extends ts.Node> {
|
||||
filter: (node: ts.Node) => node is T;
|
||||
position?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -64,6 +66,11 @@ export function findFirstMatchingNode<T extends ts.Node>(
|
|||
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<T extends ts.Node>(
|
|||
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<ClassDeclaration> | 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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue