Given the template `<div (click)="doSomething($event)"></div>` If you request references for the `$event`, the results include both `$event` and `(click)="doSomething($event)"`. This happens because in the TCB, `$event` is passed to the `subscribe`/`addEventListener` function as an argument. So when we ask typescript to give us the references, we get the result from the usage in the subscribe body as well as the one passed in as an argument. This commit adds an identifier to the `$event` parameter in the TCB so that the result returned from `getReferencesAtPosition` can be identified and filtered out. fixes #40157 PR Close #40158
197 lines
8.5 KiB
TypeScript
197 lines
8.5 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
import {TmplAstBoundAttribute, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
|
|
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 {
|
|
private readonly ttc = this.compiler.getTemplateTypeChecker();
|
|
|
|
constructor(
|
|
private readonly strategy: TypeCheckingProgramStrategy,
|
|
private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler) {}
|
|
|
|
get(filePath: string, position: number): ts.ReferenceEntry[]|undefined {
|
|
this.ttc.generateAllTypeCheckBlocks();
|
|
const templateInfo = getTemplateInfoAtPosition(filePath, position, this.compiler);
|
|
return templateInfo !== undefined ?
|
|
this.getReferencesAtTemplatePosition(templateInfo, position) :
|
|
this.getReferencesAtTypescriptPosition(filePath, position);
|
|
}
|
|
|
|
private getReferencesAtTemplatePosition({template, component}: TemplateInfo, position: number):
|
|
ts.ReferenceEntry[]|undefined {
|
|
// Find the AST node in the template at the position.
|
|
const positionDetails = getTargetAtPosition(template, position);
|
|
if (positionDetails === null) {
|
|
return undefined;
|
|
}
|
|
|
|
const node = positionDetails.nodeInContext.node;
|
|
|
|
// Get the information about the TCB at the template position.
|
|
const symbol = this.ttc.getSymbolOfNode(node, component);
|
|
if (symbol === null) {
|
|
return undefined;
|
|
}
|
|
switch (symbol.kind) {
|
|
case SymbolKind.Directive:
|
|
case SymbolKind.Template:
|
|
// References to elements, templates, and directives will be through template references
|
|
// (#ref). They shouldn't be used directly for a Language Service reference request.
|
|
return undefined;
|
|
case SymbolKind.Element: {
|
|
const matches = getDirectiveMatchesForElementTag(symbol.templateNode, symbol.directives);
|
|
return this.getReferencesForDirectives(matches);
|
|
}
|
|
case SymbolKind.DomBinding: {
|
|
// Dom bindings aren't currently type-checked (see `checkTypeOfDomBindings`) so they don't
|
|
// have a shim location. This means we can't match dom bindings to their lib.dom reference,
|
|
// but we can still see if they match to a directive.
|
|
if (!(node instanceof TmplAstTextAttribute) && !(node instanceof TmplAstBoundAttribute)) {
|
|
return undefined;
|
|
}
|
|
const directives = getDirectiveMatchesForAttribute(
|
|
node.name, symbol.host.templateNode, symbol.host.directives);
|
|
return this.getReferencesForDirectives(directives);
|
|
}
|
|
case SymbolKind.Reference: {
|
|
const {shimPath, positionInShimFile} = symbol.referenceVarLocation;
|
|
return this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile);
|
|
}
|
|
case SymbolKind.Variable: {
|
|
const {positionInShimFile: initializerPosition, shimPath} = symbol.initializerLocation;
|
|
const localVarPosition = symbol.localVarLocation.positionInShimFile;
|
|
const templateNode = positionDetails.nodeInContext.node;
|
|
|
|
if ((templateNode instanceof TmplAstVariable)) {
|
|
if (templateNode.valueSpan !== undefined && isWithin(position, templateNode.valueSpan)) {
|
|
// In the valueSpan of the variable, we want to get the reference of the initializer.
|
|
return this.getReferencesAtTypescriptPosition(shimPath, initializerPosition);
|
|
} else if (isWithin(position, templateNode.keySpan)) {
|
|
// In the keySpan of the variable, we want to get the reference of the local variable.
|
|
return this.getReferencesAtTypescriptPosition(shimPath, localVarPosition);
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
// If the templateNode is not the `TmplAstVariable`, it must be a usage of the variable
|
|
// somewhere in the template.
|
|
return this.getReferencesAtTypescriptPosition(shimPath, localVarPosition);
|
|
}
|
|
case SymbolKind.Input:
|
|
case SymbolKind.Output: {
|
|
// TODO(atscott): Determine how to handle when the binding maps to several inputs/outputs
|
|
const {shimPath, positionInShimFile} = symbol.bindings[0].shimLocation;
|
|
return this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile);
|
|
}
|
|
case SymbolKind.Pipe:
|
|
case SymbolKind.Expression: {
|
|
const {shimPath, positionInShimFile} = symbol.shimLocation;
|
|
return this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
private getReferencesForDirectives(directives: Set<DirectiveSymbol>):
|
|
ts.ReferenceEntry[]|undefined {
|
|
const allDirectiveRefs: ts.ReferenceEntry[] = [];
|
|
for (const dir of directives.values()) {
|
|
const dirClass = dir.tsSymbol.valueDeclaration;
|
|
if (dirClass === undefined || !ts.isClassDeclaration(dirClass) ||
|
|
dirClass.name === undefined) {
|
|
continue;
|
|
}
|
|
|
|
const dirFile = dirClass.getSourceFile().fileName;
|
|
const dirPosition = dirClass.name.getStart();
|
|
const directiveRefs = this.getReferencesAtTypescriptPosition(dirFile, dirPosition);
|
|
if (directiveRefs !== undefined) {
|
|
allDirectiveRefs.push(...directiveRefs);
|
|
}
|
|
}
|
|
|
|
return allDirectiveRefs.length > 0 ? allDirectiveRefs : undefined;
|
|
}
|
|
|
|
private getReferencesAtTypescriptPosition(fileName: string, position: number):
|
|
ts.ReferenceEntry[]|undefined {
|
|
const refs = this.tsLS.getReferencesAtPosition(fileName, position);
|
|
if (refs === undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
const entries: ts.ReferenceEntry[] = [];
|
|
for (const ref of refs) {
|
|
if (this.ttc.isTrackedTypeCheckFile(absoluteFrom(ref.fileName))) {
|
|
const entry = this.convertToTemplateReferenceEntry(ref, this.ttc);
|
|
if (entry !== null) {
|
|
entries.push(entry);
|
|
}
|
|
} else {
|
|
entries.push(ref);
|
|
}
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
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),
|
|
};
|
|
}
|
|
}
|