diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/comments.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/comments.ts index 7007b1a33c..7e114f4774 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/comments.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/comments.ts @@ -43,6 +43,7 @@ export enum CommentTriviaType { export enum ExpressionIdentifier { DIRECTIVE = 'DIR', COMPONENT_COMPLETION = 'COMPCOMP', + EVENT_PARAMETER = 'EP', } /** Tags the node with the given expression identifier. */ diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index 3db0a4eacf..1940aa5504 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -1837,6 +1837,7 @@ function tcbCreateEventHandler( /* name */ EVENT_PARAMETER, /* questionToken */ undefined, /* type */ eventParamType); + addExpressionIdentifier(eventParam, ExpressionIdentifier.EVENT_PARAMETER); return ts.createFunctionExpression( /* modifier */ undefined, diff --git a/packages/language-service/ivy/references.ts b/packages/language-service/ivy/references.ts index 605cd7b767..13a79ee1be 100644 --- a/packages/language-service/ivy/references.ts +++ b/packages/language-service/ivy/references.ts @@ -9,9 +9,11 @@ import {TmplAstBoundAttribute, TmplAstTextAttribute, TmplAstVariable} from '@ang import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core'; import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system'; import {DirectiveSymbol, SymbolKind, TemplateTypeChecker, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; +import {ExpressionIdentifier, hasExpressionIdentifier} from '@angular/compiler-cli/src/ngtsc/typecheck/src/comments'; import * as ts from 'typescript'; import {getTargetAtPosition} from './template_target'; +import {findTightestNode} from './ts_utils'; import {getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, isWithin, TemplateInfo, toTextSpan} from './utils'; export class ReferenceBuilder { @@ -135,7 +137,7 @@ export class ReferenceBuilder { const entries: ts.ReferenceEntry[] = []; for (const ref of refs) { if (this.ttc.isTrackedTypeCheckFile(absoluteFrom(ref.fileName))) { - const entry = convertToTemplateReferenceEntry(ref, this.ttc); + const entry = this.convertToTemplateReferenceEntry(ref, this.ttc); if (entry !== null) { entries.push(entry); } @@ -145,37 +147,50 @@ export class ReferenceBuilder { } return entries; } -} -function convertToTemplateReferenceEntry( - shimReferenceEntry: ts.ReferenceEntry, - templateTypeChecker: TemplateTypeChecker): ts.ReferenceEntry|null { - // TODO(atscott): Determine how to consistently resolve paths. i.e. with the project serverHost or - // LSParseConfigHost in the adapter. We should have a better defined way to normalize paths. - const mapping = templateTypeChecker.getTemplateMappingAtShimLocation({ - shimPath: absoluteFrom(shimReferenceEntry.fileName), - positionInShimFile: shimReferenceEntry.textSpan.start, - }); - if (mapping === null) { - return null; + private convertToTemplateReferenceEntry( + shimReferenceEntry: ts.ReferenceEntry, + templateTypeChecker: TemplateTypeChecker): ts.ReferenceEntry|null { + const sf = this.strategy.getProgram().getSourceFile(shimReferenceEntry.fileName); + if (sf === undefined) { + return null; + } + const tcbNode = findTightestNode(sf, shimReferenceEntry.textSpan.start); + if (tcbNode === undefined || + hasExpressionIdentifier(sf, tcbNode, ExpressionIdentifier.EVENT_PARAMETER)) { + // If the reference result is the $event parameter in the subscribe/addEventListener function + // in the TCB, we want to filter this result out of the references. We really only want to + // return references to the parameter in the template itself. + return null; + } + + // TODO(atscott): Determine how to consistently resolve paths. i.e. with the project serverHost + // or LSParseConfigHost in the adapter. We should have a better defined way to normalize paths. + const mapping = templateTypeChecker.getTemplateMappingAtShimLocation({ + shimPath: absoluteFrom(shimReferenceEntry.fileName), + positionInShimFile: shimReferenceEntry.textSpan.start, + }); + if (mapping === null) { + return null; + } + const {templateSourceMapping, span} = mapping; + + let templateUrl: AbsoluteFsPath; + if (templateSourceMapping.type === 'direct') { + templateUrl = absoluteFromSourceFile(templateSourceMapping.node.getSourceFile()); + } else if (templateSourceMapping.type === 'external') { + templateUrl = absoluteFrom(templateSourceMapping.templateUrl); + } else { + // This includes indirect mappings, which are difficult to map directly to the code location. + // Diagnostics similarly return a synthetic template string for this case rather than a real + // location. + return null; + } + + return { + ...shimReferenceEntry, + fileName: templateUrl, + textSpan: toTextSpan(span), + }; } - const {templateSourceMapping, span} = mapping; - - let templateUrl: AbsoluteFsPath; - if (templateSourceMapping.type === 'direct') { - templateUrl = absoluteFromSourceFile(templateSourceMapping.node.getSourceFile()); - } else if (templateSourceMapping.type === 'external') { - templateUrl = absoluteFrom(templateSourceMapping.templateUrl); - } else { - // This includes indirect mappings, which are difficult to map directly to the code location. - // Diagnostics similarly return a synthetic template string for this case rather than a real - // location. - return null; - } - - return { - ...shimReferenceEntry, - fileName: templateUrl, - textSpan: toTextSpan(span), - }; } diff --git a/packages/language-service/ivy/test/references_spec.ts b/packages/language-service/ivy/test/references_spec.ts index eff89b6d50..fa21130919 100644 --- a/packages/language-service/ivy/test/references_spec.ts +++ b/packages/language-service/ivy/test/references_spec.ts @@ -107,6 +107,22 @@ describe('find references', () => { assertTextSpans(refs, ['title']); }); + it('should work for $event in method call arguments', () => { + const {text, cursor} = extractCursorInfo(` + import {Component} from '@angular/core'; + + @Component({template: '
'}) + export class AppCmp { + setTitle(s: any) {} + }`); + const appFile = {name: _('/app.ts'), contents: text}; + env = createModuleWithDeclarations([appFile]); + const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; + expect(refs.length).toBe(1); + + assertTextSpans(refs, ['$event']); + }); + it('should work for property writes', () => { const appFile = { name: _('/app.ts'), diff --git a/packages/language-service/ivy/ts_utils.ts b/packages/language-service/ivy/ts_utils.ts index 4ebb791870..4021407b03 100644 --- a/packages/language-service/ivy/ts_utils.ts +++ b/packages/language-service/ivy/ts_utils.ts @@ -27,4 +27,4 @@ export function getParentClassDeclaration(startNode: ts.Node): ts.ClassDeclarati startNode = startNode.parent; } return undefined; -} +} \ No newline at end of file