2020-09-30 11:47:36 -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-10-12 15:48:56 -04:00
|
|
|
import {AST, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstTemplate, TmplAstTextAttribute} from '@angular/compiler';
|
2020-09-30 11:47:36 -04:00
|
|
|
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
2020-10-09 13:58:16 -04:00
|
|
|
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ShimLocation, Symbol, SymbolKind, TemplateSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
2020-09-30 11:47:36 -04:00
|
|
|
import * as ts from 'typescript';
|
|
|
|
|
2020-10-12 15:48:56 -04:00
|
|
|
import {getPathToNodeAtPosition} from './hybrid_visitor';
|
2020-10-02 16:54:18 -04:00
|
|
|
import {flatMap, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, getTextSpanOfNode, isDollarEvent, TemplateInfo, toTextSpan} from './utils';
|
|
|
|
|
|
|
|
interface DefinitionMeta {
|
|
|
|
node: AST|TmplAstNode;
|
2020-10-12 15:48:56 -04:00
|
|
|
path: Array<AST|TmplAstNode>;
|
2020-10-02 16:54:18 -04:00
|
|
|
symbol: Symbol;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface HasShimLocation {
|
|
|
|
shimLocation: ShimLocation;
|
|
|
|
}
|
2020-09-30 11:47:36 -04:00
|
|
|
|
|
|
|
export class DefinitionBuilder {
|
|
|
|
constructor(private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler) {}
|
|
|
|
|
|
|
|
getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan
|
|
|
|
|undefined {
|
|
|
|
const templateInfo = getTemplateInfoAtPosition(fileName, position, this.compiler);
|
|
|
|
if (templateInfo === undefined) {
|
2020-10-02 16:54:18 -04:00
|
|
|
return;
|
2020-09-30 11:47:36 -04:00
|
|
|
}
|
2020-10-02 16:54:18 -04:00
|
|
|
const definitionMeta = this.getDefinitionMetaAtPosition(templateInfo, position);
|
2020-09-30 11:47:36 -04:00
|
|
|
// The `$event` of event handlers would point to the $event parameter in the shim file, as in
|
|
|
|
// `_outputHelper(_t3["x"]).subscribe(function ($event): any { $event }) ;`
|
|
|
|
// If we wanted to return something for this, it would be more appropriate for something like
|
|
|
|
// `getTypeDefinition`.
|
2020-10-02 16:54:18 -04:00
|
|
|
if (definitionMeta === undefined || isDollarEvent(definitionMeta.node)) {
|
2020-09-30 11:47:36 -04:00
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2020-10-12 15:48:56 -04:00
|
|
|
const definitions = this.getDefinitionsForSymbol({...definitionMeta, ...templateInfo});
|
2020-10-02 16:54:18 -04:00
|
|
|
return {definitions, textSpan: getTextSpanOfNode(definitionMeta.node)};
|
2020-09-30 11:47:36 -04:00
|
|
|
}
|
|
|
|
|
2020-10-12 15:48:56 -04:00
|
|
|
private getDefinitionsForSymbol({symbol, node, path, component}: DefinitionMeta&
|
|
|
|
TemplateInfo): readonly ts.DefinitionInfo[]|undefined {
|
2020-09-30 11:47:36 -04:00
|
|
|
switch (symbol.kind) {
|
|
|
|
case SymbolKind.Directive:
|
|
|
|
case SymbolKind.Element:
|
|
|
|
case SymbolKind.Template:
|
|
|
|
case SymbolKind.DomBinding:
|
2020-10-09 13:58:16 -04:00
|
|
|
// Though it is generally more appropriate for the above symbol definitions to be
|
|
|
|
// associated with "type definitions" since the location in the template is the
|
|
|
|
// actual definition location, the better user experience would be to allow
|
|
|
|
// LS users to "go to definition" on an item in the template that maps to a class and be
|
|
|
|
// taken to the directive or HTML class.
|
|
|
|
return this.getTypeDefinitionsForTemplateInstance(symbol, node);
|
2020-09-30 11:47:36 -04:00
|
|
|
case SymbolKind.Output:
|
2020-10-12 15:48:56 -04:00
|
|
|
case SymbolKind.Input: {
|
|
|
|
const bindingDefs = this.getDefinitionsForSymbols(...symbol.bindings);
|
|
|
|
// Also attempt to get directive matches for the input name. If there is a directive that
|
|
|
|
// has the input name as part of the selector, we want to return that as well.
|
|
|
|
const directiveDefs = this.getDirectiveTypeDefsForBindingNode(node, path, component);
|
|
|
|
return [...bindingDefs, ...directiveDefs];
|
|
|
|
}
|
2020-09-30 11:47:36 -04:00
|
|
|
case SymbolKind.Variable:
|
|
|
|
case SymbolKind.Reference: {
|
|
|
|
const definitions: ts.DefinitionInfo[] = [];
|
|
|
|
if (symbol.declaration !== node) {
|
|
|
|
definitions.push({
|
|
|
|
name: symbol.declaration.name,
|
|
|
|
containerName: '',
|
|
|
|
containerKind: ts.ScriptElementKind.unknown,
|
|
|
|
kind: ts.ScriptElementKind.variableElement,
|
|
|
|
textSpan: getTextSpanOfNode(symbol.declaration),
|
|
|
|
contextSpan: toTextSpan(symbol.declaration.sourceSpan),
|
|
|
|
fileName: symbol.declaration.sourceSpan.start.file.url,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (symbol.kind === SymbolKind.Variable) {
|
2020-10-02 16:54:18 -04:00
|
|
|
definitions.push(...this.getDefinitionsForSymbols(symbol));
|
2020-09-30 11:47:36 -04:00
|
|
|
}
|
|
|
|
return definitions;
|
|
|
|
}
|
|
|
|
case SymbolKind.Expression: {
|
2020-10-02 16:54:18 -04:00
|
|
|
return this.getDefinitionsForSymbols(symbol);
|
2020-09-30 11:47:36 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-02 16:54:18 -04:00
|
|
|
private getDefinitionsForSymbols(...symbols: HasShimLocation[]): ts.DefinitionInfo[] {
|
|
|
|
return flatMap(symbols, ({shimLocation}) => {
|
|
|
|
const {shimPath, positionInShimFile} = shimLocation;
|
|
|
|
return this.tsLS.getDefinitionAtPosition(shimPath, positionInShimFile) ?? [];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
getTypeDefinitionsAtPosition(fileName: string, position: number):
|
|
|
|
readonly ts.DefinitionInfo[]|undefined {
|
|
|
|
const templateInfo = getTemplateInfoAtPosition(fileName, position, this.compiler);
|
|
|
|
if (templateInfo === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const definitionMeta = this.getDefinitionMetaAtPosition(templateInfo, position);
|
|
|
|
if (definitionMeta === undefined) {
|
|
|
|
return undefined;
|
2020-09-30 11:47:36 -04:00
|
|
|
}
|
2020-10-02 16:54:18 -04:00
|
|
|
|
|
|
|
const {symbol, node} = definitionMeta;
|
2020-10-09 13:58:16 -04:00
|
|
|
switch (symbol.kind) {
|
|
|
|
case SymbolKind.Directive:
|
|
|
|
case SymbolKind.DomBinding:
|
|
|
|
case SymbolKind.Element:
|
|
|
|
case SymbolKind.Template:
|
|
|
|
return this.getTypeDefinitionsForTemplateInstance(symbol, node);
|
|
|
|
case SymbolKind.Output:
|
2020-10-12 15:48:56 -04:00
|
|
|
case SymbolKind.Input: {
|
|
|
|
const bindingDefs = this.getTypeDefinitionsForSymbols(...symbol.bindings);
|
|
|
|
// Also attempt to get directive matches for the input name. If there is a directive that
|
|
|
|
// has the input name as part of the selector, we want to return that as well.
|
|
|
|
const directiveDefs = this.getDirectiveTypeDefsForBindingNode(
|
|
|
|
node, definitionMeta.path, templateInfo.component);
|
|
|
|
return [...bindingDefs, ...directiveDefs];
|
|
|
|
}
|
2020-10-09 13:58:16 -04:00
|
|
|
case SymbolKind.Reference:
|
|
|
|
case SymbolKind.Expression:
|
|
|
|
case SymbolKind.Variable:
|
|
|
|
return this.getTypeDefinitionsForSymbols(symbol);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private getTypeDefinitionsForTemplateInstance(
|
|
|
|
symbol: TemplateSymbol|ElementSymbol|DomBindingSymbol|DirectiveSymbol,
|
|
|
|
node: AST|TmplAstNode): ts.DefinitionInfo[] {
|
2020-10-02 16:54:18 -04:00
|
|
|
switch (symbol.kind) {
|
|
|
|
case SymbolKind.Template: {
|
|
|
|
const matches = getDirectiveMatchesForElementTag(symbol.templateNode, symbol.directives);
|
|
|
|
return this.getTypeDefinitionsForSymbols(...matches);
|
|
|
|
}
|
|
|
|
case SymbolKind.Element: {
|
2020-10-09 13:58:16 -04:00
|
|
|
const matches = getDirectiveMatchesForElementTag(symbol.templateNode, symbol.directives);
|
2020-10-02 16:54:18 -04:00
|
|
|
// If one of the directive matches is a component, we should not include the native element
|
|
|
|
// in the results because it is replaced by the component.
|
|
|
|
return Array.from(matches).some(dir => dir.isComponent) ?
|
|
|
|
this.getTypeDefinitionsForSymbols(...matches) :
|
|
|
|
this.getTypeDefinitionsForSymbols(...matches, symbol);
|
|
|
|
}
|
|
|
|
case SymbolKind.DomBinding: {
|
|
|
|
if (!(node instanceof TmplAstTextAttribute)) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
const dirs = getDirectiveMatchesForAttribute(
|
|
|
|
node.name, symbol.host.templateNode, symbol.host.directives);
|
|
|
|
return this.getTypeDefinitionsForSymbols(...dirs);
|
|
|
|
}
|
|
|
|
case SymbolKind.Directive:
|
|
|
|
return this.getTypeDefinitionsForSymbols(symbol);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-12 15:48:56 -04:00
|
|
|
private getDirectiveTypeDefsForBindingNode(
|
|
|
|
node: TmplAstNode|AST, pathToNode: Array<TmplAstNode|AST>, component: ts.ClassDeclaration) {
|
|
|
|
if (!(node instanceof TmplAstBoundAttribute) && !(node instanceof TmplAstTextAttribute) &&
|
|
|
|
!(node instanceof TmplAstBoundEvent)) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
const parent = pathToNode[pathToNode.length - 2];
|
|
|
|
if (!(parent instanceof TmplAstTemplate || parent instanceof TmplAstElement)) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
const templateOrElementSymbol =
|
|
|
|
this.compiler.getTemplateTypeChecker().getSymbolOfNode(parent, component);
|
|
|
|
if (templateOrElementSymbol === null ||
|
|
|
|
(templateOrElementSymbol.kind !== SymbolKind.Template &&
|
|
|
|
templateOrElementSymbol.kind !== SymbolKind.Element)) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
const dirs =
|
|
|
|
getDirectiveMatchesForAttribute(node.name, parent, templateOrElementSymbol.directives);
|
|
|
|
return this.getTypeDefinitionsForSymbols(...dirs);
|
|
|
|
}
|
|
|
|
|
2020-10-02 16:54:18 -04:00
|
|
|
private getTypeDefinitionsForSymbols(...symbols: HasShimLocation[]): ts.DefinitionInfo[] {
|
|
|
|
return flatMap(symbols, ({shimLocation}) => {
|
|
|
|
const {shimPath, positionInShimFile} = shimLocation;
|
|
|
|
return this.tsLS.getTypeDefinitionAtPosition(shimPath, positionInShimFile) ?? [];
|
|
|
|
});
|
2020-09-30 11:47:36 -04:00
|
|
|
}
|
|
|
|
|
2020-10-02 16:54:18 -04:00
|
|
|
private getDefinitionMetaAtPosition({template, component}: TemplateInfo, position: number):
|
|
|
|
DefinitionMeta|undefined {
|
2020-10-12 15:48:56 -04:00
|
|
|
const path = getPathToNodeAtPosition(template, position);
|
|
|
|
if (path === undefined) {
|
2020-10-02 16:54:18 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-10-12 15:48:56 -04:00
|
|
|
const node = path[path.length - 1];
|
2020-10-02 16:54:18 -04:00
|
|
|
const symbol = this.compiler.getTemplateTypeChecker().getSymbolOfNode(node, component);
|
|
|
|
if (symbol === null) {
|
|
|
|
return;
|
|
|
|
}
|
2020-10-12 15:48:56 -04:00
|
|
|
return {node, path, symbol};
|
2020-09-30 11:47:36 -04:00
|
|
|
}
|
|
|
|
}
|