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

View File

@ -87,6 +87,56 @@ describe('definitions', () => {
assertFileNames([def, def2], ['dir2.ts', 'dir.ts']); 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( function getDefinitionsAndAssertBoundSpan(
env: LanguageServiceTestEnvironment, fileName: AbsoluteFsPath, cursor: number) { env: LanguageServiceTestEnvironment, fileName: AbsoluteFsPath, cursor: number) {
env.expectNoSourceDiagnostics(); env.expectNoSourceDiagnostics();