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:
Keen Yee Liau 2020-05-07 08:55:33 -07:00 committed by Misko Hevery
parent 7eb1a58b26
commit 40025f55ad
8 changed files with 160 additions and 145 deletions

View File

@ -9,8 +9,12 @@ ts_library(
"*.ts",
"src/**/*.ts",
],
exclude = [
"src/ts_utils.ts",
],
),
deps = [
":ts_utils",
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli",
@ -20,6 +24,14 @@ ts_library(
],
)
ts_library(
name = "ts_utils",
srcs = ["src/ts_utils.ts"],
deps = [
"@npm//typescript",
],
)
pkg_npm(
name = "npm_package",
srcs = ["package.json"],

View File

@ -10,8 +10,8 @@ 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';
import {findTightestNode, getPropertyAssignmentFromValue, isClassDecoratorProperty} from './utils';
/**
* 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.
//
// First search for `templateUrl`.
let asgn = getPropertyAssignmentFromValue(urlNode);
if (!asgn || asgn.name.getText() !== 'templateUrl') {
let asgn = getPropertyAssignmentFromValue(urlNode, 'templateUrl');
if (!asgn) {
// `templateUrl` assignment not found; search for `styleUrls` array assignment.
asgn = getPropertyAssignmentFromValue(urlNode.parent);
if (!asgn || asgn.name.getText() !== 'styleUrls') {
asgn = getPropertyAssignmentFromValue(urlNode.parent, 'styleUrls');
if (!asgn) {
// Nothing found, bail.
return;
}
@ -132,7 +132,9 @@ function getUrlFromProperty(
// If the property assignment is not a property of a class decorator, don't generate definitions
// for it.
if (!isClassDecoratorProperty(asgn)) return;
if (!getClassDeclFromDecoratorProp(asgn)) {
return;
}
const sf = urlNode.getSourceFile();
// Extract url path specified by the url node, which is relative to the TypeScript source file

View File

@ -12,9 +12,10 @@ import * as ts from 'typescript';
import {createDiagnostic, Diagnostic} from './diagnostic_messages';
import {getTemplateExpressionDiagnostics} from './expression_diagnostics';
import {findPropertyValueOfType, findTightestNode} from './ts_utils';
import * as ng from './types';
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.

View File

@ -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));
}

View File

@ -13,9 +13,8 @@ import * as tss from 'typescript/lib/tsserverlibrary';
import {createLanguageService} from './language_service';
import {ReflectorHost} from './reflector_host';
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 {findTightestNode, getClassDeclFromDecoratorProp, getDirectiveClassLike, getPropertyAssignmentFromValue} from './utils';
/**
* Create a `LanguageServiceHost`
@ -370,8 +369,8 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
if (!tss.isStringLiteralLike(node)) {
return;
}
const tmplAsgn = getPropertyAssignmentFromValue(node);
if (!tmplAsgn || tmplAsgn.name.getText() !== 'template') {
const tmplAsgn = getPropertyAssignmentFromValue(node, 'template');
if (!tmplAsgn) {
return;
}
const classDecl = getClassDeclFromDecoratorProp(tmplAsgn);

View File

@ -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 * as ts from 'typescript';
import {AstResult, DiagnosticTemplateInfo, SelectorInfo, Span, Symbol, SymbolQuery} from './types';
interface SpanHolder {
@ -146,78 +144,6 @@ export function findTemplateAstAt(ast: TemplateAst[], position: number): Templat
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
* 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);
}

View File

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

View File

@ -9,7 +9,8 @@
import * as ng from '@angular/compiler';
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';
describe('getDirectiveClassLike', () => {