angular-docs-cn/packages/language-service/src/locate_symbol.ts

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();
}
}