2020-09-28 14:26:07 -04:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright Google LLC All Rights Reserved.
|
|
|
|
*
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
*/
|
2020-11-03 19:49:30 -05:00
|
|
|
import {AST, ImplicitReceiver, MethodCall, ThisReceiver, TmplAstBoundAttribute, TmplAstNode, TmplAstTextAttribute} from '@angular/compiler';
|
2020-09-28 14:26:07 -04:00
|
|
|
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
2020-11-03 19:49:30 -05:00
|
|
|
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, InputBindingSymbol, OutputBindingSymbol, PipeSymbol, ReferenceSymbol, ShimLocation, Symbol, SymbolKind, VariableSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
2020-09-28 14:26:07 -04:00
|
|
|
import * as ts from 'typescript';
|
|
|
|
|
2020-10-12 15:51:43 -04:00
|
|
|
import {createDisplayParts, DisplayInfoKind, SYMBOL_PUNC, SYMBOL_SPACE, SYMBOL_TEXT, unsafeCastDisplayInfoKindToScriptElementKind} from './display_parts';
|
2020-11-03 19:49:30 -05:00
|
|
|
import {filterAliasImports, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTextSpanOfNode} from './utils';
|
2020-09-28 14:26:07 -04:00
|
|
|
|
|
|
|
export class QuickInfoBuilder {
|
fix(compiler-cli): ensure the compiler tracks `ts.Program`s correctly (#41291)
`NgCompiler` previously had a notion of the "next" `ts.Program`, which
served two purposes:
* it allowed a client using the `ts.createProgram` API to query for the
latest program produced by the previous `NgCompiler`, as a starting
point for building the _next_ program that incorporated any new user
changes.
* it allowed the old `NgCompiler` to be queried for the `ts.Program` on
which all prior state is based, which is needed to compute the delta
from the new program to ultimately determine how much of the prior
state can be reused.
This system contained a flaw: it relied on the `NgCompiler` knowing when
the `ts.Program` would be changed. This works fine for changes that
originate in `NgCompiler` APIs, but a client of the `TemplateTypeChecker`
may use that API in ways that create new `ts.Program`s without the
`NgCompiler`'s knowledge. This caused the `NgCompiler`'s concept of the
"next" program to get out of sync, causing incorrectness in future
incremental analysis.
This refactoring cleans up the compiler's `ts.Program` management in
several ways:
* `TypeCheckingProgramStrategy`, the API which controls `ts.Program`
updating, is renamed to the `ProgramDriver` and extracted to a separate
ngtsc package.
* It loses its responsibility of determining component shim filenames. That
functionality now lives exclusively in the template type-checking package.
* The "next" `ts.Program` concept is renamed to the "current" program, as
the "next" name was misleading in several ways.
* `NgCompiler` now wraps the `ProgramDriver` used in the
`TemplateTypeChecker` to know when a new `ts.Program` is created,
regardless of which API drove the creation, which actually fixes the bug.
PR Close #41291
2021-03-19 20:06:10 -04:00
|
|
|
private readonly typeChecker = this.compiler.getCurrentProgram().getTypeChecker();
|
2020-09-28 14:26:07 -04:00
|
|
|
|
2020-10-13 13:28:15 -04:00
|
|
|
constructor(
|
|
|
|
private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler,
|
|
|
|
private readonly component: ts.ClassDeclaration, private node: TmplAstNode|AST) {}
|
2020-09-28 14:26:07 -04:00
|
|
|
|
2020-10-13 13:28:15 -04:00
|
|
|
get(): ts.QuickInfo|undefined {
|
|
|
|
const symbol =
|
|
|
|
this.compiler.getTemplateTypeChecker().getSymbolOfNode(this.node, this.component);
|
2020-09-28 14:26:07 -04:00
|
|
|
if (symbol === null) {
|
2020-10-13 13:28:15 -04:00
|
|
|
return isDollarAny(this.node) ? createDollarAnyQuickInfo(this.node) : undefined;
|
2020-09-28 14:26:07 -04:00
|
|
|
}
|
|
|
|
|
2020-10-13 13:28:15 -04:00
|
|
|
return this.getQuickInfoForSymbol(symbol);
|
2020-09-28 14:26:07 -04:00
|
|
|
}
|
|
|
|
|
2020-10-13 13:28:15 -04:00
|
|
|
private getQuickInfoForSymbol(symbol: Symbol): ts.QuickInfo|undefined {
|
2020-09-28 14:26:07 -04:00
|
|
|
switch (symbol.kind) {
|
|
|
|
case SymbolKind.Input:
|
|
|
|
case SymbolKind.Output:
|
2020-10-13 13:28:15 -04:00
|
|
|
return this.getQuickInfoForBindingSymbol(symbol);
|
2020-09-28 14:26:07 -04:00
|
|
|
case SymbolKind.Template:
|
2020-10-13 13:28:15 -04:00
|
|
|
return createNgTemplateQuickInfo(this.node);
|
2020-09-28 14:26:07 -04:00
|
|
|
case SymbolKind.Element:
|
|
|
|
return this.getQuickInfoForElementSymbol(symbol);
|
|
|
|
case SymbolKind.Variable:
|
2020-10-13 13:28:15 -04:00
|
|
|
return this.getQuickInfoForVariableSymbol(symbol);
|
2020-09-28 14:26:07 -04:00
|
|
|
case SymbolKind.Reference:
|
2020-10-13 13:28:15 -04:00
|
|
|
return this.getQuickInfoForReferenceSymbol(symbol);
|
2020-09-28 14:26:07 -04:00
|
|
|
case SymbolKind.DomBinding:
|
2020-10-13 13:28:15 -04:00
|
|
|
return this.getQuickInfoForDomBinding(symbol);
|
2020-09-28 14:26:07 -04:00
|
|
|
case SymbolKind.Directive:
|
2020-10-13 13:28:15 -04:00
|
|
|
return this.getQuickInfoAtShimLocation(symbol.shimLocation);
|
2020-11-03 19:49:30 -05:00
|
|
|
case SymbolKind.Pipe:
|
|
|
|
return this.getQuickInfoForPipeSymbol(symbol);
|
2020-09-28 14:26:07 -04:00
|
|
|
case SymbolKind.Expression:
|
2020-11-03 19:49:30 -05:00
|
|
|
return this.getQuickInfoAtShimLocation(symbol.shimLocation);
|
2020-09-28 14:26:07 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-13 13:28:15 -04:00
|
|
|
private getQuickInfoForBindingSymbol(symbol: InputBindingSymbol|OutputBindingSymbol): ts.QuickInfo
|
2020-09-28 14:26:07 -04:00
|
|
|
|undefined {
|
|
|
|
if (symbol.bindings.length === 0) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2020-10-12 15:51:43 -04:00
|
|
|
const kind =
|
|
|
|
symbol.kind === SymbolKind.Input ? DisplayInfoKind.PROPERTY : DisplayInfoKind.EVENT;
|
2020-09-28 14:26:07 -04:00
|
|
|
|
2020-10-13 13:28:15 -04:00
|
|
|
const quickInfo = this.getQuickInfoAtShimLocation(symbol.bindings[0].shimLocation);
|
2020-09-28 14:26:07 -04:00
|
|
|
return quickInfo === undefined ? undefined : updateQuickInfoKind(quickInfo, kind);
|
|
|
|
}
|
|
|
|
|
|
|
|
private getQuickInfoForElementSymbol(symbol: ElementSymbol): ts.QuickInfo {
|
|
|
|
const {templateNode} = symbol;
|
2020-10-02 16:54:18 -04:00
|
|
|
const matches = getDirectiveMatchesForElementTag(templateNode, symbol.directives);
|
2020-09-28 14:26:07 -04:00
|
|
|
if (matches.size > 0) {
|
|
|
|
return this.getQuickInfoForDirectiveSymbol(matches.values().next().value, templateNode);
|
|
|
|
}
|
|
|
|
|
|
|
|
return createQuickInfo(
|
2020-10-12 15:51:43 -04:00
|
|
|
templateNode.name, DisplayInfoKind.ELEMENT, getTextSpanOfNode(templateNode),
|
2020-09-28 14:26:07 -04:00
|
|
|
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType));
|
|
|
|
}
|
|
|
|
|
2020-10-13 13:28:15 -04:00
|
|
|
private getQuickInfoForVariableSymbol(symbol: VariableSymbol): ts.QuickInfo {
|
2020-11-16 14:22:11 -05:00
|
|
|
const documentation = this.getDocumentationFromTypeDefAtLocation(symbol.initializerLocation);
|
2020-09-28 14:26:07 -04:00
|
|
|
return createQuickInfo(
|
2020-10-13 13:28:15 -04:00
|
|
|
symbol.declaration.name, DisplayInfoKind.VARIABLE, getTextSpanOfNode(this.node),
|
2020-09-28 14:26:07 -04:00
|
|
|
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType), documentation);
|
|
|
|
}
|
|
|
|
|
2020-10-13 13:28:15 -04:00
|
|
|
private getQuickInfoForReferenceSymbol(symbol: ReferenceSymbol): ts.QuickInfo {
|
2020-11-16 14:22:11 -05:00
|
|
|
const documentation = this.getDocumentationFromTypeDefAtLocation(symbol.targetLocation);
|
2020-09-28 14:26:07 -04:00
|
|
|
return createQuickInfo(
|
2020-10-13 13:28:15 -04:00
|
|
|
symbol.declaration.name, DisplayInfoKind.REFERENCE, getTextSpanOfNode(this.node),
|
2020-09-28 14:26:07 -04:00
|
|
|
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType), documentation);
|
|
|
|
}
|
|
|
|
|
2020-11-03 19:49:30 -05:00
|
|
|
private getQuickInfoForPipeSymbol(symbol: PipeSymbol): ts.QuickInfo|undefined {
|
|
|
|
if (symbol.tsSymbol !== null) {
|
|
|
|
const quickInfo = this.getQuickInfoAtShimLocation(symbol.shimLocation);
|
|
|
|
return quickInfo === undefined ? undefined :
|
|
|
|
updateQuickInfoKind(quickInfo, DisplayInfoKind.PIPE);
|
|
|
|
} else {
|
|
|
|
return createQuickInfo(
|
|
|
|
this.typeChecker.typeToString(symbol.classSymbol.tsType), DisplayInfoKind.PIPE,
|
|
|
|
getTextSpanOfNode(this.node));
|
|
|
|
}
|
2020-09-28 14:26:07 -04:00
|
|
|
}
|
|
|
|
|
2020-10-13 13:28:15 -04:00
|
|
|
private getQuickInfoForDomBinding(symbol: DomBindingSymbol) {
|
|
|
|
if (!(this.node instanceof TmplAstTextAttribute) &&
|
|
|
|
!(this.node instanceof TmplAstBoundAttribute)) {
|
2020-09-28 14:26:07 -04:00
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
const directives = getDirectiveMatchesForAttribute(
|
2020-10-13 13:28:15 -04:00
|
|
|
this.node.name, symbol.host.templateNode, symbol.host.directives);
|
2020-09-28 14:26:07 -04:00
|
|
|
if (directives.size === 0) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2020-10-13 13:28:15 -04:00
|
|
|
return this.getQuickInfoForDirectiveSymbol(directives.values().next().value);
|
2020-09-28 14:26:07 -04:00
|
|
|
}
|
|
|
|
|
2020-10-13 13:28:15 -04:00
|
|
|
private getQuickInfoForDirectiveSymbol(dir: DirectiveSymbol, node: TmplAstNode|AST = this.node):
|
2020-09-28 14:26:07 -04:00
|
|
|
ts.QuickInfo {
|
2020-10-12 15:51:43 -04:00
|
|
|
const kind = dir.isComponent ? DisplayInfoKind.COMPONENT : DisplayInfoKind.DIRECTIVE;
|
2020-09-28 14:26:07 -04:00
|
|
|
const documentation = this.getDocumentationFromTypeDefAtLocation(dir.shimLocation);
|
2020-10-02 15:13:25 -04:00
|
|
|
let containerName: string|undefined;
|
|
|
|
if (ts.isClassDeclaration(dir.tsSymbol.valueDeclaration) && dir.ngModule !== null) {
|
|
|
|
containerName = dir.ngModule.name.getText();
|
|
|
|
}
|
|
|
|
|
2020-09-28 14:26:07 -04:00
|
|
|
return createQuickInfo(
|
2020-10-13 13:28:15 -04:00
|
|
|
this.typeChecker.typeToString(dir.tsType), kind, getTextSpanOfNode(this.node),
|
|
|
|
containerName, undefined, documentation);
|
2020-09-28 14:26:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
private getDocumentationFromTypeDefAtLocation(shimLocation: ShimLocation):
|
|
|
|
ts.SymbolDisplayPart[]|undefined {
|
|
|
|
const typeDefs = this.tsLS.getTypeDefinitionAtPosition(
|
|
|
|
shimLocation.shimPath, shimLocation.positionInShimFile);
|
|
|
|
if (typeDefs === undefined || typeDefs.length === 0) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
return this.tsLS.getQuickInfoAtPosition(typeDefs[0].fileName, typeDefs[0].textSpan.start)
|
|
|
|
?.documentation;
|
|
|
|
}
|
|
|
|
|
2020-10-13 13:28:15 -04:00
|
|
|
private getQuickInfoAtShimLocation(location: ShimLocation): ts.QuickInfo|undefined {
|
2020-09-28 14:26:07 -04:00
|
|
|
const quickInfo =
|
|
|
|
this.tsLS.getQuickInfoAtPosition(location.shimPath, location.positionInShimFile);
|
|
|
|
if (quickInfo === undefined || quickInfo.displayParts === undefined) {
|
|
|
|
return quickInfo;
|
|
|
|
}
|
|
|
|
|
|
|
|
quickInfo.displayParts = filterAliasImports(quickInfo.displayParts);
|
|
|
|
|
2020-10-13 13:28:15 -04:00
|
|
|
const textSpan = getTextSpanOfNode(this.node);
|
2020-09-28 14:26:07 -04:00
|
|
|
return {...quickInfo, textSpan};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-12 15:51:43 -04:00
|
|
|
function updateQuickInfoKind(quickInfo: ts.QuickInfo, kind: DisplayInfoKind): ts.QuickInfo {
|
2020-09-28 14:26:07 -04:00
|
|
|
if (quickInfo.displayParts === undefined) {
|
|
|
|
return quickInfo;
|
|
|
|
}
|
|
|
|
|
|
|
|
const startsWithKind = quickInfo.displayParts.length >= 3 &&
|
|
|
|
displayPartsEqual(quickInfo.displayParts[0], {text: '(', kind: SYMBOL_PUNC}) &&
|
|
|
|
quickInfo.displayParts[1].kind === SYMBOL_TEXT &&
|
|
|
|
displayPartsEqual(quickInfo.displayParts[2], {text: ')', kind: SYMBOL_PUNC});
|
|
|
|
if (startsWithKind) {
|
|
|
|
quickInfo.displayParts[1].text = kind;
|
|
|
|
} else {
|
|
|
|
quickInfo.displayParts = [
|
|
|
|
{text: '(', kind: SYMBOL_PUNC},
|
|
|
|
{text: kind, kind: SYMBOL_TEXT},
|
|
|
|
{text: ')', kind: SYMBOL_PUNC},
|
|
|
|
{text: ' ', kind: SYMBOL_SPACE},
|
|
|
|
...quickInfo.displayParts,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
return quickInfo;
|
|
|
|
}
|
|
|
|
|
|
|
|
function displayPartsEqual(a: {text: string, kind: string}, b: {text: string, kind: string}) {
|
|
|
|
return a.text === b.text && a.kind === b.kind;
|
|
|
|
}
|
|
|
|
|
|
|
|
function isDollarAny(node: TmplAstNode|AST): node is MethodCall {
|
|
|
|
return node instanceof MethodCall && node.receiver instanceof ImplicitReceiver &&
|
fix(compiler): preserve this.$event and this.$any accesses in expressions (#39323)
Currently expressions `$event.foo()` and `this.$event.foo()`, as well as `$any(foo)` and
`this.$any(foo)`, are treated as the same expression by the compiler, because `this` is considered
the same implicit receiver as when the receiver is omitted. This introduces the following issues:
1. Any time something called `$any` is used, it'll be stripped away, leaving only the first parameter.
2. If something called `$event` is used anywhere in a template, it'll be preserved as `$event`,
rather than being rewritten to `ctx.$event`, causing the value to undefined at runtime. This
applies to listener, property and text bindings.
These changes resolve the first issue and part of the second one by preserving anything that
is accessed through `this`, even if it's one of the "special" ones like `$any` or `$event`.
Furthermore, these changes only expose the `$event` global variable inside event listeners,
whereas previously it was available everywhere.
Fixes #30278.
PR Close #39323
2020-10-18 11:41:29 -04:00
|
|
|
!(node.receiver instanceof ThisReceiver) && node.name === '$any' && node.args.length === 1;
|
2020-09-28 14:26:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
function createDollarAnyQuickInfo(node: MethodCall): ts.QuickInfo {
|
|
|
|
return createQuickInfo(
|
|
|
|
'$any',
|
2020-10-12 15:51:43 -04:00
|
|
|
DisplayInfoKind.METHOD,
|
2020-09-28 14:26:07 -04:00
|
|
|
getTextSpanOfNode(node),
|
|
|
|
/** containerName */ undefined,
|
|
|
|
'any',
|
|
|
|
[{
|
|
|
|
kind: SYMBOL_TEXT,
|
|
|
|
text: 'function to cast an expression to the `any` type',
|
|
|
|
}],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO(atscott): Create special `ts.QuickInfo` for `ng-template` and `ng-container` as well.
|
|
|
|
function createNgTemplateQuickInfo(node: TmplAstNode|AST): ts.QuickInfo {
|
|
|
|
return createQuickInfo(
|
|
|
|
'ng-template',
|
2020-10-12 15:51:43 -04:00
|
|
|
DisplayInfoKind.TEMPLATE,
|
2020-09-28 14:26:07 -04:00
|
|
|
getTextSpanOfNode(node),
|
|
|
|
/** containerName */ undefined,
|
|
|
|
/** type */ undefined,
|
|
|
|
[{
|
|
|
|
kind: SYMBOL_TEXT,
|
|
|
|
text:
|
|
|
|
'The `<ng-template>` is an Angular element for rendering HTML. It is never displayed directly.',
|
|
|
|
}],
|
|
|
|
);
|
|
|
|
}
|
2020-10-12 15:51:43 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Construct a QuickInfo object taking into account its container and type.
|
|
|
|
* @param name Name of the QuickInfo target
|
|
|
|
* @param kind component, directive, pipe, etc.
|
|
|
|
* @param textSpan span of the target
|
|
|
|
* @param containerName either the Symbol's container or the NgModule that contains the directive
|
|
|
|
* @param type user-friendly name of the type
|
|
|
|
* @param documentation docstring or comment
|
|
|
|
*/
|
|
|
|
export function createQuickInfo(
|
|
|
|
name: string, kind: DisplayInfoKind, textSpan: ts.TextSpan, containerName?: string,
|
|
|
|
type?: string, documentation?: ts.SymbolDisplayPart[]): ts.QuickInfo {
|
|
|
|
const displayParts = createDisplayParts(name, kind, containerName, type);
|
|
|
|
|
|
|
|
return {
|
|
|
|
kind: unsafeCastDisplayInfoKindToScriptElementKind(kind),
|
|
|
|
kindModifiers: ts.ScriptElementKindModifier.none,
|
|
|
|
textSpan: textSpan,
|
|
|
|
displayParts,
|
|
|
|
documentation,
|
|
|
|
};
|
2020-11-16 14:22:11 -05:00
|
|
|
}
|