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
This commit is contained in:
Andrew Scott 2021-02-22 11:12:09 -08:00
parent b84f719747
commit f31a6015a0
2 changed files with 145 additions and 32 deletions

View File

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

View File

@ -27,4 +27,55 @@ export function getParentClassDeclaration(startNode: ts.Node): ts.ClassDeclarati
startNode = startNode.parent;
}
return undefined;
}
}
/**
* 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: '<div></div>'
* ^^^^^^^^^^^^^^^^^^^^^^^---- 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;
}