refactor(language-service): introduce DisplayParts abstraction for Ivy (#39505)

This commit refactors the QuickInfo abstraction shared between the VE and
Ivy services and used to implement hover tooltips (quick info), which was
extracted from the VE code in commit faa81dc. The new DisplayParts
abstraction is more general and can be used to extract information needed by
various LS functions (e.g. autocompletion).

This commit effectively reverts faa81dc, returning the original code to the
VE implementation as the Ivy code is now diverged.

PR Close #39505
This commit is contained in:
Alex Rickabaugh 2020-10-12 12:51:43 -07:00 committed by Joey Perrott
parent cf48d508af
commit 643c96184c
20 changed files with 563 additions and 334 deletions

View File

@ -19,7 +19,6 @@ ts_library(
"//packages/compiler",
"//packages/compiler-cli",
"//packages/core",
"//packages/language-service/common",
"@npm//@types/node",
"@npm//typescript",
],

View File

@ -1,11 +0,0 @@
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//packages/language-service:__subpackages__"])
ts_library(
name = "common",
srcs = glob(["*.ts"]),
deps = [
"@npm//typescript",
],
)

View File

@ -1,107 +0,0 @@
/**
* @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 * as ts from 'typescript';
import {findTightestNode, getClassDeclFromDecoratorProp, getPropertyAssignmentFromValue} from './ts_utils';
export interface ResourceResolver {
/**
* Resolve the url of a resource relative to the file that contains the reference to it.
*
* @param file The, possibly relative, url of the resource.
* @param basePath The path to the file that contains the URL of the resource.
* @returns A resolved url of resource.
* @throws An error if the resource cannot be resolved.
*/
resolve(file: string, basePath: string): string;
}
/**
* Gets an Angular-specific definition in a TypeScript source file.
*/
export function getTsDefinitionAndBoundSpan(
sf: ts.SourceFile, position: number,
resourceResolver: ResourceResolver): ts.DefinitionInfoAndBoundSpan|undefined {
const node = findTightestNode(sf, position);
if (!node) return;
switch (node.kind) {
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
// Attempt to extract definition of a URL in a property assignment.
return getUrlFromProperty(node as ts.StringLiteralLike, resourceResolver);
default:
return undefined;
}
}
/**
* Attempts to get the definition of a file whose URL is specified in a property assignment in a
* directive decorator.
* Currently applies to `templateUrl` and `styleUrls` properties.
*/
function getUrlFromProperty(urlNode: ts.StringLiteralLike, resourceResolver: ResourceResolver):
ts.DefinitionInfoAndBoundSpan|undefined {
// Get the property assignment node corresponding to the `templateUrl` or `styleUrls` assignment.
// These assignments are specified differently; `templateUrl` is a string, and `styleUrls` is
// an array of strings:
// {
// templateUrl: './template.ng.html',
// styleUrls: ['./style.css', './other-style.css']
// }
// `templateUrl`'s property assignment can be found from the string literal node;
// `styleUrls`'s property assignment can be found from the array (parent) node.
//
// First search for `templateUrl`.
let asgn = getPropertyAssignmentFromValue(urlNode, 'templateUrl');
if (!asgn) {
// `templateUrl` assignment not found; search for `styleUrls` array assignment.
asgn = getPropertyAssignmentFromValue(urlNode.parent, 'styleUrls');
if (!asgn) {
// Nothing found, bail.
return;
}
}
// If the property assignment is not a property of a class decorator, don't generate definitions
// for it.
if (!getClassDeclFromDecoratorProp(asgn)) {
return;
}
const sf = urlNode.getSourceFile();
let url: string;
try {
url = resourceResolver.resolve(urlNode.text, sf.fileName);
} catch {
// If the file does not exist, bail.
return;
}
const templateDefinitions: ts.DefinitionInfo[] = [{
kind: ts.ScriptElementKind.externalModuleName,
name: url,
containerKind: ts.ScriptElementKind.unknown,
containerName: '',
// Reading the template is expensive, so don't provide a preview.
// TODO(ayazhafiz): Consider providing an actual span:
// 1. We're likely to read the template anyway
// 2. We could show just the first 100 chars or so
textSpan: {start: 0, length: 0},
fileName: url,
}];
return {
definitions: templateDefinitions,
textSpan: {
// Exclude opening and closing quotes in the url span.
start: urlNode.getStart() + 1,
length: urlNode.getWidth() - 2,
},
};
}

View File

@ -1,60 +0,0 @@
/**
* @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 * as ts from 'typescript';
// Reverse mappings of enum would generate strings
export const ALIAS_NAME = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.aliasName];
export const SYMBOL_INTERFACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.interfaceName];
export const SYMBOL_PUNC = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.punctuation];
export const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space];
export const SYMBOL_TEXT = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.text];
/**
* 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: 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 typeDisplayParts = type ?
[
{text: ':', kind: SYMBOL_PUNC},
{text: ' ', kind: SYMBOL_SPACE},
{text: type, kind: SYMBOL_INTERFACE},
] :
[];
return {
kind: kind as ts.ScriptElementKind,
kindModifiers: ts.ScriptElementKindModifier.none,
textSpan: textSpan,
displayParts: [
{text: '(', kind: SYMBOL_PUNC},
{text: kind, kind: SYMBOL_TEXT},
{text: ')', kind: SYMBOL_PUNC},
{text: ' ', kind: SYMBOL_SPACE},
...containerDisplayParts,
{text: name, kind: SYMBOL_INTERFACE},
...typeDisplayParts,
],
documentation,
};
}

View File

@ -1,87 +0,0 @@
/**
* @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 * as ts from 'typescript';
/**
* Return the node that most tightly encompass the specified `position`.
* @param node
* @param position
*/
export function findTightestNode(node: ts.Node, position: number): ts.Node|undefined {
if (node.getStart() <= position && position < node.getEnd()) {
return node.forEachChild(c => findTightestNode(c, position)) || node;
}
}
/**
* Returns a property assignment from the assignment value if the property name
* matches the specified `key`, or `undefined` if there is no match.
*/
export function getPropertyAssignmentFromValue(value: ts.Node, key: string): ts.PropertyAssignment|
undefined {
const propAssignment = value.parent;
if (!propAssignment || !ts.isPropertyAssignment(propAssignment) ||
propAssignment.name.getText() !== key) {
return;
}
return propAssignment;
}
/**
* Given a decorator property assignment, return the ClassDeclaration node that corresponds to the
* directive class the property applies to.
* If the property assignment is not on a class decorator, no declaration is returned.
*
* For example,
*
* @Component({
* template: '<div></div>'
* ^^^^^^^^^^^^^^^^^^^^^^^---- property assignment
* })
* class AppComponent {}
* ^---- class declaration node
*
* @param propAsgnNode property assignment
*/
export function getClassDeclFromDecoratorProp(propAsgnNode: ts.PropertyAssignment):
ts.ClassDeclaration|undefined {
if (!propAsgnNode.parent || !ts.isObjectLiteralExpression(propAsgnNode.parent)) {
return;
}
const objLitExprNode = propAsgnNode.parent;
if (!objLitExprNode.parent || !ts.isCallExpression(objLitExprNode.parent)) {
return;
}
const callExprNode = objLitExprNode.parent;
if (!callExprNode.parent || !ts.isDecorator(callExprNode.parent)) {
return;
}
const decorator = callExprNode.parent;
if (!decorator.parent || !ts.isClassDeclaration(decorator.parent)) {
return;
}
const classDeclNode = decorator.parent;
return classDeclNode;
}
/**
* Given the node which is the string of the inline template for a component, returns the
* `ts.ClassDeclaration` for the component.
*/
export function getClassDeclOfInlineTemplateNode(templateStringNode: ts.Node): ts.ClassDeclaration|
undefined {
if (!ts.isStringLiteralLike(templateStringNode)) {
return;
}
const tmplAsgn = getPropertyAssignmentFromValue(templateStringNode, 'template');
if (!tmplAsgn) {
return;
}
return getClassDeclFromDecoratorProp(tmplAsgn);
}

View File

@ -17,7 +17,6 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/shims",
"//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"//packages/language-service/common",
"@npm//typescript",
],
)

View File

@ -11,10 +11,106 @@ import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ShimLocation, Symbol, SymbolKind, TemplateSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
import * as ts from 'typescript';
import {getTsDefinitionAndBoundSpan, ResourceResolver} from '../common/definitions';
import {getPathToNodeAtPosition} from './hybrid_visitor';
import {flatMap, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, getTextSpanOfNode, isDollarEvent, isTypeScriptFile, TemplateInfo, toTextSpan} from './utils';
import {findTightestNode, flatMap, getClassDeclFromDecoratorProp, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getPropertyAssignmentFromValue, getTemplateInfoAtPosition, getTextSpanOfNode, isDollarEvent, isTypeScriptFile, TemplateInfo, toTextSpan} from './utils';
export interface ResourceResolver {
/**
* Resolve the url of a resource relative to the file that contains the reference to it.
*
* @param file The, possibly relative, url of the resource.
* @param basePath The path to the file that contains the URL of the resource.
* @returns A resolved url of resource.
* @throws An error if the resource cannot be resolved.
*/
resolve(file: string, basePath: string): string;
}
/**
* Gets an Angular-specific definition in a TypeScript source file.
*/
export function getTsDefinitionAndBoundSpan(
sf: ts.SourceFile, position: number,
resourceResolver: ResourceResolver): ts.DefinitionInfoAndBoundSpan|undefined {
const node = findTightestNode(sf, position);
if (!node) return;
switch (node.kind) {
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
// Attempt to extract definition of a URL in a property assignment.
return getUrlFromProperty(node as ts.StringLiteralLike, resourceResolver);
default:
return undefined;
}
}
/**
* Attempts to get the definition of a file whose URL is specified in a property assignment in a
* directive decorator.
* Currently applies to `templateUrl` and `styleUrls` properties.
*/
function getUrlFromProperty(urlNode: ts.StringLiteralLike, resourceResolver: ResourceResolver):
ts.DefinitionInfoAndBoundSpan|undefined {
// Get the property assignment node corresponding to the `templateUrl` or `styleUrls` assignment.
// These assignments are specified differently; `templateUrl` is a string, and `styleUrls` is
// an array of strings:
// {
// templateUrl: './template.ng.html',
// styleUrls: ['./style.css', './other-style.css']
// }
// `templateUrl`'s property assignment can be found from the string literal node;
// `styleUrls`'s property assignment can be found from the array (parent) node.
//
// First search for `templateUrl`.
let asgn = getPropertyAssignmentFromValue(urlNode, 'templateUrl');
if (!asgn) {
// `templateUrl` assignment not found; search for `styleUrls` array assignment.
asgn = getPropertyAssignmentFromValue(urlNode.parent, 'styleUrls');
if (!asgn) {
// Nothing found, bail.
return;
}
}
// If the property assignment is not a property of a class decorator, don't generate definitions
// for it.
if (!getClassDeclFromDecoratorProp(asgn)) {
return;
}
const sf = urlNode.getSourceFile();
let url: string;
try {
url = resourceResolver.resolve(urlNode.text, sf.fileName);
} catch {
// If the file does not exist, bail.
return;
}
const templateDefinitions: ts.DefinitionInfo[] = [{
kind: ts.ScriptElementKind.externalModuleName,
name: url,
containerKind: ts.ScriptElementKind.unknown,
containerName: '',
// Reading the template is expensive, so don't provide a preview.
// TODO(ayazhafiz): Consider providing an actual span:
// 1. We're likely to read the template anyway
// 2. We could show just the first 100 chars or so
textSpan: {start: 0, length: 0},
fileName: url,
}];
return {
definitions: templateDefinitions,
textSpan: {
// Exclude opening and closing quotes in the url span.
start: urlNode.getStart() + 1,
length: urlNode.getWidth() - 2,
},
};
}
interface DefinitionMeta {
node: AST|TmplAstNode;

View File

@ -0,0 +1,126 @@
/**
* @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 {ReferenceSymbol, ShimLocation, Symbol, SymbolKind, VariableSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
import * as ts from 'typescript';
// Reverse mappings of enum would generate strings
export const ALIAS_NAME = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.aliasName];
export const SYMBOL_INTERFACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.interfaceName];
export const SYMBOL_PUNC = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.punctuation];
export const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space];
export const SYMBOL_TEXT = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.text];
/**
* Label for various kinds of Angular entities for TS display info.
*/
export enum DisplayInfoKind {
COMPONENT = 'component',
DIRECTIVE = 'directive',
EVENT = 'event',
REFERENCE = 'reference',
ELEMENT = 'element',
VARIABLE = 'variable',
PIPE = 'pipe',
PROPERTY = 'property',
METHOD = 'method',
TEMPLATE = 'template',
}
export interface DisplayInfo {
kind: DisplayInfoKind;
displayParts: ts.SymbolDisplayPart[];
documentation: ts.SymbolDisplayPart[]|undefined;
}
export function getDisplayInfo(
tsLS: ts.LanguageService, typeChecker: ts.TypeChecker,
symbol: ReferenceSymbol|VariableSymbol): DisplayInfo {
let kind: DisplayInfoKind;
if (symbol.kind === SymbolKind.Reference) {
kind = DisplayInfoKind.REFERENCE;
} else if (symbol.kind === SymbolKind.Variable) {
kind = DisplayInfoKind.VARIABLE;
} else {
throw new Error(
`AssertionError: unexpected symbol kind ${SymbolKind[(symbol as Symbol).kind]}`);
}
const displayParts = createDisplayParts(
symbol.declaration.name, kind, /* containerName */ undefined,
typeChecker.typeToString(symbol.tsType));
const documentation = getDocumentationFromTypeDefAtLocation(tsLS, symbol.shimLocation);
return {
kind,
displayParts,
documentation,
};
}
/**
* Construct a compound `ts.SymbolDisplayPart[]` which incorporates the container and type of a
* target declaration.
* @param name Name of the target
* @param kind component, directive, pipe, etc.
* @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 createDisplayParts(
name: string, kind: DisplayInfoKind, containerName: string|undefined,
type: string|undefined): ts.SymbolDisplayPart[] {
const containerDisplayParts = containerName !== undefined ?
[
{text: containerName, kind: SYMBOL_INTERFACE},
{text: '.', kind: SYMBOL_PUNC},
] :
[];
const typeDisplayParts = type !== undefined ?
[
{text: ':', kind: SYMBOL_PUNC},
{text: ' ', kind: SYMBOL_SPACE},
{text: type, kind: SYMBOL_INTERFACE},
] :
[];
return [
{text: '(', kind: SYMBOL_PUNC},
{text: kind, kind: SYMBOL_TEXT},
{text: ')', kind: SYMBOL_PUNC},
{text: ' ', kind: SYMBOL_SPACE},
...containerDisplayParts,
{text: name, kind: SYMBOL_INTERFACE},
...typeDisplayParts,
];
}
/**
* Convert a `SymbolDisplayInfoKind` to a `ts.ScriptElementKind` type, allowing it to pass through
* TypeScript APIs.
*
* In practice, this is an "illegal" type cast. Since `ts.ScriptElementKind` is a string, this is
* safe to do if TypeScript only uses the value in a string context. Consumers of this conversion
* function are responsible for ensuring this is the case.
*/
export function unsafeCastDisplayInfoKindToScriptElementKind(kind: DisplayInfoKind):
ts.ScriptElementKind {
return kind as string as ts.ScriptElementKind;
}
function getDocumentationFromTypeDefAtLocation(
tsLS: ts.LanguageService, shimLocation: ShimLocation): ts.SymbolDisplayPart[]|undefined {
const typeDefs =
tsLS.getTypeDefinitionAtPosition(shimLocation.shimPath, shimLocation.positionInShimFile);
if (typeDefs === undefined || typeDefs.length === 0) {
return undefined;
}
return tsLS.getQuickInfoAtPosition(typeDefs[0].fileName, typeDefs[0].textSpan.start)
?.documentation;
}

View File

@ -11,8 +11,7 @@ import {absoluteFrom, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file
import {AdapterResourceLoader} from '@angular/compiler-cli/src/ngtsc/resource';
import {isShim} from '@angular/compiler-cli/src/ngtsc/shims';
import * as ts from 'typescript/lib/tsserverlibrary';
import {ResourceResolver} from '../common/definitions';
import {ResourceResolver} from './definitions';
import {isTypeScriptFile} from './utils';

View File

@ -10,27 +10,10 @@ import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, ReferenceSymbol, ShimLocation, Symbol, SymbolKind, VariableSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
import * as ts from 'typescript';
import {createQuickInfo, SYMBOL_PUNC, SYMBOL_SPACE, SYMBOL_TEXT} from '../common/quick_info';
import {createDisplayParts, DisplayInfoKind, SYMBOL_PUNC, SYMBOL_SPACE, SYMBOL_TEXT, unsafeCastDisplayInfoKindToScriptElementKind} from './display_parts';
import {findNodeAtPosition} from './hybrid_visitor';
import {filterAliasImports, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, getTextSpanOfNode} from './utils';
/**
* The type of Angular directive. Used for QuickInfo in template.
*/
export enum QuickInfoKind {
COMPONENT = 'component',
DIRECTIVE = 'directive',
EVENT = 'event',
REFERENCE = 'reference',
ELEMENT = 'element',
VARIABLE = 'variable',
PIPE = 'pipe',
PROPERTY = 'property',
METHOD = 'method',
TEMPLATE = 'template',
}
export class QuickInfoBuilder {
private readonly typeChecker = this.compiler.getNextProgram().getTypeChecker();
constructor(private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler) {}
@ -86,7 +69,8 @@ export class QuickInfoBuilder {
return undefined;
}
const kind = symbol.kind === SymbolKind.Input ? QuickInfoKind.PROPERTY : QuickInfoKind.EVENT;
const kind =
symbol.kind === SymbolKind.Input ? DisplayInfoKind.PROPERTY : DisplayInfoKind.EVENT;
const quickInfo = this.getQuickInfoAtShimLocation(symbol.bindings[0].shimLocation, node);
return quickInfo === undefined ? undefined : updateQuickInfoKind(quickInfo, kind);
@ -100,7 +84,7 @@ export class QuickInfoBuilder {
}
return createQuickInfo(
templateNode.name, QuickInfoKind.ELEMENT, getTextSpanOfNode(templateNode),
templateNode.name, DisplayInfoKind.ELEMENT, getTextSpanOfNode(templateNode),
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType));
}
@ -108,7 +92,7 @@ export class QuickInfoBuilder {
ts.QuickInfo {
const documentation = this.getDocumentationFromTypeDefAtLocation(symbol.shimLocation);
return createQuickInfo(
symbol.declaration.name, QuickInfoKind.VARIABLE, getTextSpanOfNode(node),
symbol.declaration.name, DisplayInfoKind.VARIABLE, getTextSpanOfNode(node),
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType), documentation);
}
@ -116,14 +100,15 @@ export class QuickInfoBuilder {
ts.QuickInfo {
const documentation = this.getDocumentationFromTypeDefAtLocation(symbol.shimLocation);
return createQuickInfo(
symbol.declaration.name, QuickInfoKind.REFERENCE, getTextSpanOfNode(node),
symbol.declaration.name, DisplayInfoKind.REFERENCE, getTextSpanOfNode(node),
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType), documentation);
}
private getQuickInfoForPipeSymbol(symbol: ExpressionSymbol, node: TmplAstNode|AST): ts.QuickInfo
|undefined {
const quickInfo = this.getQuickInfoAtShimLocation(symbol.shimLocation, node);
return quickInfo === undefined ? undefined : updateQuickInfoKind(quickInfo, QuickInfoKind.PIPE);
return quickInfo === undefined ? undefined :
updateQuickInfoKind(quickInfo, DisplayInfoKind.PIPE);
}
private getQuickInfoForDomBinding(node: TmplAstNode|AST, symbol: DomBindingSymbol) {
@ -141,7 +126,7 @@ export class QuickInfoBuilder {
private getQuickInfoForDirectiveSymbol(dir: DirectiveSymbol, node: TmplAstNode|AST):
ts.QuickInfo {
const kind = dir.isComponent ? QuickInfoKind.COMPONENT : QuickInfoKind.DIRECTIVE;
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) {
@ -179,7 +164,7 @@ export class QuickInfoBuilder {
}
}
function updateQuickInfoKind(quickInfo: ts.QuickInfo, kind: QuickInfoKind): ts.QuickInfo {
function updateQuickInfoKind(quickInfo: ts.QuickInfo, kind: DisplayInfoKind): ts.QuickInfo {
if (quickInfo.displayParts === undefined) {
return quickInfo;
}
@ -214,7 +199,7 @@ function isDollarAny(node: TmplAstNode|AST): node is MethodCall {
function createDollarAnyQuickInfo(node: MethodCall): ts.QuickInfo {
return createQuickInfo(
'$any',
QuickInfoKind.METHOD,
DisplayInfoKind.METHOD,
getTextSpanOfNode(node),
/** containerName */ undefined,
'any',
@ -229,7 +214,7 @@ function createDollarAnyQuickInfo(node: MethodCall): ts.QuickInfo {
function createNgTemplateQuickInfo(node: TmplAstNode|AST): ts.QuickInfo {
return createQuickInfo(
'ng-template',
QuickInfoKind.TEMPLATE,
DisplayInfoKind.TEMPLATE,
getTextSpanOfNode(node),
/** containerName */ undefined,
/** type */ undefined,
@ -240,3 +225,26 @@ function createNgTemplateQuickInfo(node: TmplAstNode|AST): ts.QuickInfo {
}],
);
}
/**
* 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,
};
}

View File

@ -13,8 +13,88 @@ import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expr
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
import * as ts from 'typescript';
import {ALIAS_NAME, SYMBOL_PUNC} from '../common/quick_info';
import {findTightestNode, getClassDeclOfInlineTemplateNode} from '../common/ts_utils';
import {ALIAS_NAME, SYMBOL_PUNC} from './display_parts';
/**
* Return the node that most tightly encompass the specified `position`.
* @param node
* @param position
*/
export function findTightestNode(node: ts.Node, position: number): ts.Node|undefined {
if (node.getStart() <= position && position < node.getEnd()) {
return node.forEachChild(c => findTightestNode(c, position)) || node;
}
}
/**
* Returns a property assignment from the assignment value if the property name
* matches the specified `key`, or `undefined` if there is no match.
*/
export function getPropertyAssignmentFromValue(value: ts.Node, key: string): ts.PropertyAssignment|
undefined {
const propAssignment = value.parent;
if (!propAssignment || !ts.isPropertyAssignment(propAssignment) ||
propAssignment.name.getText() !== key) {
return;
}
return propAssignment;
}
/**
* Given a decorator property assignment, return the ClassDeclaration node that corresponds to the
* directive class the property applies to.
* If the property assignment is not on a class decorator, no declaration is returned.
*
* For example,
*
* @Component({
* template: '<div></div>'
* ^^^^^^^^^^^^^^^^^^^^^^^---- property assignment
* })
* class AppComponent {}
* ^---- class declaration node
*
* @param propAsgnNode property assignment
*/
export function getClassDeclFromDecoratorProp(propAsgnNode: ts.PropertyAssignment):
ts.ClassDeclaration|undefined {
if (!propAsgnNode.parent || !ts.isObjectLiteralExpression(propAsgnNode.parent)) {
return;
}
const objLitExprNode = propAsgnNode.parent;
if (!objLitExprNode.parent || !ts.isCallExpression(objLitExprNode.parent)) {
return;
}
const callExprNode = objLitExprNode.parent;
if (!callExprNode.parent || !ts.isDecorator(callExprNode.parent)) {
return;
}
const decorator = callExprNode.parent;
if (!decorator.parent || !ts.isClassDeclaration(decorator.parent)) {
return;
}
const classDeclNode = decorator.parent;
return classDeclNode;
}
/**
* Given the node which is the string of the inline template for a component, returns the
* `ts.ClassDeclaration` for the component.
*/
export function getClassDeclOfInlineTemplateNode(templateStringNode: ts.Node): ts.ClassDeclaration|
undefined {
if (!ts.isStringLiteralLike(templateStringNode)) {
return;
}
const tmplAsgn = getPropertyAssignmentFromValue(templateStringNode, 'template');
if (!tmplAsgn) {
return;
}
return getClassDeclFromDecoratorProp(tmplAsgn);
}
export function getTextSpanOfNode(node: t.Node|e.AST): ts.TextSpan {
if (isTemplateNodeWithKeyAndValue(node)) {

View File

@ -6,9 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as path from 'path';
import * as ts from 'typescript'; // used as value and is provided at runtime
import {locateSymbols} from './locate_symbol';
import {findTightestNode, getClassDeclFromDecoratorProp, getPropertyAssignmentFromValue} from './ts_utils';
import {AstResult, Span} from './types';
/**
@ -22,6 +24,69 @@ function ngSpanToTsTextSpan(span: Span): ts.TextSpan {
length: span.end - span.start,
};
}
/**
* Attempts to get the definition of a file whose URL is specified in a property assignment in a
* directive decorator.
* Currently applies to `templateUrl` and `styleUrls` properties.
*/
function getUrlFromProperty(
urlNode: ts.StringLiteralLike,
tsLsHost: Readonly<ts.LanguageServiceHost>): ts.DefinitionInfoAndBoundSpan|undefined {
// Get the property assignment node corresponding to the `templateUrl` or `styleUrls` assignment.
// These assignments are specified differently; `templateUrl` is a string, and `styleUrls` is
// an array of strings:
// {
// templateUrl: './template.ng.html',
// styleUrls: ['./style.css', './other-style.css']
// }
// `templateUrl`'s property assignment can be found from the string literal node;
// `styleUrls`'s property assignment can be found from the array (parent) node.
//
// First search for `templateUrl`.
let asgn = getPropertyAssignmentFromValue(urlNode, 'templateUrl');
if (!asgn) {
// `templateUrl` assignment not found; search for `styleUrls` array assignment.
asgn = getPropertyAssignmentFromValue(urlNode.parent, 'styleUrls');
if (!asgn) {
// Nothing found, bail.
return;
}
}
// If the property assignment is not a property of a class decorator, don't generate definitions
// for it.
if (!getClassDeclFromDecoratorProp(asgn)) {
return;
}
const sf = urlNode.getSourceFile();
// Extract url path specified by the url node, which is relative to the TypeScript source file
// the url node is defined in.
const url = path.join(path.dirname(sf.fileName), urlNode.text);
// If the file does not exist, bail. It is possible that the TypeScript language service host
// does not have a `fileExists` method, in which case optimistically assume the file exists.
if (tsLsHost.fileExists && !tsLsHost.fileExists(url)) return;
const templateDefinitions: ts.DefinitionInfo[] = [{
kind: ts.ScriptElementKind.externalModuleName,
name: url,
containerKind: ts.ScriptElementKind.unknown,
containerName: '',
// Reading the template is expensive, so don't provide a preview.
textSpan: {start: 0, length: 0},
fileName: url,
}];
return {
definitions: templateDefinitions,
textSpan: {
// Exclude opening and closing quotes in the url span.
start: urlNode.getStart() + 1,
length: urlNode.getWidth() - 2,
},
};
}
/**
* Traverse the template AST and look for the symbol located at `position`, then
@ -80,3 +145,21 @@ export function getDefinitionAndBoundSpan(
textSpan: symbols[0].span,
};
}
/**
* Gets an Angular-specific definition in a TypeScript source file.
*/
export function getTsDefinitionAndBoundSpan(
sf: ts.SourceFile, position: number,
tsLsHost: Readonly<ts.LanguageServiceHost>): ts.DefinitionInfoAndBoundSpan|undefined {
const node = findTightestNode(sf, position);
if (!node) return;
switch (node.kind) {
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
// Attempt to extract definition of a URL in a property assignment.
return getUrlFromProperty(node as ts.StringLiteralLike, tsLsHost);
default:
return undefined;
}
}

View File

@ -10,11 +10,9 @@ import {NgAnalyzedModules} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';
import {findTightestNode} from '../common/ts_utils';
import {createDiagnostic, Diagnostic} from './diagnostic_messages';
import {getTemplateExpressionDiagnostics} from './expression_diagnostics';
import {findPropertyValueOfType} from './ts_utils';
import {findPropertyValueOfType, findTightestNode} from './ts_utils';
import * as ng from './types';
import {TypeScriptServiceHost} from './typescript_host';
import {offsetSpan, spanOf} from './utils';

View File

@ -9,8 +9,6 @@
import {NgAnalyzedModules} from '@angular/compiler';
import * as ts from 'typescript';
import {createQuickInfo} from '../common/quick_info';
import {locateSymbols} from './locate_symbol';
import * as ng from './types';
import {inSpan} from './utils';
@ -67,3 +65,56 @@ export function getTsHover(
}
}
}
// Reverse mappings of enum would generate strings
const ALIAS_NAME = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.aliasName];
const SYMBOL_INTERFACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.interfaceName];
const SYMBOL_PUNC = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.punctuation];
const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space];
const SYMBOL_TEXT = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.text];
/**
* 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 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 typeDisplayParts = type ?
[
{text: ':', kind: SYMBOL_PUNC},
{text: ' ', kind: SYMBOL_SPACE},
{text: type, kind: SYMBOL_INTERFACE},
] :
[];
return {
kind: kind as ts.ScriptElementKind,
kindModifiers: ts.ScriptElementKindModifier.none,
textSpan: textSpan,
displayParts: [
{text: '(', kind: SYMBOL_PUNC},
{text: kind, kind: SYMBOL_TEXT},
{text: ')', kind: SYMBOL_PUNC},
{text: ' ', kind: SYMBOL_SPACE},
...containerDisplayParts,
{text: name, kind: SYMBOL_INTERFACE},
...typeDisplayParts,
],
documentation,
};
}

View File

@ -9,10 +9,8 @@
import * as path from 'path';
import * as tss from 'typescript/lib/tsserverlibrary';
import {getTsDefinitionAndBoundSpan, ResourceResolver} from '../common/definitions';
import {getTemplateCompletions} from './completions';
import {getDefinitionAndBoundSpan} from './definitions';
import {getDefinitionAndBoundSpan, getTsDefinitionAndBoundSpan} from './definitions';
import {getDeclarationDiagnostics, getTemplateDiagnostics, ngDiagnosticToTsDiagnostic} from './diagnostics';
import {getTemplateHover, getTsHover} from './hover';
import * as ng from './types';
@ -83,8 +81,7 @@ class LanguageServiceImpl implements ng.LanguageService {
if (fileName.endsWith('.ts')) {
const sf = this.host.getSourceFile(fileName);
if (sf) {
return getTsDefinitionAndBoundSpan(
sf, position, new ViewEngineLSResourceResolver(this.host.tsLsHost));
return getTsDefinitionAndBoundSpan(sf, position, this.host.tsLsHost);
}
}
}
@ -115,20 +112,3 @@ class LanguageServiceImpl implements ng.LanguageService {
return this.host.tsLS.getReferencesAtPosition(tsDef.fileName, tsDef.textSpan.start);
}
}
class ViewEngineLSResourceResolver implements ResourceResolver {
constructor(private host: ts.LanguageServiceHost) {}
resolve(file: string, basePath: string): string {
// Extract url path specified by the url node, which is relative to the TypeScript source file
// the url node is defined in.
const url = path.join(path.dirname(basePath), file);
// If the file does not exist, bail. It is possible that the TypeScript language service host
// does not have a `fileExists` method, in which case optimistically assume the file exists.
if (this.host.fileExists && !this.host.fileExists(url)) {
throw new Error(`ResourceResolver: could not resolve ${url} in context of ${basePath})`);
}
return url;
}
}

View File

@ -68,3 +68,81 @@ export function findPropertyValueOfType<T extends ts.Node>(
}
return startNode.forEachChild(c => findPropertyValueOfType(c, propName, predicate));
}
/**
* Return the node that most tightly encompass the specified `position`.
* @param node
* @param position
*/
export function findTightestNode(node: ts.Node, position: number): ts.Node|undefined {
if (node.getStart() <= position && position < node.getEnd()) {
return node.forEachChild(c => findTightestNode(c, position)) || node;
}
}
/**
* Returns a property assignment from the assignment value if the property name
* matches the specified `key`, or `undefined` if there is no match.
*/
export function getPropertyAssignmentFromValue(value: ts.Node, key: string): ts.PropertyAssignment|
undefined {
const propAssignment = value.parent;
if (!propAssignment || !ts.isPropertyAssignment(propAssignment) ||
propAssignment.name.getText() !== key) {
return;
}
return propAssignment;
}
/**
* Given the node which is the string of the inline template for a component, returns the
* `ts.ClassDeclaration` for the component.
*/
export function getClassDeclOfInlineTemplateNode(templateStringNode: ts.Node): ts.ClassDeclaration|
undefined {
if (!ts.isStringLiteralLike(templateStringNode)) {
return;
}
const tmplAsgn = getPropertyAssignmentFromValue(templateStringNode, 'template');
if (!tmplAsgn) {
return;
}
return getClassDeclFromDecoratorProp(tmplAsgn);
}
/**
* Given a decorator property assignment, return the ClassDeclaration node that corresponds to the
* directive class the property applies to.
* If the property assignment is not on a class decorator, no declaration is returned.
*
* For example,
*
* @Component({
* template: '<div></div>'
* ^^^^^^^^^^^^^^^^^^^^^^^---- property assignment
* })
* class AppComponent {}
* ^---- class declaration node
*
* @param propAsgnNode property assignment
*/
export function getClassDeclFromDecoratorProp(propAsgnNode: ts.PropertyAssignment):
ts.ClassDeclaration|undefined {
if (!propAsgnNode.parent || !ts.isObjectLiteralExpression(propAsgnNode.parent)) {
return;
}
const objLitExprNode = propAsgnNode.parent;
if (!objLitExprNode.parent || !ts.isCallExpression(objLitExprNode.parent)) {
return;
}
const callExprNode = objLitExprNode.parent;
if (!callExprNode.parent || !ts.isDecorator(callExprNode.parent)) {
return;
}
const decorator = callExprNode.parent;
if (!decorator.parent || !ts.isClassDeclaration(decorator.parent)) {
return;
}
const classDeclNode = decorator.parent;
return classDeclNode;
}

View File

@ -10,12 +10,10 @@ import {analyzeNgModules, AotSummaryResolver, CompileDirectiveSummary, CompileMe
import {SchemaMetadata, ViewEncapsulation, ɵConsole as Console} from '@angular/core';
import * as tss from 'typescript/lib/tsserverlibrary';
import {findTightestNode, getClassDeclOfInlineTemplateNode} from '../common/ts_utils';
import {createLanguageService} from './language_service';
import {ReflectorHost} from './reflector_host';
import {ExternalTemplate, InlineTemplate} from './template';
import {getDirectiveClassLike} from './ts_utils';
import {findTightestNode, getClassDeclOfInlineTemplateNode, getDirectiveClassLike} from './ts_utils';
import {AstResult, Declaration, DeclarationError, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, TemplateSource} from './types';
/**

View File

@ -7,6 +7,7 @@
*/
import {AstPath, BoundEventAst, CompileDirectiveSummary, CompileTypeMetadata, CssSelector, DirectiveAst, ElementAst, EmbeddedTemplateAst, HtmlAstPath, identifierName, Identifiers, Node, ParseSourceSpan, RecursiveTemplateAstVisitor, RecursiveVisitor, TemplateAst, TemplateAstPath, templateVisitAll, visitAll} from '@angular/compiler';
import {getClassDeclFromDecoratorProp, getPropertyAssignmentFromValue} from './ts_utils';
import {AstResult, DiagnosticTemplateInfo, SelectorInfo, Span, Symbol, SymbolQuery} from './types';
interface SpanHolder {

View File

@ -59,7 +59,6 @@ ts_library(
"//packages/compiler",
"//packages/language-service",
"//packages/language-service:ts_utils",
"//packages/language-service/common",
"@npm//typescript",
],
)

View File

@ -9,8 +9,7 @@
import * as ng from '@angular/compiler';
import * as ts from 'typescript';
import {getClassDeclFromDecoratorProp} from '../common/ts_utils';
import {getDirectiveClassLike} from '../src/ts_utils';
import {getClassDeclFromDecoratorProp, getDirectiveClassLike} from '../src/ts_utils';
import {getPathToNodeAtPosition} from '../src/utils';
import {MockTypescriptHost} from './test_utils';