fix(language-service): 'go to defininition' for objects defined in template (#42559)

Previously, the "go to definition" action did no account for the
possibility that something may actually be defined in a template. This
change updates the logic in the definition builder to convert any
results that are locations in template typecheck files to their
corresponding locations in the template.

PR Close #42559
This commit is contained in:
Andrew Scott 2021-06-11 13:05:42 -07:00 committed by Alex Rickabaugh
parent 228beeabd1
commit 4001e9d808
3 changed files with 66 additions and 4 deletions

View File

@ -8,10 +8,13 @@
import {AST, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstTemplate, TmplAstTextAttribute} from '@angular/compiler';
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
import {isExternalResource} from '@angular/compiler-cli/src/ngtsc/metadata';
import {ProgramDriver} from '@angular/compiler-cli/src/ngtsc/program_driver';
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ShimLocation, Symbol, SymbolKind, TemplateSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
import * as ts from 'typescript';
import {convertToTemplateDocumentSpan} from './references_and_rename_utils';
import {getTargetAtPosition, TargetNodeKind} from './template_target';
import {findTightestNode, getParentClassDeclaration} from './ts_utils';
import {flatMap, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, getTemplateLocationFromShimLocation, getTextSpanOfNode, isDollarEvent, isTypeScriptFile, TemplateInfo, toTextSpan} from './utils';
@ -27,7 +30,11 @@ interface HasShimLocation {
}
export class DefinitionBuilder {
constructor(private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler) {}
private readonly ttc = this.compiler.getTemplateTypeChecker();
constructor(
private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler,
private readonly driver: ProgramDriver) {}
getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan
|undefined {
@ -132,10 +139,36 @@ export class DefinitionBuilder {
private getDefinitionsForSymbols(...symbols: HasShimLocation[]): ts.DefinitionInfo[] {
return flatMap(symbols, ({shimLocation}) => {
const {shimPath, positionInShimFile} = shimLocation;
return this.tsLS.getDefinitionAtPosition(shimPath, positionInShimFile) ?? [];
const definitionInfos = this.tsLS.getDefinitionAtPosition(shimPath, positionInShimFile);
if (definitionInfos === undefined) {
return [];
}
return this.mapShimResultsToTemplates(definitionInfos);
});
}
/**
* Converts and definition info result that points to a template typecheck file to a reference to
* the corresponding location in the template.
*/
private mapShimResultsToTemplates(definitionInfos: readonly ts.DefinitionInfo[]):
readonly ts.DefinitionInfo[] {
const result: ts.DefinitionInfo[] = [];
for (const info of definitionInfos) {
if (this.ttc.isTrackedTypeCheckFile(absoluteFrom(info.fileName))) {
const templateDefinitionInfo =
convertToTemplateDocumentSpan(info, this.ttc, this.driver.getProgram());
if (templateDefinitionInfo === null) {
continue;
}
result.push(templateDefinitionInfo);
} else {
result.push(info);
}
}
return result;
}
getTypeDefinitionsAtPosition(fileName: string, position: number):
readonly ts.DefinitionInfo[]|undefined {
const templateInfo = getTemplateInfoAtPosition(fileName, position, this.compiler);

View File

@ -118,7 +118,7 @@ export class LanguageService {
if (!isInAngularContext(compiler.getCurrentProgram(), fileName, position)) {
return undefined;
}
return new DefinitionBuilder(this.tsLS, compiler)
return new DefinitionBuilder(this.tsLS, compiler, this.programDriver)
.getDefinitionAndBoundSpan(fileName, position);
});
}
@ -129,7 +129,7 @@ export class LanguageService {
if (!isTemplateContext(compiler.getCurrentProgram(), fileName, position)) {
return undefined;
}
return new DefinitionBuilder(this.tsLS, compiler)
return new DefinitionBuilder(this.tsLS, compiler, this.programDriver)
.getTypeDefinitionsAtPosition(fileName, position);
});
}

View File

@ -176,6 +176,35 @@ describe('definitions', () => {
assertFileNames(definitions, ['style.scss']);
});
it('gets definition for property of variable declared in template', () => {
initMockFileSystem('Native');
const files = {
'app.html': `
<ng-container *ngIf="{prop: myVal} as myVar">
{{myVar.prop.name}}
</ng-container>
`,
'app.ts': `
import {Component} from '@angular/core';
@Component({templateUrl: '/app.html'})
export class AppCmp {
myVal = {name: 'Andrew'};
}
`,
};
const env = LanguageServiceTestEnv.setup();
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
const template = project.openFile('app.html');
project.expectNoSourceDiagnostics();
template.moveCursorToText('{{myVar.pro¦p.name}}');
const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
expect(definitions![0].name).toEqual('"prop"');
assertFileNames(Array.from(definitions!), ['app.html']);
});
function getDefinitionsAndAssertBoundSpan(env: LanguageServiceTestEnv, file: OpenBuffer) {
env.expectNoSourceDiagnostics();
const definitionAndBoundSpan = file.getDefinitionAndBoundSpan();