diff --git a/packages/language-service/common/BUILD.bazel b/packages/language-service/common/BUILD.bazel index afe8f39b56..5c3ccefc5a 100644 --- a/packages/language-service/common/BUILD.bazel +++ b/packages/language-service/common/BUILD.bazel @@ -6,6 +6,7 @@ ts_library( name = "common", srcs = glob(["*.ts"]), deps = [ + "@npm//@types/node", "@npm//typescript", ], ) diff --git a/packages/language-service/common/definitions.ts b/packages/language-service/common/definitions.ts new file mode 100644 index 0000000000..0db481cd14 --- /dev/null +++ b/packages/language-service/common/definitions.ts @@ -0,0 +1,94 @@ +/** + * @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 path from 'path'; +import * as ts from 'typescript'; + +import {findTightestNode, getClassDeclFromDecoratorProp, getPropertyAssignmentFromValue} from './ts_utils'; + +/** + * Gets an Angular-specific definition in a TypeScript source file. + */ +export function getTsDefinitionAndBoundSpan( + sf: ts.SourceFile, position: number, + tsLsHost: Pick): 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; + } +} + +/** + * 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: Pick): 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, + }, + }; +} diff --git a/packages/language-service/common/ts_utils.ts b/packages/language-service/common/ts_utils.ts new file mode 100644 index 0000000000..08b0d22f56 --- /dev/null +++ b/packages/language-service/common/ts_utils.ts @@ -0,0 +1,87 @@ +/** + * @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: '
' + * ^^^^^^^^^^^^^^^^^^^^^^^---- 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) +} diff --git a/packages/language-service/src/definitions.ts b/packages/language-service/src/definitions.ts index f986ac1eb2..6539895d68 100644 --- a/packages/language-service/src/definitions.ts +++ b/packages/language-service/src/definitions.ts @@ -6,11 +6,9 @@ * 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'; /** @@ -82,85 +80,3 @@ 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.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; - } -} - -/** - * 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.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, - }, - }; -} diff --git a/packages/language-service/src/diagnostics.ts b/packages/language-service/src/diagnostics.ts index adbf824a04..1848156516 100644 --- a/packages/language-service/src/diagnostics.ts +++ b/packages/language-service/src/diagnostics.ts @@ -10,9 +10,11 @@ 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, findTightestNode} from './ts_utils'; +import {findPropertyValueOfType} from './ts_utils'; import * as ng from './types'; import {TypeScriptServiceHost} from './typescript_host'; import {offsetSpan, spanOf} from './utils'; diff --git a/packages/language-service/src/language_service.ts b/packages/language-service/src/language_service.ts index da40fb7cb8..3ee3e24a4c 100644 --- a/packages/language-service/src/language_service.ts +++ b/packages/language-service/src/language_service.ts @@ -6,10 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ +import {getTsDefinitionAndBoundSpan} from '@angular/language-service/common/definitions'; import * as tss from 'typescript/lib/tsserverlibrary'; import {getTemplateCompletions} from './completions'; -import {getDefinitionAndBoundSpan, getTsDefinitionAndBoundSpan} from './definitions'; +import {getDefinitionAndBoundSpan} from './definitions'; import {getDeclarationDiagnostics, getTemplateDiagnostics, ngDiagnosticToTsDiagnostic} from './diagnostics'; import {getTemplateHover, getTsHover} from './hover'; import * as ng from './types'; diff --git a/packages/language-service/src/ts_utils.ts b/packages/language-service/src/ts_utils.ts index 581e9c921f..11c756007b 100644 --- a/packages/language-service/src/ts_utils.ts +++ b/packages/language-service/src/ts_utils.ts @@ -8,68 +8,6 @@ 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; diff --git a/packages/language-service/src/typescript_host.ts b/packages/language-service/src/typescript_host.ts index ae927fd77d..0921f50400 100644 --- a/packages/language-service/src/typescript_host.ts +++ b/packages/language-service/src/typescript_host.ts @@ -10,10 +10,12 @@ 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 {findTightestNode, getClassDeclFromDecoratorProp, getDirectiveClassLike, getPropertyAssignmentFromValue} from './ts_utils'; +import {getDirectiveClassLike} from './ts_utils'; import {AstResult, Declaration, DeclarationError, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, TemplateSource} from './types'; /** @@ -382,11 +384,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost { if (!tss.isStringLiteralLike(node)) { return; } - const tmplAsgn = getPropertyAssignmentFromValue(node, 'template'); - if (!tmplAsgn) { - return; - } - const classDecl = getClassDeclFromDecoratorProp(tmplAsgn); + const classDecl = getClassDeclOfInlineTemplateNode(node); if (!classDecl || !classDecl.name) { // Does not handle anonymous class return; }