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-01 16:30:03 -04:00
|
|
|
import {createQuickInfo, SYMBOL_PUNC, SYMBOL_SPACE, SYMBOL_TEXT} from '../common/quick_info';
|
2020-09-28 14:26:07 -04:00
|
|
|
|
|
|
|
import {findNodeAtPosition} from './hybrid_visitor';
|
2020-10-02 16:54:18 -04:00
|
|
|
import {filterAliasImports, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, getTextSpanOfNode} from './utils';
|
2020-09-28 14:26:07 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The type of Angular directive. Used for QuickInfo in template.
|
|
|
|
*/
|
|
|
|
export enum QuickInfoKind {
|
|
|
|
COMPONENT = 'component',
|
|
|
|
DIRECTIVE = 'directive',
|
|
|
|
EVENT = 'event',
|
|
|
|
REFERENCE = 'reference',
|
|
|
|
ELEMENT = 'element',
|
|
|
|
VARIABLE = 'variable',
|
|
|
|
PIPE = 'pipe',
|
|
|
|
PROPERTY = 'property',
|
|
|
|
METHOD = 'method',
|
|
|
|
TEMPLATE = 'template',
|
|
|
|
}
|
|
|
|
|
|
|
|
export class QuickInfoBuilder {
|
|
|
|
private readonly typeChecker = this.compiler.getNextProgram().getTypeChecker();
|
|
|
|
constructor(private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler) {}
|
|
|
|
|
|
|
|
get(fileName: string, position: number): ts.QuickInfo|undefined {
|
|
|
|
const templateInfo = getTemplateInfoAtPosition(fileName, position, this.compiler);
|
|
|
|
if (templateInfo === undefined) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
const {template, component} = templateInfo;
|
|
|
|
|
|
|
|
const node = findNodeAtPosition(template, position);
|
|
|
|
if (node === undefined) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const symbol = this.compiler.getTemplateTypeChecker().getSymbolOfNode(node, component);
|
|
|
|
if (symbol === null) {
|
|
|
|
return isDollarAny(node) ? createDollarAnyQuickInfo(node) : undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.getQuickInfoForSymbol(symbol, node);
|
|
|
|
}
|
|
|
|
|
|
|
|
private getQuickInfoForSymbol(symbol: Symbol, node: TmplAstNode|AST): ts.QuickInfo|undefined {
|
|
|
|
switch (symbol.kind) {
|
|
|
|
case SymbolKind.Input:
|
|
|
|
case SymbolKind.Output:
|
|
|
|
return this.getQuickInfoForBindingSymbol(symbol, node);
|
|
|
|
case SymbolKind.Template:
|
|
|
|
return createNgTemplateQuickInfo(node);
|
|
|
|
case SymbolKind.Element:
|
|
|
|
return this.getQuickInfoForElementSymbol(symbol);
|
|
|
|
case SymbolKind.Variable:
|
|
|
|
return this.getQuickInfoForVariableSymbol(symbol, node);
|
|
|
|
case SymbolKind.Reference:
|
|
|
|
return this.getQuickInfoForReferenceSymbol(symbol, node);
|
|
|
|
case SymbolKind.DomBinding:
|
|
|
|
return this.getQuickInfoForDomBinding(node, symbol);
|
|
|
|
case SymbolKind.Directive:
|
|
|
|
return this.getQuickInfoAtShimLocation(symbol.shimLocation, node);
|
|
|
|
case SymbolKind.Expression:
|
|
|
|
return node instanceof BindingPipe ?
|
|
|
|
this.getQuickInfoForPipeSymbol(symbol, node) :
|
|
|
|
this.getQuickInfoAtShimLocation(symbol.shimLocation, node);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private getQuickInfoForBindingSymbol(
|
|
|
|
symbol: InputBindingSymbol|OutputBindingSymbol, node: TmplAstNode|AST): ts.QuickInfo
|
|
|
|
|undefined {
|
|
|
|
if (symbol.bindings.length === 0) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const kind = symbol.kind === SymbolKind.Input ? QuickInfoKind.PROPERTY : QuickInfoKind.EVENT;
|
|
|
|
|
|
|
|
const quickInfo = this.getQuickInfoAtShimLocation(symbol.bindings[0].shimLocation, node);
|
|
|
|
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(
|
|
|
|
templateNode.name, QuickInfoKind.ELEMENT, getTextSpanOfNode(templateNode),
|
|
|
|
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType));
|
|
|
|
}
|
|
|
|
|
|
|
|
private getQuickInfoForVariableSymbol(symbol: VariableSymbol, node: TmplAstNode|AST):
|
|
|
|
ts.QuickInfo {
|
|
|
|
const documentation = this.getDocumentationFromTypeDefAtLocation(symbol.shimLocation);
|
|
|
|
return createQuickInfo(
|
|
|
|
symbol.declaration.name, QuickInfoKind.VARIABLE, getTextSpanOfNode(node),
|
|
|
|
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType), documentation);
|
|
|
|
}
|
|
|
|
|
|
|
|
private getQuickInfoForReferenceSymbol(symbol: ReferenceSymbol, node: TmplAstNode|AST):
|
|
|
|
ts.QuickInfo {
|
|
|
|
const documentation = this.getDocumentationFromTypeDefAtLocation(symbol.shimLocation);
|
|
|
|
return createQuickInfo(
|
|
|
|
symbol.declaration.name, QuickInfoKind.REFERENCE, getTextSpanOfNode(node),
|
|
|
|
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType), documentation);
|
|
|
|
}
|
|
|
|
|
|
|
|
private getQuickInfoForPipeSymbol(symbol: ExpressionSymbol, node: TmplAstNode|AST): ts.QuickInfo
|
|
|
|
|undefined {
|
|
|
|
const quickInfo = this.getQuickInfoAtShimLocation(symbol.shimLocation, node);
|
|
|
|
return quickInfo === undefined ? undefined : updateQuickInfoKind(quickInfo, QuickInfoKind.PIPE);
|
|
|
|
}
|
|
|
|
|
|
|
|
private getQuickInfoForDomBinding(node: TmplAstNode|AST, symbol: DomBindingSymbol) {
|
|
|
|
if (!(node instanceof TmplAstTextAttribute) && !(node instanceof TmplAstBoundAttribute)) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
const directives = getDirectiveMatchesForAttribute(
|
|
|
|
node.name, symbol.host.templateNode, symbol.host.directives);
|
|
|
|
if (directives.size === 0) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.getQuickInfoForDirectiveSymbol(directives.values().next().value, node);
|
|
|
|
}
|
|
|
|
|
|
|
|
private getQuickInfoForDirectiveSymbol(dir: DirectiveSymbol, node: TmplAstNode|AST):
|
|
|
|
ts.QuickInfo {
|
|
|
|
const kind = dir.isComponent ? QuickInfoKind.COMPONENT : QuickInfoKind.DIRECTIVE;
|
|
|
|
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-02 15:13:25 -04:00
|
|
|
this.typeChecker.typeToString(dir.tsType), kind, getTextSpanOfNode(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;
|
|
|
|
}
|
|
|
|
|
|
|
|
private getQuickInfoAtShimLocation(location: ShimLocation, node: TmplAstNode|AST): ts.QuickInfo
|
|
|
|
|undefined {
|
|
|
|
const quickInfo =
|
|
|
|
this.tsLS.getQuickInfoAtPosition(location.shimPath, location.positionInShimFile);
|
|
|
|
if (quickInfo === undefined || quickInfo.displayParts === undefined) {
|
|
|
|
return quickInfo;
|
|
|
|
}
|
|
|
|
|
|
|
|
quickInfo.displayParts = filterAliasImports(quickInfo.displayParts);
|
|
|
|
|
|
|
|
const textSpan = getTextSpanOfNode(node);
|
|
|
|
return {...quickInfo, textSpan};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function updateQuickInfoKind(quickInfo: ts.QuickInfo, kind: QuickInfoKind): ts.QuickInfo {
|
|
|
|
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',
|
|
|
|
QuickInfoKind.METHOD,
|
|
|
|
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',
|
|
|
|
QuickInfoKind.TEMPLATE,
|
|
|
|
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.',
|
|
|
|
}],
|
|
|
|
);
|
|
|
|
}
|