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 bf3df64c20..2cd6d2c746 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 @@ -176,41 +176,67 @@ export class SymbolBuilder { } const consumer = this.templateData.boundTarget.getConsumerOfBinding(eventBinding); - if (consumer === null || consumer instanceof TmplAstTemplate || - consumer instanceof TmplAstElement) { - // Bindings to element or template events produce `addEventListener` which - // we cannot get the field for. - return null; - } - const outputFieldAccess = TcbDirectiveOutputsOp.decodeOutputCallExpression(node); - if (outputFieldAccess === null) { + if (consumer === null) { return null; } - const tsSymbol = - this.getTypeChecker().getSymbolAtLocation(outputFieldAccess.argumentExpression); - if (tsSymbol === undefined) { - return null; + if (consumer instanceof TmplAstTemplate || consumer instanceof TmplAstElement) { + if (!ts.isPropertyAccessExpression(node.expression) || + node.expression.name.text !== 'addEventListener') { + return null; + } + + const addEventListener = node.expression.name; + const tsSymbol = this.getTypeChecker().getSymbolAtLocation(addEventListener); + const tsType = this.getTypeChecker().getTypeAtLocation(addEventListener); + const positionInShimFile = this.getShimPositionForNode(addEventListener); + const target = this.getSymbol(consumer); + + if (target === null || tsSymbol === undefined) { + return null; + } + + return { + kind: SymbolKind.Output, + bindings: [{ + kind: SymbolKind.Binding, + tsSymbol, + tsType, + target, + shimLocation: {shimPath: this.shimPath, positionInShimFile}, + }], + }; + } else { + const outputFieldAccess = TcbDirectiveOutputsOp.decodeOutputCallExpression(node); + if (outputFieldAccess === null) { + return null; + } + + const tsSymbol = + this.getTypeChecker().getSymbolAtLocation(outputFieldAccess.argumentExpression); + if (tsSymbol === undefined) { + return null; + } + + + const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess, consumer); + if (target === null) { + return null; + } + + const positionInShimFile = this.getShimPositionForNode(outputFieldAccess); + const tsType = this.getTypeChecker().getTypeAtLocation(node); + return { + kind: SymbolKind.Output, + bindings: [{ + kind: SymbolKind.Binding, + tsSymbol, + tsType, + target, + shimLocation: {shimPath: this.shimPath, positionInShimFile}, + }], + }; } - - - const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess, consumer); - if (target === null) { - return null; - } - - const positionInShimFile = this.getShimPositionForNode(outputFieldAccess); - const tsType = this.getTypeChecker().getTypeAtLocation(node); - return { - kind: SymbolKind.Output, - bindings: [{ - kind: SymbolKind.Binding, - tsSymbol, - tsType, - target, - shimLocation: {shimPath: this.shimPath, positionInShimFile}, - }], - }; } private getSymbolOfInputBinding(binding: TmplAstBoundAttribute| diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts index 464282fae3..d39489714a 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts @@ -1247,38 +1247,29 @@ runInEachFileSystem(() => { .toEqual('TestDir'); }); - it('returns empty list when binding does not match any directive output', () => { - const fileName = absoluteFrom('/main.ts'); - const dirFile = absoluteFrom('/dir.ts'); - const {program, templateTypeChecker} = setup([ - { - fileName, - templates: {'Cmp': `
`}, - declarations: [ - { - name: 'TestDir', - selector: '[dir]', - file: dirFile, - type: 'directive', - outputs: {outputA: 'outputA'}, - }, - ] - }, - { - fileName: dirFile, - source: `export class TestDir {outputA!: EventEmitter;}`, - templates: {}, - } - ]); - const sf = getSourceFileOrError(program, fileName); - const cmp = getClass(sf, 'Cmp'); + it('returns addEventListener binding to native element when no match to any directive output', + () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: {'Cmp': `
`}, + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const cmp = getClass(sf, 'Cmp'); - const nodes = templateTypeChecker.getTemplate(cmp)!; + const nodes = templateTypeChecker.getTemplate(cmp)!; - const outputABinding = (nodes[0] as TmplAstElement).outputs[0]; - const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp); - expect(symbol).toBeNull(); - }); + const outputABinding = (nodes[0] as TmplAstElement).outputs[0]; + const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp)!; + assertOutputBindingSymbol(symbol); + expect(program.getTypeChecker().symbolToString(symbol.bindings[0].tsSymbol!)) + .toEqual('addEventListener'); + + const eventSymbol = templateTypeChecker.getSymbolOfNode(outputABinding.handler, cmp)!; + assertExpressionSymbol(eventSymbol); + }); it('returns empty list when checkTypeOfOutputEvents is false', () => { const fileName = absoluteFrom('/main.ts'); diff --git a/packages/language-service/ivy/test/legacy/definitions_spec.ts b/packages/language-service/ivy/test/legacy/definitions_spec.ts index 58bfa993df..67a4953419 100644 --- a/packages/language-service/ivy/test/legacy/definitions_spec.ts +++ b/packages/language-service/ivy/test/legacy/definitions_spec.ts @@ -228,6 +228,18 @@ describe('definitions', () => { expect(directiveDef.textSpan).toEqual('EventSelectorDirective'); expect(directiveDef.contextSpan).toContain('export class EventSelectorDirective'); }); + + it('should work for $event from native element', () => { + const definitions = getDefinitionsAndAssertBoundSpan({ + templateOverride: `
`, + expectedSpanText: 'click', + }); + expect(definitions!.length).toEqual(1); + expect(definitions[0].textSpan).toEqual('addEventListener'); + expect(definitions[0].contextSpan) + .toContain('addEventListener'); + expect(definitions[0].fileName).toContain('lib.dom.d.ts'); + }); }); }); diff --git a/packages/language-service/ivy/test/legacy/type_definitions_spec.ts b/packages/language-service/ivy/test/legacy/type_definitions_spec.ts index 1121460873..7b1c234469 100644 --- a/packages/language-service/ivy/test/legacy/type_definitions_spec.ts +++ b/packages/language-service/ivy/test/legacy/type_definitions_spec.ts @@ -32,7 +32,7 @@ describe('type definitions', () => { describe('elements', () => { it('should work for native elements', () => { - const defs = getTypeDefinitionsAndAssertBoundSpan({ + const defs = getTypeDefinitions({ templateOverride: ``, }); expect(defs.length).toEqual(2); @@ -42,7 +42,7 @@ describe('type definitions', () => { }); it('should return directives which match the element tag', () => { - const defs = getTypeDefinitionsAndAssertBoundSpan({ + const defs = getTypeDefinitions({ templateOverride: ``, }); expect(defs.length).toEqual(3); @@ -63,7 +63,7 @@ describe('type definitions', () => { describe('directives', () => { it('should work for directives', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: `
`, }); expect(definitions.length).toEqual(1); @@ -73,7 +73,7 @@ describe('type definitions', () => { }); it('should work for components', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: ``, }); expect(definitions.length).toEqual(1); @@ -82,7 +82,7 @@ describe('type definitions', () => { }); it('should work for structural directives', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: `
`, }); expect(definitions.length).toEqual(1); @@ -94,12 +94,12 @@ describe('type definitions', () => { }); it('should work for directives with compound selectors', () => { - let defs = getTypeDefinitionsAndAssertBoundSpan({ + let defs = getTypeDefinitions({ templateOverride: ``, }); expect(defs.length).toEqual(1); expect(defs[0].contextSpan).toContain('export class CompoundCustomButtonDirective'); - defs = getTypeDefinitionsAndAssertBoundSpan({ + defs = getTypeDefinitions({ templateOverride: ``, }); expect(defs.length).toEqual(1); @@ -110,7 +110,7 @@ describe('type definitions', () => { describe('bindings', () => { describe('inputs', () => { it('should return something for input providers with non-primitive types', () => { - const defs = getTypeDefinitionsAndAssertBoundSpan({ + const defs = getTypeDefinitions({ templateOverride: ``, }); expect(defs.length).toEqual(1); @@ -118,7 +118,7 @@ describe('type definitions', () => { }); it('should work for structural directive inputs ngForTrackBy', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: `
`, }); expect(definitions!.length).toEqual(1); @@ -129,7 +129,7 @@ describe('type definitions', () => { }); it('should work for structural directive inputs ngForOf', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: `
`, }); // In addition to all the array defs, this will also return the NgForOf def because the @@ -140,7 +140,7 @@ describe('type definitions', () => { }); it('should return nothing for two-way binding providers', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: ``, }); // TODO(atscott): This should actually return EventEmitter type but we only match the input @@ -151,7 +151,7 @@ describe('type definitions', () => { describe('outputs', () => { it('should work for event providers', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: ``, }); expect(definitions!.length).toEqual(2); @@ -167,7 +167,7 @@ describe('type definitions', () => { }); it('should return the directive when the event is part of the selector', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: `
`, }); expect(definitions!.length).toEqual(3); @@ -176,12 +176,24 @@ describe('type definitions', () => { const directiveDef = definitions[2]; expect(directiveDef.contextSpan).toContain('export class EventSelectorDirective'); }); + + it('should work for native event outputs', () => { + const definitions = getTypeDefinitions({ + templateOverride: `
`, + }); + expect(definitions!.length).toEqual(1); + expect(definitions[0].textSpan).toEqual('addEventListener'); + expect(definitions[0].contextSpan) + .toEqual( + 'addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;'); + expect(definitions[0].fileName).toContain('lib.dom.d.ts'); + }); }); }); describe('references', () => { it('should work for element references', () => { - const defs = getTypeDefinitionsAndAssertBoundSpan({ + const defs = getTypeDefinitions({ templateOverride: `
{{char¦t}}`, }); expect(defs.length).toEqual(2); @@ -190,7 +202,7 @@ describe('type definitions', () => { }); it('should work for directive references', () => { - const defs = getTypeDefinitionsAndAssertBoundSpan({ + const defs = getTypeDefinitions({ templateOverride: `
`, }); expect(defs.length).toEqual(1); @@ -201,7 +213,7 @@ describe('type definitions', () => { describe('variables', () => { it('should work for array members', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: `
{{her¦o}}
`, }); expect(definitions!.length).toEqual(1); @@ -214,7 +226,7 @@ describe('type definitions', () => { describe('pipes', () => { it('should work for pipes', () => { const templateOverride = `

The hero's birthday is {{birthday | da¦te: "MM/dd/yy"}}

`; - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride, }); expect(definitions!.length).toEqual(1); @@ -227,7 +239,7 @@ describe('type definitions', () => { describe('expressions', () => { it('should return nothing for primitives', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: `
{{ tit¦le }}
`, }); expect(definitions!.length).toEqual(0); @@ -236,7 +248,7 @@ describe('type definitions', () => { // TODO(atscott): Investigate why this returns nothing in the test environment. This actually // works in the extension. xit('should work for functions on primitives', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: `
{{ title.toLower¦case() }}
`, }); expect(definitions!.length).toEqual(1); @@ -245,7 +257,7 @@ describe('type definitions', () => { }); it('should work for accessed property reads', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: `
{{heroes[0].addre¦ss}}
`, }); expect(definitions!.length).toEqual(1); @@ -256,7 +268,7 @@ describe('type definitions', () => { }); it('should work for $event', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: ``, }); expect(definitions!.length).toEqual(2); @@ -269,7 +281,7 @@ describe('type definitions', () => { }); it('should work for method calls', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: `
`, }); expect(definitions!.length).toEqual(1); @@ -280,7 +292,7 @@ describe('type definitions', () => { }); it('should work for accessed properties in writes', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: `
`, }); expect(definitions!.length).toEqual(1); @@ -291,21 +303,21 @@ describe('type definitions', () => { }); it('should work for variables in structural directives', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: `
`, }); expectAllDefinitions(definitions, new Set(['Array']), possibleArrayDefFiles); }); it('should work for uses of members in structural directives', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: `
{{her¦oes2}}
`, }); expectAllDefinitions(definitions, new Set(['Array']), possibleArrayDefFiles); }); it('should work for members in structural directives', () => { - const definitions = getTypeDefinitionsAndAssertBoundSpan({ + const definitions = getTypeDefinitions({ templateOverride: `
`, }); expectAllDefinitions(definitions, new Set(['Array']), possibleArrayDefFiles); @@ -319,7 +331,7 @@ describe('type definitions', () => { }); }); - function getTypeDefinitionsAndAssertBoundSpan({templateOverride}: {templateOverride: string}): + function getTypeDefinitions({templateOverride}: {templateOverride: string}): HumanizedDefinitionInfo[] { const {position} = service.overwriteInlineTemplate(APP_COMPONENT, templateOverride); const defs = ngLS.getTypeDefinitionAtPosition(APP_COMPONENT, position); diff --git a/packages/language-service/ivy/test/quick_info_spec.ts b/packages/language-service/ivy/test/quick_info_spec.ts index 45bc6b3eda..0a863ecd09 100644 --- a/packages/language-service/ivy/test/quick_info_spec.ts +++ b/packages/language-service/ivy/test/quick_info_spec.ts @@ -309,6 +309,25 @@ describe('quick info', () => { expectedDisplayString: '(reference) chart: HTMLDivElement' }); }); + + it('should work for $event from native element', () => { + expectQuickInfo({ + templateOverride: `
`, + expectedSpanText: '$event', + expectedDisplayString: '(parameter) $event: MouseEvent' + }); + }); + + it('should work for click output from native element', () => { + expectQuickInfo({ + templateOverride: `
`, + expectedSpanText: 'click', + expectedDisplayString: + '(event) HTMLDivElement.addEventListener<"click">(type: "click", ' + + 'listener: (this: HTMLDivElement, ev: MouseEvent) => any, ' + + 'options?: boolean | AddEventListenerOptions | undefined): void (+1 overload)' + }); + }); }); describe('variables', () => {