refactor(language-service): move TS utils to separate file (#36984)
This commit refactors TS-only utility functions to a separate file so that they could be shared with Ivy language service. A separate ts_library rule is created so that there is no dependency on `compiler` and `compiler-cli` to make the compilation fast and light-weight. The method `getPropertyAssignmentFromValue` is modified slightly to improve the ergonomics of the function. PR Close #36984
This commit is contained in:
parent
7eb1a58b26
commit
40025f55ad
|
@ -9,8 +9,12 @@ ts_library(
|
||||||
"*.ts",
|
"*.ts",
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
],
|
],
|
||||||
|
exclude = [
|
||||||
|
"src/ts_utils.ts",
|
||||||
|
],
|
||||||
),
|
),
|
||||||
deps = [
|
deps = [
|
||||||
|
":ts_utils",
|
||||||
"//packages:types",
|
"//packages:types",
|
||||||
"//packages/compiler",
|
"//packages/compiler",
|
||||||
"//packages/compiler-cli",
|
"//packages/compiler-cli",
|
||||||
|
@ -20,6 +24,14 @@ ts_library(
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "ts_utils",
|
||||||
|
srcs = ["src/ts_utils.ts"],
|
||||||
|
deps = [
|
||||||
|
"@npm//typescript",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
pkg_npm(
|
pkg_npm(
|
||||||
name = "npm_package",
|
name = "npm_package",
|
||||||
srcs = ["package.json"],
|
srcs = ["package.json"],
|
||||||
|
|
|
@ -10,8 +10,8 @@ import * as path from 'path';
|
||||||
import * as ts from 'typescript'; // used as value and is provided at runtime
|
import * as ts from 'typescript'; // used as value and is provided at runtime
|
||||||
|
|
||||||
import {locateSymbols} from './locate_symbol';
|
import {locateSymbols} from './locate_symbol';
|
||||||
|
import {findTightestNode, getClassDeclFromDecoratorProp, getPropertyAssignmentFromValue} from './ts_utils';
|
||||||
import {AstResult, Span} from './types';
|
import {AstResult, Span} from './types';
|
||||||
import {findTightestNode, getPropertyAssignmentFromValue, isClassDecoratorProperty} from './utils';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert Angular Span to TypeScript TextSpan. Angular Span has 'start' and
|
* Convert Angular Span to TypeScript TextSpan. Angular Span has 'start' and
|
||||||
|
@ -120,11 +120,11 @@ function getUrlFromProperty(
|
||||||
// `styleUrls`'s property assignment can be found from the array (parent) node.
|
// `styleUrls`'s property assignment can be found from the array (parent) node.
|
||||||
//
|
//
|
||||||
// First search for `templateUrl`.
|
// First search for `templateUrl`.
|
||||||
let asgn = getPropertyAssignmentFromValue(urlNode);
|
let asgn = getPropertyAssignmentFromValue(urlNode, 'templateUrl');
|
||||||
if (!asgn || asgn.name.getText() !== 'templateUrl') {
|
if (!asgn) {
|
||||||
// `templateUrl` assignment not found; search for `styleUrls` array assignment.
|
// `templateUrl` assignment not found; search for `styleUrls` array assignment.
|
||||||
asgn = getPropertyAssignmentFromValue(urlNode.parent);
|
asgn = getPropertyAssignmentFromValue(urlNode.parent, 'styleUrls');
|
||||||
if (!asgn || asgn.name.getText() !== 'styleUrls') {
|
if (!asgn) {
|
||||||
// Nothing found, bail.
|
// Nothing found, bail.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -132,7 +132,9 @@ function getUrlFromProperty(
|
||||||
|
|
||||||
// If the property assignment is not a property of a class decorator, don't generate definitions
|
// If the property assignment is not a property of a class decorator, don't generate definitions
|
||||||
// for it.
|
// for it.
|
||||||
if (!isClassDecoratorProperty(asgn)) return;
|
if (!getClassDeclFromDecoratorProp(asgn)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const sf = urlNode.getSourceFile();
|
const sf = urlNode.getSourceFile();
|
||||||
// Extract url path specified by the url node, which is relative to the TypeScript source file
|
// Extract url path specified by the url node, which is relative to the TypeScript source file
|
||||||
|
|
|
@ -12,9 +12,10 @@ import * as ts from 'typescript';
|
||||||
|
|
||||||
import {createDiagnostic, Diagnostic} from './diagnostic_messages';
|
import {createDiagnostic, Diagnostic} from './diagnostic_messages';
|
||||||
import {getTemplateExpressionDiagnostics} from './expression_diagnostics';
|
import {getTemplateExpressionDiagnostics} from './expression_diagnostics';
|
||||||
|
import {findPropertyValueOfType, findTightestNode} from './ts_utils';
|
||||||
import * as ng from './types';
|
import * as ng from './types';
|
||||||
import {TypeScriptServiceHost} from './typescript_host';
|
import {TypeScriptServiceHost} from './typescript_host';
|
||||||
import {findPropertyValueOfType, findTightestNode, offsetSpan, spanOf} from './utils';
|
import {offsetSpan, spanOf} from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return diagnostic information for the parsed AST of the template.
|
* Return diagnostic information for the parsed AST of the template.
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. 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/lib/tsserverlibrary';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 propAsgn 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DirectiveClassLike {
|
||||||
|
decoratorId: ts.Identifier; // decorator identifier, like @Component
|
||||||
|
classId: ts.Identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return metadata about `node` if it looks like an Angular directive class.
|
||||||
|
* In this case, potential matches are `@NgModule`, `@Component`, `@Directive`,
|
||||||
|
* `@Pipe`, etc.
|
||||||
|
* These class declarations all share some common attributes, namely their
|
||||||
|
* decorator takes exactly one parameter and the parameter must be an object
|
||||||
|
* literal.
|
||||||
|
*
|
||||||
|
* For example,
|
||||||
|
* v---------- `decoratorId`
|
||||||
|
* @NgModule({ <
|
||||||
|
* declarations: [], < classDecl
|
||||||
|
* }) <
|
||||||
|
* class AppModule {} <
|
||||||
|
* ^----- `classId`
|
||||||
|
*
|
||||||
|
* @param node Potential node that represents an Angular directive.
|
||||||
|
*/
|
||||||
|
export function getDirectiveClassLike(node: ts.Node): DirectiveClassLike|undefined {
|
||||||
|
if (!ts.isClassDeclaration(node) || !node.name || !node.decorators) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const d of node.decorators) {
|
||||||
|
const expr = d.expression;
|
||||||
|
if (!ts.isCallExpression(expr) || expr.arguments.length !== 1 ||
|
||||||
|
!ts.isIdentifier(expr.expression)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const arg = expr.arguments[0];
|
||||||
|
if (ts.isObjectLiteralExpression(arg)) {
|
||||||
|
return {
|
||||||
|
decoratorId: expr.expression,
|
||||||
|
classId: node.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the value of a property assignment that is nested in a TypeScript node and is of a certain
|
||||||
|
* type T.
|
||||||
|
*
|
||||||
|
* @param startNode node to start searching for nested property assignment from
|
||||||
|
* @param propName property assignment name
|
||||||
|
* @param predicate function to verify that a node is of type T.
|
||||||
|
* @return node property assignment value of type T, or undefined if none is found
|
||||||
|
*/
|
||||||
|
export function findPropertyValueOfType<T extends ts.Node>(
|
||||||
|
startNode: ts.Node, propName: string, predicate: (node: ts.Node) => node is T): T|undefined {
|
||||||
|
if (ts.isPropertyAssignment(startNode) && startNode.name.getText() === propName) {
|
||||||
|
const {initializer} = startNode;
|
||||||
|
if (predicate(initializer)) return initializer;
|
||||||
|
}
|
||||||
|
return startNode.forEachChild(c => findPropertyValueOfType(c, propName, predicate));
|
||||||
|
}
|
|
@ -13,9 +13,8 @@ import * as tss from 'typescript/lib/tsserverlibrary';
|
||||||
import {createLanguageService} from './language_service';
|
import {createLanguageService} from './language_service';
|
||||||
import {ReflectorHost} from './reflector_host';
|
import {ReflectorHost} from './reflector_host';
|
||||||
import {ExternalTemplate, InlineTemplate} from './template';
|
import {ExternalTemplate, InlineTemplate} from './template';
|
||||||
|
import {findTightestNode, getClassDeclFromDecoratorProp, getDirectiveClassLike, getPropertyAssignmentFromValue} from './ts_utils';
|
||||||
import {AstResult, Declaration, DeclarationError, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, TemplateSource} from './types';
|
import {AstResult, Declaration, DeclarationError, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, TemplateSource} from './types';
|
||||||
import {findTightestNode, getClassDeclFromDecoratorProp, getDirectiveClassLike, getPropertyAssignmentFromValue} from './utils';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a `LanguageServiceHost`
|
* Create a `LanguageServiceHost`
|
||||||
|
@ -370,8 +369,8 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
|
||||||
if (!tss.isStringLiteralLike(node)) {
|
if (!tss.isStringLiteralLike(node)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tmplAsgn = getPropertyAssignmentFromValue(node);
|
const tmplAsgn = getPropertyAssignmentFromValue(node, 'template');
|
||||||
if (!tmplAsgn || tmplAsgn.name.getText() !== 'template') {
|
if (!tmplAsgn) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const classDecl = getClassDeclFromDecoratorProp(tmplAsgn);
|
const classDecl = getClassDeclFromDecoratorProp(tmplAsgn);
|
||||||
|
|
|
@ -7,8 +7,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AstPath, BoundEventAst, CompileDirectiveSummary, CompileTypeMetadata, CssSelector, DirectiveAst, ElementAst, EmbeddedTemplateAst, HtmlAstPath, identifierName, Identifiers, Node, ParseSourceSpan, RecursiveTemplateAstVisitor, RecursiveVisitor, TemplateAst, TemplateAstPath, templateVisitAll, visitAll} from '@angular/compiler';
|
import {AstPath, BoundEventAst, CompileDirectiveSummary, CompileTypeMetadata, CssSelector, DirectiveAst, ElementAst, EmbeddedTemplateAst, HtmlAstPath, identifierName, Identifiers, Node, ParseSourceSpan, RecursiveTemplateAstVisitor, RecursiveVisitor, TemplateAst, TemplateAstPath, templateVisitAll, visitAll} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
|
||||||
|
|
||||||
import {AstResult, DiagnosticTemplateInfo, SelectorInfo, Span, Symbol, SymbolQuery} from './types';
|
import {AstResult, DiagnosticTemplateInfo, SelectorInfo, Span, Symbol, SymbolQuery} from './types';
|
||||||
|
|
||||||
interface SpanHolder {
|
interface SpanHolder {
|
||||||
|
@ -146,78 +144,6 @@ export function findTemplateAstAt(ast: TemplateAst[], position: number): Templat
|
||||||
return new AstPath<TemplateAst>(path, position);
|
return new AstPath<TemplateAst>(path, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DirectiveClassLike {
|
|
||||||
decoratorId: ts.Identifier; // decorator identifier, like @Component
|
|
||||||
classId: ts.Identifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return metadata about `node` if it looks like an Angular directive class.
|
|
||||||
* In this case, potential matches are `@NgModule`, `@Component`, `@Directive`,
|
|
||||||
* `@Pipe`, etc.
|
|
||||||
* These class declarations all share some common attributes, namely their
|
|
||||||
* decorator takes exactly one parameter and the parameter must be an object
|
|
||||||
* literal.
|
|
||||||
*
|
|
||||||
* For example,
|
|
||||||
* v---------- `decoratorId`
|
|
||||||
* @NgModule({ <
|
|
||||||
* declarations: [], < classDecl
|
|
||||||
* }) <
|
|
||||||
* class AppModule {} <
|
|
||||||
* ^----- `classId`
|
|
||||||
*
|
|
||||||
* @param node Potential node that represents an Angular directive.
|
|
||||||
*/
|
|
||||||
export function getDirectiveClassLike(node: ts.Node): DirectiveClassLike|undefined {
|
|
||||||
if (!ts.isClassDeclaration(node) || !node.name || !node.decorators) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const d of node.decorators) {
|
|
||||||
const expr = d.expression;
|
|
||||||
if (!ts.isCallExpression(expr) || expr.arguments.length !== 1 ||
|
|
||||||
!ts.isIdentifier(expr.expression)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const arg = expr.arguments[0];
|
|
||||||
if (ts.isObjectLiteralExpression(arg)) {
|
|
||||||
return {
|
|
||||||
decoratorId: expr.expression,
|
|
||||||
classId: node.name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the value of a property assignment that is nested in a TypeScript node and is of a certain
|
|
||||||
* type T.
|
|
||||||
*
|
|
||||||
* @param startNode node to start searching for nested property assignment from
|
|
||||||
* @param propName property assignment name
|
|
||||||
* @param predicate function to verify that a node is of type T.
|
|
||||||
* @return node property assignment value of type T, or undefined if none is found
|
|
||||||
*/
|
|
||||||
export function findPropertyValueOfType<T extends ts.Node>(
|
|
||||||
startNode: ts.Node, propName: string, predicate: (node: ts.Node) => node is T): T|undefined {
|
|
||||||
if (ts.isPropertyAssignment(startNode) && startNode.name.getText() === propName) {
|
|
||||||
const {initializer} = startNode;
|
|
||||||
if (predicate(initializer)) return initializer;
|
|
||||||
}
|
|
||||||
return startNode.forEachChild(c => findPropertyValueOfType(c, propName, predicate));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the tightest node at the specified `position` from the AST `nodes`, and
|
* Find the tightest node at the specified `position` from the AST `nodes`, and
|
||||||
* return the path to the node.
|
* return the path to the node.
|
||||||
|
@ -277,62 +203,3 @@ export function findOutputBinding(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a property assignment from the assignment value, or `undefined` if there is no
|
|
||||||
* assignment.
|
|
||||||
*/
|
|
||||||
export function getPropertyAssignmentFromValue(value: ts.Node): ts.PropertyAssignment|undefined {
|
|
||||||
if (!value.parent || !ts.isPropertyAssignment(value.parent)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return value.parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 propAsgn 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if a property assignment is on a class decorator.
|
|
||||||
* See `getClassDeclFromDecoratorProperty`, which gets the class the decorator is applied to, for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* @param prop property assignment
|
|
||||||
*/
|
|
||||||
export function isClassDecoratorProperty(propAsgn: ts.PropertyAssignment): boolean {
|
|
||||||
return !!getClassDeclFromDecoratorProp(propAsgn);
|
|
||||||
}
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ ts_library(
|
||||||
":test_utils_lib",
|
":test_utils_lib",
|
||||||
"//packages/compiler",
|
"//packages/compiler",
|
||||||
"//packages/language-service",
|
"//packages/language-service",
|
||||||
|
"//packages/language-service:ts_utils",
|
||||||
"@npm//typescript",
|
"@npm//typescript",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
import * as ng from '@angular/compiler';
|
import * as ng from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {getClassDeclFromDecoratorProp, getDirectiveClassLike, getPathToNodeAtPosition} from '../src/utils';
|
import {getClassDeclFromDecoratorProp, getDirectiveClassLike} from '../src/ts_utils';
|
||||||
|
import {getPathToNodeAtPosition} from '../src/utils';
|
||||||
import {MockTypescriptHost} from './test_utils';
|
import {MockTypescriptHost} from './test_utils';
|
||||||
|
|
||||||
describe('getDirectiveClassLike', () => {
|
describe('getDirectiveClassLike', () => {
|
||||||
|
|
Loading…
Reference in New Issue