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
|
|
|
|
*/
|
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
|
|
|
import {AST, BindingPipe, 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';
|
|
|
|
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, ReferenceSymbol, ShimLocation, Symbol, SymbolKind, VariableSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
|
|
|
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-10-02 16:54:18 -04:00
|
|
|
import {filterAliasImports, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, getTextSpanOfNode} from './utils';
|
2020-09-28 14:26:07 -04:00
|
|
|
|
|
|
|
export class QuickInfoBuilder {
|
|
|
|
private readonly typeChecker = this.compiler.getNextProgram().getTypeChecker();
|
|
|
|
|
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-09-28 14:26:07 -04:00
|
|
|
case SymbolKind.Expression:
|
2020-10-13 13:28:15 -04:00
|
|
|
return this.node instanceof BindingPipe ?
|
|
|
|
this.getQuickInfoForPipeSymbol(symbol) :
|
|
|
|
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-10-13 13:28:15 -04:00
|
|
|
private getQuickInfoForPipeSymbol(symbol: ExpressionSymbol): ts.QuickInfo|undefined {
|
|
|
|
const quickInfo = this.getQuickInfoAtShimLocation(symbol.shimLocation);
|
2020-10-12 15:51:43 -04:00
|
|
|
return quickInfo === undefined ? undefined :
|
|
|
|
updateQuickInfoKind(quickInfo, DisplayInfoKind.PIPE);
|
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
|
|
|
}
|