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-12-22 12:12:26 -05:00
|
|
|
import {AbsoluteSourceSpan, CssSelector, ParseSourceSpan, SelectorMatcher, TmplAstBoundEvent} from '@angular/compiler';
|
2020-09-28 14:26:07 -04:00
|
|
|
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
2020-10-21 14:01:10 -04:00
|
|
|
import {isExternalResource} from '@angular/compiler-cli/src/ngtsc/metadata';
|
2020-09-29 15:42:20 -04:00
|
|
|
import {DeclarationNode} from '@angular/compiler-cli/src/ngtsc/reflection';
|
2020-09-28 14:26:07 -04:00
|
|
|
import {DirectiveSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
2020-10-30 18:16:39 -04:00
|
|
|
import {Diagnostic as ngDiagnostic, isNgDiagnostic} from '@angular/compiler-cli/src/transformers/api';
|
2020-09-28 14:26:07 -04:00
|
|
|
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
|
|
|
|
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
|
|
|
|
import * as ts from 'typescript';
|
|
|
|
|
2020-10-12 15:51:43 -04:00
|
|
|
import {ALIAS_NAME, SYMBOL_PUNC} from './display_parts';
|
2020-10-21 14:01:10 -04:00
|
|
|
import {findTightestNode, getParentClassDeclaration} from './ts_utils';
|
2020-09-28 14:26:07 -04:00
|
|
|
|
|
|
|
export function getTextSpanOfNode(node: t.Node|e.AST): ts.TextSpan {
|
|
|
|
if (isTemplateNodeWithKeyAndValue(node)) {
|
|
|
|
return toTextSpan(node.keySpan);
|
|
|
|
} else if (
|
|
|
|
node instanceof e.PropertyWrite || node instanceof e.MethodCall ||
|
|
|
|
node instanceof e.BindingPipe || node instanceof e.PropertyRead) {
|
|
|
|
// The `name` part of a `PropertyWrite`, `MethodCall`, and `BindingPipe` does not
|
|
|
|
// have its own AST so there is no way to retrieve a `Symbol` for just the `name` via a specific
|
|
|
|
// node.
|
|
|
|
return toTextSpan(node.nameSpan);
|
|
|
|
} else {
|
|
|
|
return toTextSpan(node.sourceSpan);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function toTextSpan(span: AbsoluteSourceSpan|ParseSourceSpan): ts.TextSpan {
|
|
|
|
let start: number, end: number;
|
|
|
|
if (span instanceof AbsoluteSourceSpan) {
|
|
|
|
start = span.start;
|
|
|
|
end = span.end;
|
|
|
|
} else {
|
|
|
|
start = span.start.offset;
|
|
|
|
end = span.end.offset;
|
|
|
|
}
|
|
|
|
return {start, length: end - start};
|
|
|
|
}
|
|
|
|
|
|
|
|
interface NodeWithKeyAndValue extends t.Node {
|
|
|
|
keySpan: ParseSourceSpan;
|
|
|
|
valueSpan?: ParseSourceSpan;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isTemplateNodeWithKeyAndValue(node: t.Node|e.AST): node is NodeWithKeyAndValue {
|
|
|
|
return isTemplateNode(node) && node.hasOwnProperty('keySpan');
|
|
|
|
}
|
|
|
|
|
2020-12-17 12:50:02 -05:00
|
|
|
export function isWithinKey(position: number, node: NodeWithKeyAndValue): boolean {
|
|
|
|
let {keySpan, valueSpan} = node;
|
|
|
|
if (valueSpan === undefined && node instanceof TmplAstBoundEvent) {
|
|
|
|
valueSpan = node.handlerSpan;
|
|
|
|
}
|
|
|
|
const isWithinKeyValue =
|
|
|
|
isWithin(position, keySpan) || !!(valueSpan && isWithin(position, valueSpan));
|
|
|
|
return isWithinKeyValue;
|
|
|
|
}
|
|
|
|
|
2020-12-22 12:12:26 -05:00
|
|
|
export function isWithinKeyValue(position: number, node: NodeWithKeyAndValue): boolean {
|
|
|
|
let {keySpan, valueSpan} = node;
|
|
|
|
if (valueSpan === undefined && node instanceof TmplAstBoundEvent) {
|
|
|
|
valueSpan = node.handlerSpan;
|
|
|
|
}
|
|
|
|
const isWithinKeyValue =
|
|
|
|
isWithin(position, keySpan) || !!(valueSpan && isWithin(position, valueSpan));
|
|
|
|
return isWithinKeyValue;
|
|
|
|
}
|
|
|
|
|
2020-09-28 14:26:07 -04:00
|
|
|
export function isTemplateNode(node: t.Node|e.AST): node is t.Node {
|
|
|
|
// Template node implements the Node interface so we cannot use instanceof.
|
|
|
|
return node.sourceSpan instanceof ParseSourceSpan;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isExpressionNode(node: t.Node|e.AST): node is e.AST {
|
|
|
|
return node instanceof e.AST;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface TemplateInfo {
|
|
|
|
template: t.Node[];
|
|
|
|
component: ts.ClassDeclaration;
|
|
|
|
}
|
|
|
|
|
2020-10-21 14:01:10 -04:00
|
|
|
function getInlineTemplateInfoAtPosition(
|
|
|
|
sf: ts.SourceFile, position: number, compiler: NgCompiler): TemplateInfo|undefined {
|
|
|
|
const expression = findTightestNode(sf, position);
|
|
|
|
if (expression === undefined) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
const classDecl = getParentClassDeclaration(expression);
|
|
|
|
if (classDecl === undefined) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return `undefined` if the position is not on the template expression or the template resource
|
|
|
|
// is not inline.
|
|
|
|
const resources = compiler.getComponentResources(classDecl);
|
|
|
|
if (resources === null || isExternalResource(resources.template) ||
|
|
|
|
expression !== resources.template.expression) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const template = compiler.getTemplateTypeChecker().getTemplate(classDecl);
|
|
|
|
if (template === null) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {template, component: classDecl};
|
|
|
|
}
|
|
|
|
|
2020-09-28 14:26:07 -04:00
|
|
|
/**
|
|
|
|
* Retrieves the `ts.ClassDeclaration` at a location along with its template nodes.
|
|
|
|
*/
|
|
|
|
export function getTemplateInfoAtPosition(
|
|
|
|
fileName: string, position: number, compiler: NgCompiler): TemplateInfo|undefined {
|
2020-10-09 14:22:03 -04:00
|
|
|
if (isTypeScriptFile(fileName)) {
|
2020-10-21 14:01:10 -04:00
|
|
|
const sf = compiler.getNextProgram().getSourceFile(fileName);
|
|
|
|
if (sf === undefined) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return getInlineTemplateInfoAtPosition(sf, position, compiler);
|
2020-09-28 14:26:07 -04:00
|
|
|
} else {
|
|
|
|
return getFirstComponentForTemplateFile(fileName, compiler);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* First, attempt to sort component declarations by file name.
|
|
|
|
* If the files are the same, sort by start location of the declaration.
|
|
|
|
*/
|
2020-09-29 15:42:20 -04:00
|
|
|
function tsDeclarationSortComparator(a: DeclarationNode, b: DeclarationNode): number {
|
2020-09-28 14:26:07 -04:00
|
|
|
const aFile = a.getSourceFile().fileName;
|
|
|
|
const bFile = b.getSourceFile().fileName;
|
|
|
|
if (aFile < bFile) {
|
|
|
|
return -1;
|
|
|
|
} else if (aFile > bFile) {
|
|
|
|
return 1;
|
|
|
|
} else {
|
|
|
|
return b.getFullStart() - a.getFullStart();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function getFirstComponentForTemplateFile(fileName: string, compiler: NgCompiler): TemplateInfo|
|
|
|
|
undefined {
|
|
|
|
const templateTypeChecker = compiler.getTemplateTypeChecker();
|
|
|
|
const components = compiler.getComponentsWithTemplateFile(fileName);
|
|
|
|
const sortedComponents = Array.from(components).sort(tsDeclarationSortComparator);
|
|
|
|
for (const component of sortedComponents) {
|
|
|
|
if (!ts.isClassDeclaration(component)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const template = templateTypeChecker.getTemplate(component);
|
|
|
|
if (template === null) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
return {template, component};
|
|
|
|
}
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-10-02 16:54:18 -04:00
|
|
|
* Given an attribute node, converts it to string form.
|
|
|
|
*/
|
2020-10-12 15:48:56 -04:00
|
|
|
function toAttributeString(attribute: t.TextAttribute|t.BoundAttribute|t.BoundEvent): string {
|
|
|
|
if (attribute instanceof t.BoundEvent) {
|
|
|
|
return `[${attribute.name}]`;
|
|
|
|
} else {
|
|
|
|
return `[${attribute.name}=${attribute.valueSpan?.toString() ?? ''}]`;
|
|
|
|
}
|
2020-10-02 16:54:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
function getNodeName(node: t.Template|t.Element): string {
|
|
|
|
return node instanceof t.Template ? node.tagName : node.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a template or element node, returns all attributes on the node.
|
|
|
|
*/
|
2020-10-12 15:48:56 -04:00
|
|
|
function getAttributes(node: t.Template|
|
|
|
|
t.Element): Array<t.TextAttribute|t.BoundAttribute|t.BoundEvent> {
|
|
|
|
const attributes: Array<t.TextAttribute|t.BoundAttribute|t.BoundEvent> =
|
|
|
|
[...node.attributes, ...node.inputs, ...node.outputs];
|
2020-10-02 16:54:18 -04:00
|
|
|
if (node instanceof t.Template) {
|
|
|
|
attributes.push(...node.templateAttrs);
|
|
|
|
}
|
|
|
|
return attributes;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given two `Set`s, returns all items in the `left` which do not appear in the `right`.
|
|
|
|
*/
|
|
|
|
function difference<T>(left: Set<T>, right: Set<T>): Set<T> {
|
|
|
|
const result = new Set<T>();
|
|
|
|
for (const dir of left) {
|
|
|
|
if (!right.has(dir)) {
|
|
|
|
result.add(dir);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given an element or template, determines which directives match because the tag is present. For
|
|
|
|
* example, if a directive selector is `div[myAttr]`, this would match div elements but would not if
|
|
|
|
* the selector were just `[myAttr]`. We find which directives are applied because of this tag by
|
|
|
|
* elimination: compare the directive matches with the tag present against the directive matches
|
|
|
|
* without it. The difference would be the directives which match because the tag is present.
|
|
|
|
*
|
|
|
|
* @param element The element or template node that the attribute/tag is part of.
|
|
|
|
* @param directives The list of directives to match against.
|
|
|
|
* @returns The list of directives matching the tag name via the strategy described above.
|
|
|
|
*/
|
|
|
|
// TODO(atscott): Add unit tests for this and the one for attributes
|
|
|
|
export function getDirectiveMatchesForElementTag(
|
|
|
|
element: t.Template|t.Element, directives: DirectiveSymbol[]): Set<DirectiveSymbol> {
|
|
|
|
const attributes = getAttributes(element);
|
|
|
|
const allAttrs = attributes.map(toAttributeString);
|
|
|
|
const allDirectiveMatches =
|
|
|
|
getDirectiveMatchesForSelector(directives, getNodeName(element) + allAttrs.join(''));
|
|
|
|
const matchesWithoutElement = getDirectiveMatchesForSelector(directives, allAttrs.join(''));
|
|
|
|
return difference(allDirectiveMatches, matchesWithoutElement);
|
|
|
|
}
|
|
|
|
|
2020-12-03 21:08:00 -05:00
|
|
|
|
|
|
|
export function makeElementSelector(element: t.Element|t.Template): string {
|
|
|
|
const attributes = getAttributes(element);
|
|
|
|
const allAttrs = attributes.map(toAttributeString);
|
|
|
|
return getNodeName(element) + allAttrs.join('');
|
|
|
|
}
|
|
|
|
|
2020-10-02 16:54:18 -04:00
|
|
|
/**
|
|
|
|
* Given an attribute name, determines which directives match because the attribute is present. We
|
|
|
|
* find which directives are applied because of this attribute by elimination: compare the directive
|
|
|
|
* matches with the attribute present against the directive matches without it. The difference would
|
|
|
|
* be the directives which match because the attribute is present.
|
2020-09-28 14:26:07 -04:00
|
|
|
*
|
2020-10-02 16:54:18 -04:00
|
|
|
* @param name The name of the attribute
|
|
|
|
* @param hostNode The node which the attribute appears on
|
2020-09-28 14:26:07 -04:00
|
|
|
* @param directives The list of directives to match against.
|
2020-10-02 16:54:18 -04:00
|
|
|
* @returns The list of directives matching the tag name via the strategy described above.
|
2020-09-28 14:26:07 -04:00
|
|
|
*/
|
|
|
|
export function getDirectiveMatchesForAttribute(
|
2020-10-02 16:54:18 -04:00
|
|
|
name: string, hostNode: t.Template|t.Element,
|
2020-09-28 14:26:07 -04:00
|
|
|
directives: DirectiveSymbol[]): Set<DirectiveSymbol> {
|
2020-10-02 16:54:18 -04:00
|
|
|
const attributes = getAttributes(hostNode);
|
|
|
|
const allAttrs = attributes.map(toAttributeString);
|
|
|
|
const allDirectiveMatches =
|
|
|
|
getDirectiveMatchesForSelector(directives, getNodeName(hostNode) + allAttrs.join(''));
|
|
|
|
const attrsExcludingName = attributes.filter(a => a.name !== name).map(toAttributeString);
|
2020-10-12 15:48:56 -04:00
|
|
|
const matchesWithoutAttr = getDirectiveMatchesForSelector(
|
|
|
|
directives, getNodeName(hostNode) + attrsExcludingName.join(''));
|
2020-10-02 16:54:18 -04:00
|
|
|
return difference(allDirectiveMatches, matchesWithoutAttr);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a list of directives and a text to use as a selector, returns the directives which match
|
|
|
|
* for the selector.
|
|
|
|
*/
|
|
|
|
function getDirectiveMatchesForSelector(
|
|
|
|
directives: DirectiveSymbol[], selector: string): Set<DirectiveSymbol> {
|
|
|
|
const selectors = CssSelector.parse(selector);
|
|
|
|
if (selectors.length === 0) {
|
|
|
|
return new Set();
|
2020-09-28 14:26:07 -04:00
|
|
|
}
|
2020-10-02 16:54:18 -04:00
|
|
|
return new Set(directives.filter((dir: DirectiveSymbol) => {
|
|
|
|
if (dir.selector === null) {
|
|
|
|
return false;
|
|
|
|
}
|
2020-09-28 14:26:07 -04:00
|
|
|
|
2020-10-02 16:54:18 -04:00
|
|
|
const matcher = new SelectorMatcher();
|
|
|
|
matcher.addSelectables(CssSelector.parse(dir.selector));
|
2020-09-28 14:26:07 -04:00
|
|
|
|
2020-10-02 16:54:18 -04:00
|
|
|
return selectors.some(selector => matcher.match(selector, null));
|
|
|
|
}));
|
2020-09-28 14:26:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a new `ts.SymbolDisplayPart` array which has the alias imports from the tcb filtered
|
|
|
|
* out, i.e. `i0.NgForOf`.
|
|
|
|
*/
|
|
|
|
export function filterAliasImports(displayParts: ts.SymbolDisplayPart[]): ts.SymbolDisplayPart[] {
|
|
|
|
const tcbAliasImportRegex = /i\d+/;
|
|
|
|
function isImportAlias(part: {kind: string, text: string}) {
|
|
|
|
return part.kind === ALIAS_NAME && tcbAliasImportRegex.test(part.text);
|
|
|
|
}
|
|
|
|
function isDotPunctuation(part: {kind: string, text: string}) {
|
|
|
|
return part.kind === SYMBOL_PUNC && part.text === '.';
|
|
|
|
}
|
|
|
|
|
|
|
|
return displayParts.filter((part, i) => {
|
|
|
|
const previousPart = displayParts[i - 1];
|
|
|
|
const nextPart = displayParts[i + 1];
|
|
|
|
|
|
|
|
const aliasNameFollowedByDot =
|
|
|
|
isImportAlias(part) && nextPart !== undefined && isDotPunctuation(nextPart);
|
|
|
|
const dotPrecededByAlias =
|
|
|
|
isDotPunctuation(part) && previousPart !== undefined && isImportAlias(previousPart);
|
|
|
|
|
|
|
|
return !aliasNameFollowedByDot && !dotPrecededByAlias;
|
|
|
|
});
|
|
|
|
}
|
2020-09-30 11:47:36 -04:00
|
|
|
|
|
|
|
export function isDollarEvent(n: t.Node|e.AST): n is e.PropertyRead {
|
|
|
|
return n instanceof e.PropertyRead && n.name === '$event' &&
|
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
|
|
|
n.receiver instanceof e.ImplicitReceiver && !(n.receiver instanceof e.ThisReceiver);
|
2020-09-30 11:47:36 -04:00
|
|
|
}
|
2020-10-02 16:54:18 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a new array formed by applying a given callback function to each element of the array,
|
|
|
|
* and then flattening the result by one level.
|
|
|
|
*/
|
|
|
|
export function flatMap<T, R>(items: T[]|readonly T[], f: (item: T) => R[] | readonly R[]): R[] {
|
|
|
|
const results: R[] = [];
|
|
|
|
for (const x of items) {
|
|
|
|
results.push(...f(x));
|
|
|
|
}
|
|
|
|
return results;
|
|
|
|
}
|
2020-10-09 14:22:03 -04:00
|
|
|
|
|
|
|
export function isTypeScriptFile(fileName: string): boolean {
|
|
|
|
return fileName.endsWith('.ts');
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isExternalTemplate(fileName: string): boolean {
|
|
|
|
return !isTypeScriptFile(fileName);
|
|
|
|
}
|
2020-11-19 16:31:34 -05:00
|
|
|
|
|
|
|
export function isWithin(position: number, span: AbsoluteSourceSpan|ParseSourceSpan): boolean {
|
|
|
|
let start: number, end: number;
|
|
|
|
if (span instanceof ParseSourceSpan) {
|
|
|
|
start = span.start.offset;
|
|
|
|
end = span.end.offset;
|
|
|
|
} else {
|
|
|
|
start = span.start;
|
|
|
|
end = span.end;
|
|
|
|
}
|
|
|
|
// Note both start and end are inclusive because we want to match conditions
|
|
|
|
// like ¦start and end¦ where ¦ is the cursor.
|
|
|
|
return start <= position && position <= end;
|
|
|
|
}
|