diff --git a/packages/language-service/BUILD.bazel b/packages/language-service/BUILD.bazel index 24336820a8..4e1ef571e9 100644 --- a/packages/language-service/BUILD.bazel +++ b/packages/language-service/BUILD.bazel @@ -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"], diff --git a/packages/language-service/src/definitions.ts b/packages/language-service/src/definitions.ts index 0164ce2b24..eb10625d7a 100644 --- a/packages/language-service/src/definitions.ts +++ b/packages/language-service/src/definitions.ts @@ -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 diff --git a/packages/language-service/src/diagnostics.ts b/packages/language-service/src/diagnostics.ts index f5c55ecd82..e50686677c 100644 --- a/packages/language-service/src/diagnostics.ts +++ b/packages/language-service/src/diagnostics.ts @@ -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. diff --git a/packages/language-service/src/ts_utils.ts b/packages/language-service/src/ts_utils.ts new file mode 100644 index 0000000000..ea64381317 --- /dev/null +++ b/packages/language-service/src/ts_utils.ts @@ -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: '
' + * ^^^^^^^^^^^^^^^^^^^^^^^---- 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( + 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)); +} diff --git a/packages/language-service/src/typescript_host.ts b/packages/language-service/src/typescript_host.ts index 13ae098310..ff36ead24e 100644 --- a/packages/language-service/src/typescript_host.ts +++ b/packages/language-service/src/typescript_host.ts @@ -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); diff --git a/packages/language-service/src/utils.ts b/packages/language-service/src/utils.ts index a73fac69a6..5969116627 100644 --- a/packages/language-service/src/utils.ts +++ b/packages/language-service/src/utils.ts @@ -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(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( - 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: '
' - * ^^^^^^^^^^^^^^^^^^^^^^^---- 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); -} diff --git a/packages/language-service/test/BUILD.bazel b/packages/language-service/test/BUILD.bazel index e3557e87c2..f2f0a5959c 100644 --- a/packages/language-service/test/BUILD.bazel +++ b/packages/language-service/test/BUILD.bazel @@ -56,6 +56,7 @@ ts_library( ":test_utils_lib", "//packages/compiler", "//packages/language-service", + "//packages/language-service:ts_utils", "@npm//typescript", ], ) diff --git a/packages/language-service/test/utils_spec.ts b/packages/language-service/test/utils_spec.ts index c4d2159a99..69cc2b9274 100644 --- a/packages/language-service/test/utils_spec.ts +++ b/packages/language-service/test/utils_spec.ts @@ -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', () => {