feat(language-service): support multiple symbol definitions (#34782)

In Angular, symbol can have multiple definitions (e.g. a two-way
binding). This commit adds support for for multiple definitions for a
queried location in a template.

PR Close #34782
This commit is contained in:
Ayaz Hafiz
2020-01-14 18:54:40 -08:00
committed by Andrew Kushnir
parent 48f8ca5483
commit 1ea04ffc05
5 changed files with 199 additions and 154 deletions

View File

@ -14,6 +14,7 @@ import {TypeScriptServiceHost} from '../src/typescript_host';
import {MockTypescriptHost} from './test_utils';
const TEST_TEMPLATE = '/app/test.ng';
const PARSING_CASES = '/app/parsing-cases.ts';
describe('definitions', () => {
const mockHost = new MockTypescriptHost(['/app/main.ts']);
@ -49,28 +50,29 @@ describe('definitions', () => {
});
it('should be able to find a field in a attribute reference', () => {
const fileName = mockHost.addCode(`
@Component({
template: '<input [(ngModel)]="«name»">'
})
export class MyComponent {
«ᐱnameᐱ: string;»
}`);
mockHost.override(TEST_TEMPLATE, `<input [(ngModel)]="«title»">`);
const marker = mockHost.getReferenceMarkerFor(fileName, 'name');
const result = ngService.getDefinitionAndBoundSpan(fileName, marker.start);
const marker = mockHost.getReferenceMarkerFor(TEST_TEMPLATE, 'title');
const result = ngService.getDefinitionAndBoundSpan(TEST_TEMPLATE, marker.start);
expect(result).toBeDefined();
const {textSpan, definitions} = result !;
expect(textSpan).toEqual(marker);
expect(definitions).toBeDefined();
expect(definitions !.length).toBe(1);
const def = definitions ![0];
expect(def.fileName).toBe(fileName);
expect(def.name).toBe('name');
expect(def.kind).toBe('property');
expect(def.textSpan).toEqual(mockHost.getDefinitionMarkerFor(fileName, 'name'));
// There are exactly two, indentical definitions here, corresponding to the "name" on the
// property and event bindings of the two-way binding. The two-way binding is effectively
// syntactic sugar for `[ngModel]="name" (ngModel)="name=$event"`.
expect(definitions !.length).toBe(2);
for (const def of definitions !) {
expect(def.fileName).toBe(PARSING_CASES);
expect(def.name).toBe('title');
expect(def.kind).toBe('property');
const fileContent = mockHost.readFile(def.fileName);
expect(fileContent !.substring(def.textSpan.start, def.textSpan.start + def.textSpan.length))
.toEqual(`title = 'Some title';`);
}
});
it('should be able to find a method from a call', () => {
@ -304,18 +306,24 @@ describe('definitions', () => {
const boundedText = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'my');
expect(textSpan).toEqual(boundedText);
// There should be exactly 1 definition
expect(definitions).toBeDefined();
expect(definitions !.length).toBe(1);
const def = definitions ![0];
expect(definitions !.length).toBe(2);
const [def1, def2] = definitions !;
const refFileName = '/app/parsing-cases.ts';
expect(def.fileName).toBe(refFileName);
expect(def.name).toBe('model');
expect(def.kind).toBe('property');
const content = mockHost.readFile(refFileName) !;
expect(content.substring(def.textSpan.start, def.textSpan.start + def.textSpan.length))
expect(def1.fileName).toBe(refFileName);
expect(def1.name).toBe('model');
expect(def1.kind).toBe('property');
let content = mockHost.readFile(refFileName) !;
expect(content.substring(def1.textSpan.start, def1.textSpan.start + def1.textSpan.length))
.toEqual(`@Input() model: string = 'model';`);
expect(def2.fileName).toBe(refFileName);
expect(def2.name).toBe('modelChange');
expect(def2.kind).toBe('event');
content = mockHost.readFile(refFileName) !;
expect(content.substring(def2.textSpan.start, def2.textSpan.start + def2.textSpan.length))
.toEqual(`@Output() modelChange: EventEmitter<string> = new EventEmitter();`);
});
it('should be able to find a template from a url', () => {