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 commitfaa81dc
. 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 revertsfaa81dc
, returning the original code to the VE implementation as the Ivy code is now diverged. PR Close #39505
This commit is contained in:
parent
cf48d508af
commit
643c96184c
|
@ -19,7 +19,6 @@ ts_library(
|
|||
"//packages/compiler",
|
||||
"//packages/compiler-cli",
|
||||
"//packages/core",
|
||||
"//packages/language-service/common",
|
||||
"@npm//@types/node",
|
||||
"@npm//typescript",
|
||||
],
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -59,7 +59,6 @@ ts_library(
|
|||
"//packages/compiler",
|
||||
"//packages/language-service",
|
||||
"//packages/language-service:ts_utils",
|
||||
"//packages/language-service/common",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
Loading…
Reference in New Issue