From f31a6015a0ee5fef34c34478566d8570b68ad8c2 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Mon, 22 Feb 2021 11:12:09 -0800 Subject: [PATCH] perf(language-service): short-circuit LS operations (#40946) When certain information is requested from the Angular Language Service, we know that there will be no additional Angular information if the requested position is not in an inline template, template url, or style url. To avoid unnecessary compiler compilations, we short circuit and return `undefined` before asking the compiler for any type of answer which would trigger a partial compilation, at the very least. fixes https://github.com/angular/vscode-ng-language-service/issues/1104 PR Close #40946 --- .../language-service/ivy/language_service.ts | 124 +++++++++++++----- packages/language-service/ivy/ts_utils.ts | 53 +++++++- 2 files changed, 145 insertions(+), 32 deletions(-) diff --git a/packages/language-service/ivy/language_service.ts b/packages/language-service/ivy/language_service.ts index 7f6a753595..682bd0bcce 100644 --- a/packages/language-service/ivy/language_service.ts +++ b/packages/language-service/ivy/language_service.ts @@ -26,6 +26,7 @@ import {DefinitionBuilder} from './definitions'; import {QuickInfoBuilder} from './quick_info'; import {ReferencesAndRenameBuilder} from './references'; import {getTargetAtPosition, TargetContext, TargetNodeKind} from './template_target'; +import {findTightestNode, getClassDeclFromDecoratorProp, getPropertyAssignmentFromValue} from './ts_utils'; import {getTemplateInfoAtPosition, isTypeScriptFile} from './utils'; export class LanguageService { @@ -74,20 +75,24 @@ export class LanguageService { getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan |undefined { - const compiler = this.compilerFactory.getOrCreate(); - const results = - new DefinitionBuilder(this.tsLS, compiler).getDefinitionAndBoundSpan(fileName, position); - this.compilerFactory.registerLastKnownProgram(); - return results; + return this.withCompiler((compiler) => { + if (!isInAngularContext(compiler.getNextProgram(), fileName, position)) { + return undefined; + } + return new DefinitionBuilder(this.tsLS, compiler) + .getDefinitionAndBoundSpan(fileName, position); + }); } getTypeDefinitionAtPosition(fileName: string, position: number): readonly ts.DefinitionInfo[]|undefined { - const compiler = this.compilerFactory.getOrCreate(); - const results = - new DefinitionBuilder(this.tsLS, compiler).getTypeDefinitionsAtPosition(fileName, position); - this.compilerFactory.registerLastKnownProgram(); - return results; + return this.withCompiler((compiler) => { + if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) { + return undefined; + } + return new DefinitionBuilder(this.tsLS, compiler) + .getTypeDefinitionsAtPosition(fileName, position); + }); } getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo|undefined { @@ -169,37 +174,51 @@ export class LanguageService { getCompletionsAtPosition( fileName: string, position: number, options: ts.GetCompletionsAtPositionOptions|undefined): ts.WithMetadata|undefined { - const builder = this.getCompletionBuilder(fileName, position); - if (builder === null) { - return undefined; - } - const result = builder.getCompletionsAtPosition(options); - this.compilerFactory.registerLastKnownProgram(); - return result; + return this.withCompiler((compiler) => { + if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) { + return undefined; + } + + const builder = this.getCompletionBuilder(fileName, position); + if (builder === null) { + return undefined; + } + return builder.getCompletionsAtPosition(options); + }); } getCompletionEntryDetails( fileName: string, position: number, entryName: string, formatOptions: ts.FormatCodeOptions|ts.FormatCodeSettings|undefined, preferences: ts.UserPreferences|undefined): ts.CompletionEntryDetails|undefined { - const builder = this.getCompletionBuilder(fileName, position); - if (builder === null) { - return undefined; - } - const result = builder.getCompletionEntryDetails(entryName, formatOptions, preferences); - this.compilerFactory.registerLastKnownProgram(); - return result; + return this.withCompiler((compiler) => { + if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) { + return undefined; + } + + const builder = this.getCompletionBuilder(fileName, position); + if (builder === null) { + return undefined; + } + return builder.getCompletionEntryDetails(entryName, formatOptions, preferences); + }); } getCompletionEntrySymbol(fileName: string, position: number, entryName: string): ts.Symbol |undefined { - const builder = this.getCompletionBuilder(fileName, position); - if (builder === null) { - return undefined; - } - const result = builder.getCompletionEntrySymbol(entryName); - this.compilerFactory.registerLastKnownProgram(); - return result; + return this.withCompiler((compiler) => { + if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) { + return undefined; + } + + const builder = this.getCompletionBuilder(fileName, position); + if (builder === null) { + return undefined; + } + const result = builder.getCompletionEntrySymbol(entryName); + this.compilerFactory.registerLastKnownProgram(); + return result; + }); } getComponentLocationsForTemplate(fileName: string): GetComponentLocationsForTemplateResponse { @@ -432,3 +451,46 @@ function nodeContextFromTarget(target: TargetContext): CompletionNodeContext { return CompletionNodeContext.None; } } + +function isTemplateContext(program: ts.Program, fileName: string, position: number): boolean { + if (!isTypeScriptFile(fileName)) { + // If we aren't in a TS file, we must be in an HTML file, which we treat as template context + return true; + } + + const node = findTightestNodeAtPosition(program, fileName, position); + if (node === undefined) { + return false; + } + + let asgn = getPropertyAssignmentFromValue(node, 'template'); + if (asgn === null) { + return false; + } + return getClassDeclFromDecoratorProp(asgn) !== null; +} + +function isInAngularContext(program: ts.Program, fileName: string, position: number) { + if (!isTypeScriptFile(fileName)) { + return true; + } + + const node = findTightestNodeAtPosition(program, fileName, position); + if (node === undefined) { + return false; + } + + const asgn = getPropertyAssignmentFromValue(node, 'template') ?? + getPropertyAssignmentFromValue(node, 'templateUrl') ?? + getPropertyAssignmentFromValue(node.parent, 'styleUrls'); + return asgn !== null && getClassDeclFromDecoratorProp(asgn) !== null; +} + +function findTightestNodeAtPosition(program: ts.Program, fileName: string, position: number) { + const sourceFile = program.getSourceFile(fileName); + if (sourceFile === undefined) { + return undefined; + } + + return findTightestNode(sourceFile, position); +} \ No newline at end of file diff --git a/packages/language-service/ivy/ts_utils.ts b/packages/language-service/ivy/ts_utils.ts index 4021407b03..ef52343337 100644 --- a/packages/language-service/ivy/ts_utils.ts +++ b/packages/language-service/ivy/ts_utils.ts @@ -27,4 +27,55 @@ export function getParentClassDeclaration(startNode: ts.Node): ts.ClassDeclarati startNode = startNode.parent; } return undefined; -} \ No newline at end of file +} + +/** + * Returns a property assignment from the assignment value if the property name + * matches the specified `key`, or `null` if there is no match. + */ +export function getPropertyAssignmentFromValue(value: ts.Node, key: string): ts.PropertyAssignment| + null { + const propAssignment = value.parent; + if (!propAssignment || !ts.isPropertyAssignment(propAssignment) || + propAssignment.name.getText() !== key) { + return null; + } + 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; +}