mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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.
This commit is contained in:
parent
de12bc7e02
commit
910dcb6d6a
25 changed files with 1114 additions and 677 deletions
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -61,11 +61,6 @@ export interface TsCompletionEntryInfo {
|
|||
export interface PotentialDirective {
|
||||
ref: Reference<ClassDeclaration>;
|
||||
|
||||
/**
|
||||
* 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<ClassDeclaration>;
|
||||
|
||||
/**
|
||||
* The `ts.Symbol` for the pipe class.
|
||||
*/
|
||||
tsSymbol: ts.Symbol;
|
||||
|
||||
/**
|
||||
* Name of the pipe.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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<string, string> | null;
|
||||
exposedOutputs: Record<string, string> | 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,9 +168,12 @@ function buildDiagnosticForSignal(
|
|||
node: PropertyRead,
|
||||
component: ts.ClassDeclaration,
|
||||
): Array<NgTemplateDiagnostic<ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED>> {
|
||||
// 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,
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ class NullishCoalescingNotNullableCheck extends TemplateCheckWithVisitor<ErrorCo
|
|||
if (symbolLeft === null || symbolLeft.kind !== SymbolKind.Expression) {
|
||||
return [];
|
||||
}
|
||||
const typeLeft = symbolLeft.tsType;
|
||||
if (typeLeft.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
|
||||
const typeLeft = ctx.templateTypeChecker.getTypeOfSymbol(symbolLeft);
|
||||
if (!typeLeft || typeLeft.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
|
||||
// We should not make assumptions about the any and unknown types; using a nullish coalescing
|
||||
// operator is acceptable for those.
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -62,8 +62,8 @@ class OptionalChainNotNullableCheck extends TemplateCheckWithVisitor<ErrorCode.O
|
|||
if (symbolLeft === null || symbolLeft.kind !== SymbolKind.Expression) {
|
||||
return [];
|
||||
}
|
||||
const typeLeft = symbolLeft.tsType;
|
||||
if (typeLeft.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
|
||||
const typeLeft = ctx.templateTypeChecker.getTypeOfSymbol(symbolLeft);
|
||||
if (!typeLeft || typeLeft.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
|
||||
// We should not make assumptions about the any and unknown types; using a nullish coalescing
|
||||
// operator is acceptable for those.
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -103,7 +103,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 fullExpressionText = generateStringFromExpression(expression, expressionText);
|
||||
const errorString = formatExtendedError(
|
||||
ErrorCode.UNINVOKED_FUNCTION_IN_EVENT_BINDING,
|
||||
|
|
|
|||
|
|
@ -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}()`,
|
||||
|
|
|
|||
|
|
@ -57,22 +57,21 @@ class UninvokedTrackFunctionCheck extends TemplateCheckWithVisitor<ErrorCode.UNI
|
|||
|
||||
const symbol = ctx.templateTypeChecker.getSymbolOfNode(node.trackBy.ast, component);
|
||||
|
||||
if (
|
||||
symbol !== null &&
|
||||
symbol.kind === SymbolKind.Expression &&
|
||||
symbol.tsType.getCallSignatures()?.length > 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 [];
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ts.ClassDeclaration>();
|
||||
|
||||
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 '<div *ngFor="let user of users;" dir>',
|
||||
// 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<ts.Node, HostDirectiveMeta>();
|
||||
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<ClassDeclaration>(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<ts.ClassDeclaration>,
|
||||
): 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<ClassDeclaration> = 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<any>)
|
||||
// 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -426,7 +426,7 @@ export class CompletionBuilder<N extends TmplAstNode | AST> {
|
|||
) {
|
||||
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<N extends TmplAstNode | AST> {
|
|||
this.tsLS,
|
||||
this.typeChecker,
|
||||
symbol,
|
||||
this.templateTypeChecker,
|
||||
);
|
||||
return {
|
||||
kind: unsafeCastDisplayInfoKindToScriptElementKind(kind),
|
||||
|
|
@ -706,10 +707,10 @@ export class CompletionBuilder<N extends TmplAstNode | AST> {
|
|||
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<N extends TmplAstNode | AST> {
|
|||
) {
|
||||
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<N extends TmplAstNode | AST> {
|
|||
}
|
||||
|
||||
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<N extends TmplAstNode | AST> {
|
|||
) {
|
||||
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<N extends TmplAstNode | AST> {
|
|||
this.typeChecker,
|
||||
propertySymbol,
|
||||
kind,
|
||||
directive.tsSymbol.name,
|
||||
directive.ref.node.name!.text,
|
||||
);
|
||||
if (info === null) {
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<DirectiveSymbol>): FilePosition[] {
|
||||
function getPositionsForDirectives(
|
||||
directives: Set<DirectiveSymbol>,
|
||||
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<DirectiveSymbol>): 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 &&
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -276,14 +276,10 @@ describe('definitions', () => {
|
|||
templateOverride: `<div *ngFor="let hero of heroes">{{her¦o}}</div>`,
|
||||
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: `<div *ngFor="let item of heroes as her¦oes2; trackBy: test;"></div>`,
|
||||
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,
|
||||
`<div *ngFor="let item of heroes as her¦oes2; trackBy: test;"></div>`,
|
||||
);
|
||||
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: `<div *ngFor="let item of heroes as heroes2">{{her¦oes2}}</div>`,
|
||||
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', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue