370 lines
13 KiB
TypeScript
370 lines
13 KiB
TypeScript
/**
|
|
* @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
|
|
*/
|
|
|
|
import {AST, Attribute, BoundDirectivePropertyAst, CssSelector, DirectiveAst, ElementAst, EmbeddedTemplateAst, RecursiveTemplateAstVisitor, SelectorMatcher, StaticSymbol, TemplateAst, TemplateAstPath, templateVisitAll, tokenReference, VariableBinding} from '@angular/compiler';
|
|
import * as tss from 'typescript/lib/tsserverlibrary';
|
|
|
|
import {getExpressionScope} from './expression_diagnostics';
|
|
import {getExpressionSymbol} from './expressions';
|
|
import {AstResult, Definition, DirectiveKind, Span, Symbol, SymbolInfo} from './types';
|
|
import {diagnosticInfoFromTemplateInfo, findOutputBinding, findTemplateAstAt, getPathToNodeAtPosition, inSpan, invertMap, isNarrower, offsetSpan, spanOf} from './utils';
|
|
|
|
/**
|
|
* Traverses a template AST and locates symbol(s) at a specified position.
|
|
* @param info template AST information set
|
|
* @param position location to locate symbols at
|
|
*/
|
|
export function locateSymbols(info: AstResult, position: number): SymbolInfo[] {
|
|
const templatePosition = position - info.template.span.start;
|
|
// TODO: update `findTemplateAstAt` to use absolute positions.
|
|
const path = findTemplateAstAt(info.templateAst, templatePosition);
|
|
const attribute = findAttribute(info, position);
|
|
|
|
if (!path.tail) return [];
|
|
|
|
const narrowest = spanOf(path.tail);
|
|
const toVisit: TemplateAst[] = [];
|
|
for (let node: TemplateAst|undefined = path.tail;
|
|
node && isNarrower(spanOf(node.sourceSpan), narrowest); node = path.parentOf(node)) {
|
|
toVisit.push(node);
|
|
}
|
|
|
|
// For the structural directive, only care about the last template AST.
|
|
if (attribute?.name.startsWith('*')) {
|
|
toVisit.splice(0, toVisit.length - 1);
|
|
}
|
|
|
|
return toVisit.map(ast => locateSymbol(ast, path, info))
|
|
.filter((sym): sym is SymbolInfo => sym !== undefined);
|
|
}
|
|
|
|
/**
|
|
* Visits a template node and locates the symbol in that node at a path position.
|
|
* @param ast template AST node to visit
|
|
* @param path non-empty set of narrowing AST nodes at a position
|
|
* @param info template AST information set
|
|
*/
|
|
function locateSymbol(ast: TemplateAst, path: TemplateAstPath, info: AstResult): SymbolInfo|
|
|
undefined {
|
|
const templatePosition = path.position;
|
|
const position = templatePosition + info.template.span.start;
|
|
let symbol: Symbol|undefined;
|
|
let span: Span|undefined;
|
|
let staticSymbol: StaticSymbol|undefined;
|
|
const attributeValueSymbol = (ast: AST): boolean => {
|
|
const attribute = findAttribute(info, position);
|
|
if (attribute) {
|
|
if (inSpan(templatePosition, spanOf(attribute.valueSpan))) {
|
|
let result: {symbol: Symbol, span: Span}|undefined;
|
|
if (attribute.name.startsWith('*')) {
|
|
result = getSymbolInMicrosyntax(info, path, attribute);
|
|
} else {
|
|
const dinfo = diagnosticInfoFromTemplateInfo(info);
|
|
const scope = getExpressionScope(dinfo, path);
|
|
result = getExpressionSymbol(scope, ast, templatePosition, info.template);
|
|
}
|
|
if (result) {
|
|
symbol = result.symbol;
|
|
span = offsetSpan(result.span, attribute.valueSpan!.start.offset);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
ast.visit(
|
|
{
|
|
visitNgContent(_ast) {},
|
|
visitEmbeddedTemplate(_ast) {},
|
|
visitElement(ast) {
|
|
const component = ast.directives.find(d => d.directive.isComponent);
|
|
if (component) {
|
|
// Need to cast because 'reference' is typed as any
|
|
staticSymbol = component.directive.type.reference as StaticSymbol;
|
|
symbol = info.template.query.getTypeSymbol(staticSymbol);
|
|
symbol = symbol && new OverrideKindSymbol(symbol, DirectiveKind.COMPONENT);
|
|
span = spanOf(ast);
|
|
} else {
|
|
// Find a directive that matches the element name
|
|
const directive = ast.directives.find(
|
|
d => d.directive.selector != null && d.directive.selector.indexOf(ast.name) >= 0);
|
|
if (directive) {
|
|
// Need to cast because 'reference' is typed as any
|
|
staticSymbol = directive.directive.type.reference as StaticSymbol;
|
|
symbol = info.template.query.getTypeSymbol(staticSymbol);
|
|
symbol = symbol && new OverrideKindSymbol(symbol, DirectiveKind.DIRECTIVE);
|
|
span = spanOf(ast);
|
|
}
|
|
}
|
|
},
|
|
visitReference(ast) {
|
|
symbol = ast.value && info.template.query.getTypeSymbol(tokenReference(ast.value));
|
|
span = spanOf(ast);
|
|
},
|
|
visitVariable(_ast) {},
|
|
visitEvent(ast) {
|
|
if (!attributeValueSymbol(ast.handler)) {
|
|
symbol = findOutputBinding(ast, path, info.template.query);
|
|
symbol = symbol && new OverrideKindSymbol(symbol, DirectiveKind.EVENT);
|
|
span = spanOf(ast);
|
|
}
|
|
},
|
|
visitElementProperty(ast) {
|
|
attributeValueSymbol(ast.value);
|
|
},
|
|
visitAttr(ast) {
|
|
const element = path.first(ElementAst);
|
|
if (!element) return;
|
|
// Create a mapping of all directives applied to the element from their selectors.
|
|
const matcher = new SelectorMatcher<DirectiveAst>();
|
|
for (const dir of element.directives) {
|
|
if (!dir.directive.selector) continue;
|
|
matcher.addSelectables(CssSelector.parse(dir.directive.selector), dir);
|
|
}
|
|
|
|
// See if this attribute matches the selector of any directive on the element.
|
|
const attributeSelector = `[${ast.name}=${ast.value}]`;
|
|
const parsedAttribute = CssSelector.parse(attributeSelector);
|
|
if (!parsedAttribute.length) return;
|
|
matcher.match(parsedAttribute[0], (_, {directive}) => {
|
|
// Need to cast because 'reference' is typed as any
|
|
staticSymbol = directive.type.reference as StaticSymbol;
|
|
symbol = info.template.query.getTypeSymbol(staticSymbol);
|
|
symbol = symbol && new OverrideKindSymbol(symbol, DirectiveKind.DIRECTIVE);
|
|
span = spanOf(ast);
|
|
});
|
|
},
|
|
visitBoundText(ast) {
|
|
const expressionPosition = templatePosition - ast.sourceSpan.start.offset;
|
|
if (inSpan(expressionPosition, ast.value.span)) {
|
|
const dinfo = diagnosticInfoFromTemplateInfo(info);
|
|
const scope = getExpressionScope(dinfo, path);
|
|
const result = getExpressionSymbol(scope, ast.value, templatePosition, info.template);
|
|
if (result) {
|
|
symbol = result.symbol;
|
|
span = offsetSpan(result.span, ast.sourceSpan.start.offset);
|
|
}
|
|
}
|
|
},
|
|
visitText(_ast) {},
|
|
visitDirective(ast) {
|
|
// Need to cast because 'reference' is typed as any
|
|
staticSymbol = ast.directive.type.reference as StaticSymbol;
|
|
symbol = info.template.query.getTypeSymbol(staticSymbol);
|
|
span = spanOf(ast);
|
|
},
|
|
visitDirectiveProperty(ast) {
|
|
if (!attributeValueSymbol(ast.value)) {
|
|
const directive = findParentOfBinding(info.templateAst, ast, templatePosition);
|
|
const attribute = findAttribute(info, position);
|
|
if (directive && attribute) {
|
|
if (attribute.name.startsWith('*')) {
|
|
const compileTypeSummary = directive.directive;
|
|
symbol = info.template.query.getTypeSymbol(compileTypeSummary.type.reference);
|
|
symbol = symbol && new OverrideKindSymbol(symbol, DirectiveKind.DIRECTIVE);
|
|
// Use 'attribute.sourceSpan' instead of the directive's,
|
|
// because the span of the directive is the whole opening tag of an element.
|
|
span = spanOf(attribute.sourceSpan);
|
|
} else {
|
|
symbol = findInputBinding(info, ast.templateName, directive);
|
|
span = spanOf(ast);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
null);
|
|
if (symbol && span) {
|
|
const {start, end} = offsetSpan(span, info.template.span.start);
|
|
return {
|
|
symbol,
|
|
span: tss.createTextSpanFromBounds(start, end),
|
|
staticSymbol,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Get the symbol in microsyntax at template position.
|
|
function getSymbolInMicrosyntax(info: AstResult, path: TemplateAstPath, attribute: Attribute):
|
|
{symbol: Symbol, span: Span}|undefined {
|
|
if (!attribute.valueSpan) {
|
|
return;
|
|
}
|
|
const absValueOffset = attribute.valueSpan.start.offset;
|
|
let result: {symbol: Symbol, span: Span}|undefined;
|
|
const {templateBindings} = info.expressionParser.parseTemplateBindings(
|
|
attribute.name, attribute.value, attribute.sourceSpan.toString(),
|
|
attribute.sourceSpan.start.offset, attribute.valueSpan.start.offset);
|
|
|
|
// Find the symbol that contains the position.
|
|
for (const tb of templateBindings) {
|
|
if (tb instanceof VariableBinding) {
|
|
// TODO(kyliau): if binding is variable we should still look for the value
|
|
// of the key. For example, "let i=index" => "index" should point to
|
|
// NgForOfContext.index
|
|
continue;
|
|
}
|
|
if (inSpan(path.position, tb.value?.ast.sourceSpan)) {
|
|
const dinfo = diagnosticInfoFromTemplateInfo(info);
|
|
const scope = getExpressionScope(dinfo, path);
|
|
result = getExpressionSymbol(scope, tb.value!, path.position, info.template);
|
|
} else if (inSpan(path.position, tb.sourceSpan)) {
|
|
const template = path.first(EmbeddedTemplateAst);
|
|
if (template) {
|
|
// One element can only have one template binding.
|
|
const directiveAst = template.directives[0];
|
|
if (directiveAst) {
|
|
const symbol = findInputBinding(info, tb.key.source.substring(1), directiveAst);
|
|
if (symbol) {
|
|
result = {
|
|
symbol,
|
|
// the span here has to be relative to the start of the template
|
|
// value so deduct the absolute offset.
|
|
// TODO(kyliau): Use absolute source span throughout completions.
|
|
span: offsetSpan(tb.key.span, -absValueOffset),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function findAttribute(info: AstResult, position: number): Attribute|undefined {
|
|
const templatePosition = position - info.template.span.start;
|
|
const path = getPathToNodeAtPosition(info.htmlAst, templatePosition);
|
|
return path.first(Attribute);
|
|
}
|
|
|
|
// TODO: remove this function after the path includes 'DirectiveAst'.
|
|
// Find the directive that corresponds to the specified 'binding'
|
|
// at the specified 'position' in the 'ast'.
|
|
function findParentOfBinding(
|
|
ast: TemplateAst[], binding: BoundDirectivePropertyAst, position: number): DirectiveAst|
|
|
undefined {
|
|
let res: DirectiveAst|undefined;
|
|
const visitor = new class extends RecursiveTemplateAstVisitor {
|
|
visit(ast: TemplateAst): any {
|
|
const span = spanOf(ast);
|
|
if (!inSpan(position, span)) {
|
|
// Returning a value here will result in the children being skipped.
|
|
return true;
|
|
}
|
|
}
|
|
|
|
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
|
|
return this.visitChildren(context, visit => {
|
|
visit(ast.directives);
|
|
visit(ast.children);
|
|
});
|
|
}
|
|
|
|
visitElement(ast: ElementAst, context: any): any {
|
|
return this.visitChildren(context, visit => {
|
|
visit(ast.directives);
|
|
visit(ast.children);
|
|
});
|
|
}
|
|
|
|
visitDirective(ast: DirectiveAst) {
|
|
const result = this.visitChildren(ast, visit => {
|
|
visit(ast.inputs);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: DirectiveAst) {
|
|
if (ast === binding) {
|
|
res = context;
|
|
}
|
|
}
|
|
};
|
|
templateVisitAll(visitor, ast);
|
|
return res;
|
|
}
|
|
|
|
// Find the symbol of input binding in 'directiveAst' by 'name'.
|
|
function findInputBinding(info: AstResult, name: string, directiveAst: DirectiveAst): Symbol|
|
|
undefined {
|
|
const invertedInput = invertMap(directiveAst.directive.inputs);
|
|
const fieldName = invertedInput[name];
|
|
if (fieldName) {
|
|
const classSymbol = info.template.query.getTypeSymbol(directiveAst.directive.type.reference);
|
|
if (classSymbol) {
|
|
return classSymbol.members().get(fieldName);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wrap a symbol and change its kind to component.
|
|
*/
|
|
class OverrideKindSymbol implements Symbol {
|
|
public readonly kind: DirectiveKind;
|
|
constructor(private sym: Symbol, kindOverride: DirectiveKind) {
|
|
this.kind = kindOverride;
|
|
}
|
|
|
|
get name(): string {
|
|
return this.sym.name;
|
|
}
|
|
|
|
get language(): string {
|
|
return this.sym.language;
|
|
}
|
|
|
|
get type(): Symbol|undefined {
|
|
return this.sym.type;
|
|
}
|
|
|
|
get container(): Symbol|undefined {
|
|
return this.sym.container;
|
|
}
|
|
|
|
get public(): boolean {
|
|
return this.sym.public;
|
|
}
|
|
|
|
get callable(): boolean {
|
|
return this.sym.callable;
|
|
}
|
|
|
|
get nullable(): boolean {
|
|
return this.sym.nullable;
|
|
}
|
|
|
|
get definition(): Definition {
|
|
return this.sym.definition;
|
|
}
|
|
|
|
get documentation(): ts.SymbolDisplayPart[] {
|
|
return this.sym.documentation;
|
|
}
|
|
|
|
members() {
|
|
return this.sym.members();
|
|
}
|
|
|
|
signatures() {
|
|
return this.sym.signatures();
|
|
}
|
|
|
|
selectSignature(types: Symbol[]) {
|
|
return this.sym.selectSignature(types);
|
|
}
|
|
|
|
indexed(argument: Symbol) {
|
|
return this.sym.indexed(argument);
|
|
}
|
|
|
|
typeArguments(): Symbol[]|undefined {
|
|
return this.sym.typeArguments();
|
|
}
|
|
}
|