245 lines
9.7 KiB
TypeScript
245 lines
9.7 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, ImplicitReceiver, MethodCall, ThisReceiver, TmplAstBoundAttribute, TmplAstNode, TmplAstTextAttribute} from '@angular/compiler';
|
|
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
|
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, InputBindingSymbol, OutputBindingSymbol, PipeSymbol, ReferenceSymbol, ShimLocation, Symbol, SymbolKind, VariableSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
|
import * as ts from 'typescript';
|
|
|
|
import {createDisplayParts, DisplayInfoKind, SYMBOL_PUNC, SYMBOL_SPACE, SYMBOL_TEXT, unsafeCastDisplayInfoKindToScriptElementKind} from './display_parts';
|
|
import {filterAliasImports, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTextSpanOfNode} from './utils';
|
|
|
|
export class QuickInfoBuilder {
|
|
private readonly typeChecker = this.compiler.getCurrentProgram().getTypeChecker();
|
|
|
|
constructor(
|
|
private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler,
|
|
private readonly component: ts.ClassDeclaration, private node: TmplAstNode|AST) {}
|
|
|
|
get(): ts.QuickInfo|undefined {
|
|
const symbol =
|
|
this.compiler.getTemplateTypeChecker().getSymbolOfNode(this.node, this.component);
|
|
if (symbol === null) {
|
|
return isDollarAny(this.node) ? createDollarAnyQuickInfo(this.node) : undefined;
|
|
}
|
|
|
|
return this.getQuickInfoForSymbol(symbol);
|
|
}
|
|
|
|
private getQuickInfoForSymbol(symbol: Symbol): ts.QuickInfo|undefined {
|
|
switch (symbol.kind) {
|
|
case SymbolKind.Input:
|
|
case SymbolKind.Output:
|
|
return this.getQuickInfoForBindingSymbol(symbol);
|
|
case SymbolKind.Template:
|
|
return createNgTemplateQuickInfo(this.node);
|
|
case SymbolKind.Element:
|
|
return this.getQuickInfoForElementSymbol(symbol);
|
|
case SymbolKind.Variable:
|
|
return this.getQuickInfoForVariableSymbol(symbol);
|
|
case SymbolKind.Reference:
|
|
return this.getQuickInfoForReferenceSymbol(symbol);
|
|
case SymbolKind.DomBinding:
|
|
return this.getQuickInfoForDomBinding(symbol);
|
|
case SymbolKind.Directive:
|
|
return this.getQuickInfoAtShimLocation(symbol.shimLocation);
|
|
case SymbolKind.Pipe:
|
|
return this.getQuickInfoForPipeSymbol(symbol);
|
|
case SymbolKind.Expression:
|
|
return this.getQuickInfoAtShimLocation(symbol.shimLocation);
|
|
}
|
|
}
|
|
|
|
private getQuickInfoForBindingSymbol(symbol: InputBindingSymbol|OutputBindingSymbol): ts.QuickInfo
|
|
|undefined {
|
|
if (symbol.bindings.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const kind =
|
|
symbol.kind === SymbolKind.Input ? DisplayInfoKind.PROPERTY : DisplayInfoKind.EVENT;
|
|
|
|
const quickInfo = this.getQuickInfoAtShimLocation(symbol.bindings[0].shimLocation);
|
|
return quickInfo === undefined ? undefined : updateQuickInfoKind(quickInfo, kind);
|
|
}
|
|
|
|
private getQuickInfoForElementSymbol(symbol: ElementSymbol): ts.QuickInfo {
|
|
const {templateNode} = symbol;
|
|
const matches = getDirectiveMatchesForElementTag(templateNode, symbol.directives);
|
|
if (matches.size > 0) {
|
|
return this.getQuickInfoForDirectiveSymbol(matches.values().next().value, templateNode);
|
|
}
|
|
|
|
return createQuickInfo(
|
|
templateNode.name, DisplayInfoKind.ELEMENT, getTextSpanOfNode(templateNode),
|
|
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType));
|
|
}
|
|
|
|
private getQuickInfoForVariableSymbol(symbol: VariableSymbol): ts.QuickInfo {
|
|
const documentation = this.getDocumentationFromTypeDefAtLocation(symbol.initializerLocation);
|
|
return createQuickInfo(
|
|
symbol.declaration.name, DisplayInfoKind.VARIABLE, getTextSpanOfNode(this.node),
|
|
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType), documentation);
|
|
}
|
|
|
|
private getQuickInfoForReferenceSymbol(symbol: ReferenceSymbol): ts.QuickInfo {
|
|
const documentation = this.getDocumentationFromTypeDefAtLocation(symbol.targetLocation);
|
|
return createQuickInfo(
|
|
symbol.declaration.name, DisplayInfoKind.REFERENCE, getTextSpanOfNode(this.node),
|
|
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType), documentation);
|
|
}
|
|
|
|
private getQuickInfoForPipeSymbol(symbol: PipeSymbol): ts.QuickInfo|undefined {
|
|
if (symbol.tsSymbol !== null) {
|
|
const quickInfo = this.getQuickInfoAtShimLocation(symbol.shimLocation);
|
|
return quickInfo === undefined ? undefined :
|
|
updateQuickInfoKind(quickInfo, DisplayInfoKind.PIPE);
|
|
} else {
|
|
return createQuickInfo(
|
|
this.typeChecker.typeToString(symbol.classSymbol.tsType), DisplayInfoKind.PIPE,
|
|
getTextSpanOfNode(this.node));
|
|
}
|
|
}
|
|
|
|
private getQuickInfoForDomBinding(symbol: DomBindingSymbol) {
|
|
if (!(this.node instanceof TmplAstTextAttribute) &&
|
|
!(this.node instanceof TmplAstBoundAttribute)) {
|
|
return undefined;
|
|
}
|
|
const directives = getDirectiveMatchesForAttribute(
|
|
this.node.name, symbol.host.templateNode, symbol.host.directives);
|
|
if (directives.size === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
return this.getQuickInfoForDirectiveSymbol(directives.values().next().value);
|
|
}
|
|
|
|
private getQuickInfoForDirectiveSymbol(dir: DirectiveSymbol, node: TmplAstNode|AST = this.node):
|
|
ts.QuickInfo {
|
|
const kind = dir.isComponent ? DisplayInfoKind.COMPONENT : DisplayInfoKind.DIRECTIVE;
|
|
const documentation = this.getDocumentationFromTypeDefAtLocation(dir.shimLocation);
|
|
let containerName: string|undefined;
|
|
if (ts.isClassDeclaration(dir.tsSymbol.valueDeclaration) && dir.ngModule !== null) {
|
|
containerName = dir.ngModule.name.getText();
|
|
}
|
|
|
|
return createQuickInfo(
|
|
this.typeChecker.typeToString(dir.tsType), kind, getTextSpanOfNode(this.node),
|
|
containerName, undefined, documentation);
|
|
}
|
|
|
|
private getDocumentationFromTypeDefAtLocation(shimLocation: ShimLocation):
|
|
ts.SymbolDisplayPart[]|undefined {
|
|
const typeDefs = this.tsLS.getTypeDefinitionAtPosition(
|
|
shimLocation.shimPath, shimLocation.positionInShimFile);
|
|
if (typeDefs === undefined || typeDefs.length === 0) {
|
|
return undefined;
|
|
}
|
|
return this.tsLS.getQuickInfoAtPosition(typeDefs[0].fileName, typeDefs[0].textSpan.start)
|
|
?.documentation;
|
|
}
|
|
|
|
private getQuickInfoAtShimLocation(location: ShimLocation): ts.QuickInfo|undefined {
|
|
const quickInfo =
|
|
this.tsLS.getQuickInfoAtPosition(location.shimPath, location.positionInShimFile);
|
|
if (quickInfo === undefined || quickInfo.displayParts === undefined) {
|
|
return quickInfo;
|
|
}
|
|
|
|
quickInfo.displayParts = filterAliasImports(quickInfo.displayParts);
|
|
|
|
const textSpan = getTextSpanOfNode(this.node);
|
|
return {...quickInfo, textSpan};
|
|
}
|
|
}
|
|
|
|
function updateQuickInfoKind(quickInfo: ts.QuickInfo, kind: DisplayInfoKind): ts.QuickInfo {
|
|
if (quickInfo.displayParts === undefined) {
|
|
return quickInfo;
|
|
}
|
|
|
|
const startsWithKind = quickInfo.displayParts.length >= 3 &&
|
|
displayPartsEqual(quickInfo.displayParts[0], {text: '(', kind: SYMBOL_PUNC}) &&
|
|
quickInfo.displayParts[1].kind === SYMBOL_TEXT &&
|
|
displayPartsEqual(quickInfo.displayParts[2], {text: ')', kind: SYMBOL_PUNC});
|
|
if (startsWithKind) {
|
|
quickInfo.displayParts[1].text = kind;
|
|
} else {
|
|
quickInfo.displayParts = [
|
|
{text: '(', kind: SYMBOL_PUNC},
|
|
{text: kind, kind: SYMBOL_TEXT},
|
|
{text: ')', kind: SYMBOL_PUNC},
|
|
{text: ' ', kind: SYMBOL_SPACE},
|
|
...quickInfo.displayParts,
|
|
];
|
|
}
|
|
return quickInfo;
|
|
}
|
|
|
|
function displayPartsEqual(a: {text: string, kind: string}, b: {text: string, kind: string}) {
|
|
return a.text === b.text && a.kind === b.kind;
|
|
}
|
|
|
|
function isDollarAny(node: TmplAstNode|AST): node is MethodCall {
|
|
return node instanceof MethodCall && node.receiver instanceof ImplicitReceiver &&
|
|
!(node.receiver instanceof ThisReceiver) && node.name === '$any' && node.args.length === 1;
|
|
}
|
|
|
|
function createDollarAnyQuickInfo(node: MethodCall): ts.QuickInfo {
|
|
return createQuickInfo(
|
|
'$any',
|
|
DisplayInfoKind.METHOD,
|
|
getTextSpanOfNode(node),
|
|
/** containerName */ undefined,
|
|
'any',
|
|
[{
|
|
kind: SYMBOL_TEXT,
|
|
text: 'function to cast an expression to the `any` type',
|
|
}],
|
|
);
|
|
}
|
|
|
|
// TODO(atscott): Create special `ts.QuickInfo` for `ng-template` and `ng-container` as well.
|
|
function createNgTemplateQuickInfo(node: TmplAstNode|AST): ts.QuickInfo {
|
|
return createQuickInfo(
|
|
'ng-template',
|
|
DisplayInfoKind.TEMPLATE,
|
|
getTextSpanOfNode(node),
|
|
/** containerName */ undefined,
|
|
/** type */ undefined,
|
|
[{
|
|
kind: SYMBOL_TEXT,
|
|
text:
|
|
'The `<ng-template>` is an Angular element for rendering HTML. It is never displayed directly.',
|
|
}],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Construct a QuickInfo object taking into account its container and type.
|
|
* @param name Name of the QuickInfo target
|
|
* @param kind component, directive, pipe, etc.
|
|
* @param textSpan span of the target
|
|
* @param containerName either the Symbol's container or the NgModule that contains the directive
|
|
* @param type user-friendly name of the type
|
|
* @param documentation docstring or comment
|
|
*/
|
|
export function createQuickInfo(
|
|
name: string, kind: DisplayInfoKind, textSpan: ts.TextSpan, containerName?: string,
|
|
type?: string, documentation?: ts.SymbolDisplayPart[]): ts.QuickInfo {
|
|
const displayParts = createDisplayParts(name, kind, containerName, type);
|
|
|
|
return {
|
|
kind: unsafeCastDisplayInfoKindToScriptElementKind(kind),
|
|
kindModifiers: ts.ScriptElementKindModifier.none,
|
|
textSpan: textSpan,
|
|
displayParts,
|
|
documentation,
|
|
};
|
|
}
|