refactor(compiler-cli): Return symbols for all matching outputs (#40144)

This commit ensures that the template type checker returns symbols for
all outputs if a template output listener binds to more than one.

PR Close #40144
This commit is contained in:
Andrew Scott 2020-12-15 17:40:28 -08:00 committed by atscott
parent da2be4b710
commit 989b4a94d4
2 changed files with 95 additions and 46 deletions

View File

@ -173,72 +173,71 @@ export class SymbolBuilder {
// * _t1.addEventListener(handler);
// Even with strict null checks disabled, we still produce the access as a separate statement
// so that it can be found here.
const outputFieldAccess = findFirstMatchingNode(
const outputFieldAccesses = findAllMatchingNodes(
this.typeCheckBlock, {withSpan: eventBinding.keySpan, filter: isAccessExpression});
if (outputFieldAccess === null) {
return null;
}
const consumer = this.templateData.boundTarget.getConsumerOfBinding(eventBinding);
if (consumer === null) {
return null;
}
if (consumer instanceof TmplAstTemplate || consumer instanceof TmplAstElement) {
if (!ts.isPropertyAccessExpression(outputFieldAccess) ||
outputFieldAccess.name.text !== 'addEventListener') {
return null;
const bindings: BindingSymbol[] = [];
for (const outputFieldAccess of outputFieldAccesses) {
const consumer = this.templateData.boundTarget.getConsumerOfBinding(eventBinding);
if (consumer === null) {
continue;
}
const addEventListener = outputFieldAccess.name;
const tsSymbol = this.getTypeChecker().getSymbolAtLocation(addEventListener);
const tsType = this.getTypeChecker().getTypeAtLocation(addEventListener);
const positionInShimFile = this.getShimPositionForNode(addEventListener);
const target = this.getSymbol(consumer);
if (consumer instanceof TmplAstTemplate || consumer instanceof TmplAstElement) {
if (!ts.isPropertyAccessExpression(outputFieldAccess) ||
outputFieldAccess.name.text !== 'addEventListener') {
continue;
}
if (target === null || tsSymbol === undefined) {
return null;
}
const addEventListener = outputFieldAccess.name;
const tsSymbol = this.getTypeChecker().getSymbolAtLocation(addEventListener);
const tsType = this.getTypeChecker().getTypeAtLocation(addEventListener);
const positionInShimFile = this.getShimPositionForNode(addEventListener);
const target = this.getSymbol(consumer);
return {
kind: SymbolKind.Output,
bindings: [{
if (target === null || tsSymbol === undefined) {
continue;
}
bindings.push({
kind: SymbolKind.Binding,
tsSymbol,
tsType,
target,
shimLocation: {shimPath: this.shimPath, positionInShimFile},
}],
};
} else {
if (!ts.isElementAccessExpression(outputFieldAccess)) {
return null;
}
const tsSymbol =
this.getTypeChecker().getSymbolAtLocation(outputFieldAccess.argumentExpression);
if (tsSymbol === undefined) {
return null;
}
});
} else {
if (!ts.isElementAccessExpression(outputFieldAccess)) {
continue;
}
const tsSymbol =
this.getTypeChecker().getSymbolAtLocation(outputFieldAccess.argumentExpression);
if (tsSymbol === undefined) {
continue;
}
const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess, consumer);
if (target === null) {
return null;
}
const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess, consumer);
if (target === null) {
continue;
}
const positionInShimFile = this.getShimPositionForNode(outputFieldAccess);
const tsType = this.getTypeChecker().getTypeAtLocation(outputFieldAccess);
return {
kind: SymbolKind.Output,
bindings: [{
const positionInShimFile = this.getShimPositionForNode(outputFieldAccess);
const tsType = this.getTypeChecker().getTypeAtLocation(outputFieldAccess);
bindings.push({
kind: SymbolKind.Binding,
tsSymbol,
tsType,
target,
shimLocation: {shimPath: this.shimPath, positionInShimFile},
}],
};
});
}
}
if (bindings.length === 0) {
return null;
}
return {kind: SymbolKind.Output, bindings};
}
private getSymbolOfInputBinding(binding: TmplAstBoundAttribute|

View File

@ -87,6 +87,56 @@ describe('definitions', () => {
assertFileNames([def, def2], ['dir2.ts', 'dir.ts']);
});
it('gets definitions for all outputs when attribute matches more than one', () => {
initMockFileSystem('Native');
const {cursor, text} = extractCursorInfo('<div dir (someEv¦ent)="doSomething()"></div>');
const templateFile = {contents: text, name: absoluteFrom('/app.html')};
const dirFile = {
name: absoluteFrom('/dir.ts'),
contents: `
import {Directive, Output, EventEmitter} from '@angular/core';
@Directive({selector: '[dir]'})
export class MyDir {
@Output() someEvent = new EventEmitter<void>();
}`,
};
const dirFile2 = {
name: absoluteFrom('/dir2.ts'),
contents: `
import {Directive, Output, EventEmitter} from '@angular/core';
@Directive({selector: '[dir]'})
export class MyDir2 {
@Output() someEvent = new EventEmitter<void>();
}`,
};
const appFile = {
name: absoluteFrom('/app.ts'),
contents: `
import {Component, NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
@Component({templateUrl: 'app.html'})
export class AppCmp {
doSomething() {}
}
`
};
const env = createModuleWithDeclarations([appFile, dirFile, dirFile2], [templateFile]);
const {textSpan, definitions} =
getDefinitionsAndAssertBoundSpan(env, absoluteFrom('/app.html'), cursor);
expect(text.substr(textSpan.start, textSpan.length)).toEqual('someEvent');
expect(definitions.length).toEqual(2);
const [def, def2] = definitions;
expect(def.textSpan).toContain('someEvent');
expect(def2.textSpan).toContain('someEvent');
// TODO(atscott): investigate why the text span includes more than just 'someEvent'
// assertTextSpans([def, def2], ['someEvent']);
assertFileNames([def, def2], ['dir2.ts', 'dir.ts']);
});
function getDefinitionsAndAssertBoundSpan(
env: LanguageServiceTestEnvironment, fileName: AbsoluteFsPath, cursor: number) {
env.expectNoSourceDiagnostics();