diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts index 9dc4a15c1e..d3fcc2f5c4 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts @@ -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| diff --git a/packages/language-service/ivy/test/definitions_spec.ts b/packages/language-service/ivy/test/definitions_spec.ts index 9700e16124..a1fcef7a11 100644 --- a/packages/language-service/ivy/test/definitions_spec.ts +++ b/packages/language-service/ivy/test/definitions_spec.ts @@ -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('
'); + 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(); + }`, + }; + const dirFile2 = { + name: absoluteFrom('/dir2.ts'), + contents: ` + import {Directive, Output, EventEmitter} from '@angular/core'; + + @Directive({selector: '[dir]'}) + export class MyDir2 { + @Output() someEvent = new EventEmitter(); + }`, + }; + 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();