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