angular-cn/packages/language-service/ivy/quick_info.ts

245 lines
9.7 KiB
TypeScript
Raw Normal View History

/**
* @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 {
fix(compiler-cli): ensure the compiler tracks `ts.Program`s correctly (#41291) `NgCompiler` previously had a notion of the "next" `ts.Program`, which served two purposes: * it allowed a client using the `ts.createProgram` API to query for the latest program produced by the previous `NgCompiler`, as a starting point for building the _next_ program that incorporated any new user changes. * it allowed the old `NgCompiler` to be queried for the `ts.Program` on which all prior state is based, which is needed to compute the delta from the new program to ultimately determine how much of the prior state can be reused. This system contained a flaw: it relied on the `NgCompiler` knowing when the `ts.Program` would be changed. This works fine for changes that originate in `NgCompiler` APIs, but a client of the `TemplateTypeChecker` may use that API in ways that create new `ts.Program`s without the `NgCompiler`'s knowledge. This caused the `NgCompiler`'s concept of the "next" program to get out of sync, causing incorrectness in future incremental analysis. This refactoring cleans up the compiler's `ts.Program` management in several ways: * `TypeCheckingProgramStrategy`, the API which controls `ts.Program` updating, is renamed to the `ProgramDriver` and extracted to a separate ngtsc package. * It loses its responsibility of determining component shim filenames. That functionality now lives exclusively in the template type-checking package. * The "next" `ts.Program` concept is renamed to the "current" program, as the "next" name was misleading in several ways. * `NgCompiler` now wraps the `ProgramDriver` used in the `TemplateTypeChecker` to know when a new `ts.Program` is created, regardless of which API drove the creation, which actually fixes the bug. PR Close #41291
2021-03-19 20:06:10 -04:00
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,
};
}