refactor(language-service): Avoid leaking host outside of LanguageService (#34941)

As part of the effort to tighten the API surface of
`TypeScriptServiceHost` in preparation for the migration to Ivy, I realized
some recently added APIs are not strictly needed.
They can be safely removed without sacrificing functionality.

This allows us to clean up the code, especially in the implementation of
QuickInfo, where the `TypeScriptServiceHost` is leaked outside of the
`LanguageService` class.

This refactoring also cleans up some duplicate code where the QuickInfo
object is generated. The logic is now consolidated into a simple
`createQuickInfo` method shared across two different implementations.

PR Close #34941
This commit is contained in:
Keen Yee Liau
2020-01-23 11:34:27 -08:00
committed by Andrew Kushnir
parent 8926f764a5
commit 3414ef276e
7 changed files with 108 additions and 205 deletions

View File

@ -6,20 +6,16 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CompileSummaryKind, StaticSymbol} from '@angular/compiler';
import {NgAnalyzedModules} from '@angular/compiler';
import * as ts from 'typescript';
import {AstResult} from './common';
import {locateSymbols} from './locate_symbol';
import * as ng from './types';
import {TypeScriptServiceHost} from './typescript_host';
import {findTightestNode} from './utils';
import {inSpan} from './utils';
// Reverse mappings of enum would generate strings
const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space];
const SYMBOL_PUNC = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.punctuation];
const SYMBOL_CLASS = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.className];
const SYMBOL_TEXT = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.text];
const SYMBOL_INTERFACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.interfaceName];
@ -28,124 +24,92 @@ const SYMBOL_INTERFACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.inter
* return the corresponding quick info.
* @param info template AST
* @param position location of the symbol
* @param host Language Service host to query
* @param analyzedModules all NgModules in the program.
*/
export function getHover(info: AstResult, position: number, host: Readonly<TypeScriptServiceHost>):
ts.QuickInfo|undefined {
export function getTemplateHover(
info: AstResult, position: number, analyzedModules: NgAnalyzedModules): ts.QuickInfo|undefined {
const symbolInfo = locateSymbols(info, position)[0];
if (!symbolInfo) {
return;
}
const {symbol, span, staticSymbol} = symbolInfo;
const {symbol, span, compileTypeSummary} = symbolInfo;
const textSpan = {start: span.start, length: span.end - span.start};
if (compileTypeSummary && compileTypeSummary.summaryKind === CompileSummaryKind.Directive) {
return getDirectiveModule(compileTypeSummary.type.reference, textSpan, host, symbol);
// The container is either the symbol's container (for example, 'AppComponent'
// is the container of the symbol 'title' in its template) or the NgModule
// that the directive belongs to (the container of AppComponent is AppModule).
let containerName: string|undefined = symbol.container ?.name;
if (!containerName && staticSymbol) {
// If there is a static symbol then the target is a directive.
const ngModule = analyzedModules.ngModuleByPipeOrDirective.get(staticSymbol);
containerName = ngModule ?.type.reference.name;
}
const containerDisplayParts: ts.SymbolDisplayPart[] = symbol.container ?
[
{text: symbol.container.name, kind: symbol.container.kind},
{text: '.', kind: SYMBOL_PUNC},
] :
[];
const typeDisplayParts: ts.SymbolDisplayPart[] = symbol.type ?
[
{text: ':', kind: SYMBOL_PUNC},
{text: ' ', kind: SYMBOL_SPACE},
{text: symbol.type.name, kind: SYMBOL_INTERFACE},
] :
[];
return {
kind: symbol.kind as ts.ScriptElementKind,
kindModifiers: '', // kindModifier info not available on 'ng.Symbol'
textSpan,
documentation: symbol.documentation,
// this would generate a string like '(property) ClassX.propY: type'
// 'kind' in displayParts does not really matter because it's dropped when
// displayParts get converted to string.
displayParts: [
{text: '(', kind: SYMBOL_PUNC},
{text: symbol.kind, kind: symbol.kind},
{text: ')', kind: SYMBOL_PUNC},
{text: ' ', kind: SYMBOL_SPACE},
...containerDisplayParts,
{text: symbol.name, kind: symbol.kind},
...typeDisplayParts,
],
};
return createQuickInfo(symbol.name, symbol.kind, span, containerName, symbol.type?.name, symbol.documentation);
}
/**
* Get quick info for Angular semantic entities in TypeScript files, like Directives.
* @param sf TypeScript source file an Angular symbol is in
* @param position location of the symbol in the source file
* @param host Language Service host to query
* @param declarations All Directive-like declarations in the source file.
* @param analyzedModules all NgModules in the program.
*/
export function getTsHover(
sf: ts.SourceFile, position: number, host: Readonly<TypeScriptServiceHost>): ts.QuickInfo|
undefined {
const node = findTightestNode(sf, position);
if (!node) return;
switch (node.kind) {
case ts.SyntaxKind.Identifier:
const directiveId = node as ts.Identifier;
if (ts.isClassDeclaration(directiveId.parent)) {
const directiveName = directiveId.text;
const directiveSymbol = host.getStaticSymbol(node.getSourceFile().fileName, directiveName);
if (!directiveSymbol) return;
return getDirectiveModule(
directiveSymbol,
{start: directiveId.getStart(), length: directiveId.end - directiveId.getStart()},
host);
}
break;
default:
break;
position: number, declarations: ng.Declaration[],
analyzedModules: NgAnalyzedModules): ts.QuickInfo|undefined {
for (const {declarationSpan, metadata} of declarations) {
if (inSpan(position, declarationSpan)) {
const staticSymbol: ng.StaticSymbol = metadata.type.reference;
const directiveName = staticSymbol.name;
const kind = metadata.isComponent ? 'component' : 'directive';
const textSpan = ts.createTextSpanFromBounds(declarationSpan.start, declarationSpan.end);
const ngModule = analyzedModules.ngModuleByPipeOrDirective.get(staticSymbol);
const moduleName = ngModule ?.type.reference.name;
return createQuickInfo(
directiveName, kind, textSpan, moduleName, ts.ScriptElementKind.classElement);
}
}
return undefined;
}
/**
* Attempts to get quick info for the NgModule a Directive is declared in.
* @param directive identifier on a potential Directive class declaration
* @param textSpan span of the symbol
* @param host Language Service host to query
* @param symbol the internal symbol that represents the directive
* 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
*/
function getDirectiveModule(
directive: StaticSymbol, textSpan: ts.TextSpan, host: Readonly<TypeScriptServiceHost>,
symbol?: ng.Symbol): ts.QuickInfo|undefined {
const analyzedModules = host.getAnalyzedModules(false);
const ngModule = analyzedModules.ngModuleByPipeOrDirective.get(directive);
if (!ngModule) return;
function createQuickInfo(
name: string, kind: string, textSpan: ts.TextSpan, containerName?: string, type?: string,
documentation?: ts.SymbolDisplayPart[]): ts.QuickInfo {
const containerDisplayParts = containerName ?
[
{text: containerName, kind: SYMBOL_INTERFACE},
{text: '.', kind: SYMBOL_PUNC},
] :
[];
const isComponent =
host.getDeclarations(directive.filePath)
.find(decl => decl.type === directive && decl.metadata && decl.metadata.isComponent);
const typeDisplayParts = type ?
[
{text: ':', kind: SYMBOL_PUNC},
{text: ' ', kind: SYMBOL_SPACE},
{text: type, kind: SYMBOL_INTERFACE},
] :
[];
const moduleName = ngModule.type.reference.name;
return {
kind: ts.ScriptElementKind.classElement,
kindModifiers:
ts.ScriptElementKindModifier.none, // kindModifier info not available on 'ng.Symbol'
textSpan,
documentation: symbol ? symbol.documentation : undefined,
// This generates a string like '(directive) NgModule.Directive: class'
// 'kind' in displayParts does not really matter because it's dropped when
// displayParts get converted to string.
kind: kind as ts.ScriptElementKind,
kindModifiers: ts.ScriptElementKindModifier.none,
textSpan: textSpan,
displayParts: [
{text: '(', kind: SYMBOL_PUNC},
{text: isComponent ? 'component' : 'directive', kind: SYMBOL_TEXT},
{text: kind, kind: SYMBOL_TEXT},
{text: ')', kind: SYMBOL_PUNC},
{text: ' ', kind: SYMBOL_SPACE},
{text: moduleName, kind: SYMBOL_CLASS},
{text: '.', kind: SYMBOL_PUNC},
{text: directive.name, kind: SYMBOL_CLASS},
{text: ':', kind: SYMBOL_PUNC},
{text: ' ', kind: SYMBOL_SPACE},
{text: ts.ScriptElementKind.classElement, kind: SYMBOL_TEXT},
...containerDisplayParts,
{text: name, kind: SYMBOL_INTERFACE},
...typeDisplayParts,
],
documentation,
};
}