417 lines
16 KiB
TypeScript
417 lines
16 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 {AST, TmplAstNode} from '@angular/compiler';
|
|
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
|
import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
|
|
import {MetaType, PipeMeta} from '@angular/compiler-cli/src/ngtsc/metadata';
|
|
import {PerfPhase} from '@angular/compiler-cli/src/ngtsc/perf';
|
|
import {ProgramDriver} from '@angular/compiler-cli/src/ngtsc/program_driver';
|
|
import {SymbolKind} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
|
import * as ts from 'typescript';
|
|
|
|
import {convertToTemplateDocumentSpan, createLocationKey, FilePosition, getParentClassMeta, getRenameTextAndSpanAtPosition, getTargetDetailsAtTemplatePosition, TemplateLocationDetails} from './references_and_rename_utils';
|
|
import {collectMemberMethods, findTightestNode} from './ts_utils';
|
|
import {getTemplateInfoAtPosition, TemplateInfo} from './utils';
|
|
|
|
export class ReferencesBuilder {
|
|
private readonly ttc = this.compiler.getTemplateTypeChecker();
|
|
|
|
constructor(
|
|
private readonly driver: ProgramDriver, private readonly tsLS: ts.LanguageService,
|
|
private readonly compiler: NgCompiler) {}
|
|
|
|
getReferencesAtPosition(filePath: string, position: number): ts.ReferenceEntry[]|undefined {
|
|
this.ttc.generateAllTypeCheckBlocks();
|
|
const templateInfo = getTemplateInfoAtPosition(filePath, position, this.compiler);
|
|
if (templateInfo === undefined) {
|
|
return this.getReferencesAtTypescriptPosition(filePath, position);
|
|
}
|
|
return this.getReferencesAtTemplatePosition(templateInfo, position);
|
|
}
|
|
|
|
private getReferencesAtTemplatePosition(templateInfo: TemplateInfo, position: number):
|
|
ts.ReferenceEntry[]|undefined {
|
|
const allTargetDetails = getTargetDetailsAtTemplatePosition(templateInfo, position, this.ttc);
|
|
if (allTargetDetails === null) {
|
|
return undefined;
|
|
}
|
|
const allReferences: ts.ReferenceEntry[] = [];
|
|
for (const targetDetails of allTargetDetails) {
|
|
for (const location of targetDetails.typescriptLocations) {
|
|
const refs = this.getReferencesAtTypescriptPosition(location.fileName, location.position);
|
|
if (refs !== undefined) {
|
|
allReferences.push(...refs);
|
|
}
|
|
}
|
|
}
|
|
return allReferences.length > 0 ? allReferences : 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 = convertToTemplateDocumentSpan(ref, this.ttc, this.driver.getProgram());
|
|
if (entry !== null) {
|
|
entries.push(entry);
|
|
}
|
|
} else {
|
|
entries.push(ref);
|
|
}
|
|
}
|
|
return entries;
|
|
}
|
|
}
|
|
|
|
enum RequestKind {
|
|
DirectFromTemplate,
|
|
DirectFromTypeScript,
|
|
PipeName,
|
|
Selector,
|
|
}
|
|
|
|
/** The context needed to perform a rename of a pipe name. */
|
|
interface PipeRenameContext {
|
|
type: RequestKind.PipeName;
|
|
|
|
/** The string literal for the pipe name that appears in the @Pipe meta */
|
|
pipeNameExpr: ts.StringLiteral;
|
|
|
|
/**
|
|
* The location to use for querying the native TS LS for rename positions. This will be the
|
|
* pipe's transform method.
|
|
*/
|
|
renamePosition: FilePosition;
|
|
}
|
|
|
|
/** The context needed to perform a rename of a directive/component selector. */
|
|
interface SelectorRenameContext {
|
|
type: RequestKind.Selector;
|
|
|
|
/** The string literal that appears in the directive/component metadata. */
|
|
selectorExpr: ts.StringLiteral;
|
|
|
|
/**
|
|
* The location to use for querying the native TS LS for rename positions. This will be the
|
|
* component/directive class itself. Doing so will allow us to find the location of the
|
|
* directive/component instantiations, which map to template elements.
|
|
*/
|
|
renamePosition: FilePosition;
|
|
}
|
|
|
|
interface DirectFromTypescriptRenameContext {
|
|
type: RequestKind.DirectFromTypeScript;
|
|
|
|
/** The node that is being renamed. */
|
|
requestNode: ts.Node;
|
|
}
|
|
|
|
interface DirectFromTemplateRenameContext {
|
|
type: RequestKind.DirectFromTemplate;
|
|
|
|
/** The position in the TCB file to use as the request to the native TSLS for renaming. */
|
|
renamePosition: FilePosition;
|
|
|
|
/** The position in the template the request originated from. */
|
|
templatePosition: number;
|
|
|
|
/** The target node in the template AST that corresponds to the template position. */
|
|
requestNode: AST|TmplAstNode;
|
|
}
|
|
|
|
type IndirectRenameContext = PipeRenameContext|SelectorRenameContext;
|
|
type RenameRequest =
|
|
IndirectRenameContext|DirectFromTemplateRenameContext|DirectFromTypescriptRenameContext;
|
|
|
|
function isDirectRenameContext(context: RenameRequest): context is DirectFromTemplateRenameContext|
|
|
DirectFromTypescriptRenameContext {
|
|
return context.type === RequestKind.DirectFromTemplate ||
|
|
context.type === RequestKind.DirectFromTypeScript;
|
|
}
|
|
|
|
export class RenameBuilder {
|
|
private readonly ttc = this.compiler.getTemplateTypeChecker();
|
|
|
|
constructor(
|
|
private readonly driver: ProgramDriver, private readonly tsLS: ts.LanguageService,
|
|
private readonly compiler: NgCompiler) {}
|
|
|
|
getRenameInfo(filePath: string, position: number):
|
|
Omit<ts.RenameInfoSuccess, 'kind'|'kindModifiers'>|ts.RenameInfoFailure {
|
|
return this.compiler.perfRecorder.inPhase(PerfPhase.LsReferencesAndRenames, () => {
|
|
const templateInfo = getTemplateInfoAtPosition(filePath, position, this.compiler);
|
|
// We could not get a template at position so we assume the request came from outside the
|
|
// template.
|
|
if (templateInfo === undefined) {
|
|
const renameRequest = this.buildRenameRequestAtTypescriptPosition(filePath, position);
|
|
if (renameRequest === null) {
|
|
return {
|
|
canRename: false,
|
|
localizedErrorMessage: 'Could not determine rename info at typescript position.',
|
|
};
|
|
}
|
|
if (renameRequest.type === RequestKind.PipeName) {
|
|
const pipeName = renameRequest.pipeNameExpr.text;
|
|
return {
|
|
canRename: true,
|
|
displayName: pipeName,
|
|
fullDisplayName: pipeName,
|
|
triggerSpan: {
|
|
length: pipeName.length,
|
|
// Offset the pipe name by 1 to account for start of string '/`/"
|
|
start: renameRequest.pipeNameExpr.getStart() + 1,
|
|
},
|
|
};
|
|
} else {
|
|
// TODO(atscott): Add support for other special indirect renames from typescript files.
|
|
return this.tsLS.getRenameInfo(filePath, position);
|
|
}
|
|
}
|
|
|
|
const allTargetDetails = getTargetDetailsAtTemplatePosition(templateInfo, position, this.ttc);
|
|
if (allTargetDetails === null) {
|
|
return {
|
|
canRename: false,
|
|
localizedErrorMessage: 'Could not find template node at position.'
|
|
};
|
|
}
|
|
const {templateTarget} = allTargetDetails[0];
|
|
const templateTextAndSpan = getRenameTextAndSpanAtPosition(
|
|
templateTarget,
|
|
position,
|
|
);
|
|
if (templateTextAndSpan === null) {
|
|
return {canRename: false, localizedErrorMessage: 'Could not determine template node text.'};
|
|
}
|
|
const {text, span} = templateTextAndSpan;
|
|
return {
|
|
canRename: true,
|
|
displayName: text,
|
|
fullDisplayName: text,
|
|
triggerSpan: span,
|
|
};
|
|
});
|
|
}
|
|
|
|
findRenameLocations(filePath: string, position: number): readonly ts.RenameLocation[]|null {
|
|
this.ttc.generateAllTypeCheckBlocks();
|
|
return this.compiler.perfRecorder.inPhase(PerfPhase.LsReferencesAndRenames, () => {
|
|
const templateInfo = getTemplateInfoAtPosition(filePath, position, this.compiler);
|
|
// We could not get a template at position so we assume the request came from outside the
|
|
// template.
|
|
if (templateInfo === undefined) {
|
|
const renameRequest = this.buildRenameRequestAtTypescriptPosition(filePath, position);
|
|
if (renameRequest === null) {
|
|
return null;
|
|
}
|
|
return this.findRenameLocationsAtTypescriptPosition(renameRequest);
|
|
}
|
|
return this.findRenameLocationsAtTemplatePosition(templateInfo, position);
|
|
});
|
|
}
|
|
|
|
private findRenameLocationsAtTemplatePosition(templateInfo: TemplateInfo, position: number):
|
|
readonly ts.RenameLocation[]|null {
|
|
const allTargetDetails = getTargetDetailsAtTemplatePosition(templateInfo, position, this.ttc);
|
|
if (allTargetDetails === null) {
|
|
return null;
|
|
}
|
|
const renameRequests = this.buildRenameRequestsFromTemplateDetails(allTargetDetails, position);
|
|
if (renameRequests === null) {
|
|
return null;
|
|
}
|
|
|
|
const allRenameLocations: ts.RenameLocation[] = [];
|
|
for (const renameRequest of renameRequests) {
|
|
const locations = this.findRenameLocationsAtTypescriptPosition(renameRequest);
|
|
// If we couldn't find rename locations for _any_ result, we should not allow renaming to
|
|
// proceed instead of having a partially complete rename.
|
|
if (locations === null) {
|
|
return null;
|
|
}
|
|
allRenameLocations.push(...locations);
|
|
}
|
|
return allRenameLocations.length > 0 ? allRenameLocations : null;
|
|
}
|
|
|
|
findRenameLocationsAtTypescriptPosition(renameRequest: RenameRequest):
|
|
readonly ts.RenameLocation[]|null {
|
|
return this.compiler.perfRecorder.inPhase(PerfPhase.LsReferencesAndRenames, () => {
|
|
const renameInfo = getExpectedRenameTextAndInitalRenameEntries(renameRequest);
|
|
if (renameInfo === null) {
|
|
return null;
|
|
}
|
|
const {entries, expectedRenameText} = renameInfo;
|
|
const {fileName, position} = getRenameRequestPosition(renameRequest);
|
|
const findInStrings = false;
|
|
const findInComments = false;
|
|
const locations =
|
|
this.tsLS.findRenameLocations(fileName, position, findInStrings, findInComments);
|
|
if (locations === undefined) {
|
|
return null;
|
|
}
|
|
|
|
for (const location of locations) {
|
|
if (this.ttc.isTrackedTypeCheckFile(absoluteFrom(location.fileName))) {
|
|
const entry = convertToTemplateDocumentSpan(
|
|
location, this.ttc, this.driver.getProgram(), expectedRenameText);
|
|
// There is no template node whose text matches the original rename request. Bail on
|
|
// renaming completely rather than providing incomplete results.
|
|
if (entry === null) {
|
|
return null;
|
|
}
|
|
entries.push(entry);
|
|
} else {
|
|
if (!isDirectRenameContext(renameRequest)) {
|
|
// Discard any non-template results for non-direct renames. We should only rename
|
|
// template results + the name/selector/alias `ts.Expression`. The other results
|
|
// will be the the `ts.Identifier` of the transform method (pipe rename) or the
|
|
// directive class (selector rename).
|
|
continue;
|
|
}
|
|
// Ensure we only allow renaming a TS result with matching text
|
|
const refNode = this.getTsNodeAtPosition(location.fileName, location.textSpan.start);
|
|
if (refNode === null || refNode.getText() !== expectedRenameText) {
|
|
return null;
|
|
}
|
|
entries.push(location);
|
|
}
|
|
}
|
|
return entries;
|
|
});
|
|
}
|
|
|
|
private getTsNodeAtPosition(filePath: string, position: number): ts.Node|null {
|
|
const sf = this.driver.getProgram().getSourceFile(filePath);
|
|
if (!sf) {
|
|
return null;
|
|
}
|
|
return findTightestNode(sf, position) ?? null;
|
|
}
|
|
|
|
private buildRenameRequestsFromTemplateDetails(
|
|
allTargetDetails: TemplateLocationDetails[], templatePosition: number): RenameRequest[]|null {
|
|
const renameRequests: RenameRequest[] = [];
|
|
for (const targetDetails of allTargetDetails) {
|
|
for (const location of targetDetails.typescriptLocations) {
|
|
if (targetDetails.symbol.kind === SymbolKind.Pipe) {
|
|
const meta =
|
|
this.compiler.getMeta(targetDetails.symbol.classSymbol.tsSymbol.valueDeclaration);
|
|
if (meta === null || meta.type !== MetaType.Pipe) {
|
|
return null;
|
|
}
|
|
const renameRequest = this.buildPipeRenameRequest(meta);
|
|
if (renameRequest === null) {
|
|
return null;
|
|
}
|
|
renameRequests.push(renameRequest);
|
|
} else {
|
|
const renameRequest: RenameRequest = {
|
|
type: RequestKind.DirectFromTemplate,
|
|
templatePosition,
|
|
requestNode: targetDetails.templateTarget,
|
|
renamePosition: location
|
|
};
|
|
renameRequests.push(renameRequest);
|
|
}
|
|
}
|
|
}
|
|
return renameRequests;
|
|
}
|
|
|
|
private buildRenameRequestAtTypescriptPosition(filePath: string, position: number): RenameRequest
|
|
|null {
|
|
const requestNode = this.getTsNodeAtPosition(filePath, position);
|
|
if (requestNode === null) {
|
|
return null;
|
|
}
|
|
const meta = getParentClassMeta(requestNode, this.compiler);
|
|
if (meta !== null && meta.type === MetaType.Pipe && meta.nameExpr === requestNode) {
|
|
return this.buildPipeRenameRequest(meta);
|
|
} else {
|
|
return {type: RequestKind.DirectFromTypeScript, requestNode};
|
|
}
|
|
}
|
|
|
|
private buildPipeRenameRequest(meta: PipeMeta): PipeRenameContext|null {
|
|
if (!ts.isClassDeclaration(meta.ref.node) || meta.nameExpr === null ||
|
|
!ts.isStringLiteral(meta.nameExpr)) {
|
|
return null;
|
|
}
|
|
const typeChecker = this.driver.getProgram().getTypeChecker();
|
|
const memberMethods = collectMemberMethods(meta.ref.node, typeChecker) ?? [];
|
|
const pipeTransformNode: ts.MethodDeclaration|undefined =
|
|
memberMethods.find(m => m.name.getText() === 'transform');
|
|
if (pipeTransformNode === undefined) {
|
|
return null;
|
|
}
|
|
return {
|
|
type: RequestKind.PipeName,
|
|
pipeNameExpr: meta.nameExpr,
|
|
renamePosition: {
|
|
fileName: pipeTransformNode.getSourceFile().fileName,
|
|
position: pipeTransformNode.getStart(),
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* From the provided `RenameRequest`, determines what text we should expect all produced
|
|
* `ts.RenameLocation`s to have and creates an initial entry for indirect renames (one which is
|
|
* required for the rename operation, but cannot be found by the native TS LS).
|
|
*/
|
|
function getExpectedRenameTextAndInitalRenameEntries(renameRequest: RenameRequest):
|
|
{expectedRenameText: string, entries: ts.RenameLocation[]}|null {
|
|
let expectedRenameText: string;
|
|
const entries: ts.RenameLocation[] = [];
|
|
if (renameRequest.type === RequestKind.DirectFromTypeScript) {
|
|
expectedRenameText = renameRequest.requestNode.getText();
|
|
} else if (renameRequest.type === RequestKind.DirectFromTemplate) {
|
|
const templateNodeText =
|
|
getRenameTextAndSpanAtPosition(renameRequest.requestNode, renameRequest.templatePosition);
|
|
if (templateNodeText === null) {
|
|
return null;
|
|
}
|
|
expectedRenameText = templateNodeText.text;
|
|
} else if (renameRequest.type === RequestKind.PipeName) {
|
|
const {pipeNameExpr} = renameRequest;
|
|
expectedRenameText = pipeNameExpr.text;
|
|
const entry: ts.RenameLocation = {
|
|
fileName: renameRequest.pipeNameExpr.getSourceFile().fileName,
|
|
textSpan: {start: pipeNameExpr.getStart() + 1, length: pipeNameExpr.getText().length - 2},
|
|
};
|
|
entries.push(entry);
|
|
} else {
|
|
// TODO(atscott): Implement other types of special renames
|
|
return null;
|
|
}
|
|
|
|
return {entries, expectedRenameText};
|
|
}
|
|
|
|
/**
|
|
* Given a `RenameRequest`, determines the `FilePosition` to use asking the native TS LS for rename
|
|
* locations.
|
|
*/
|
|
function getRenameRequestPosition(renameRequest: RenameRequest): FilePosition {
|
|
const fileName = renameRequest.type === RequestKind.DirectFromTypeScript ?
|
|
renameRequest.requestNode.getSourceFile().fileName :
|
|
renameRequest.renamePosition.fileName;
|
|
const position = renameRequest.type === RequestKind.DirectFromTypeScript ?
|
|
renameRequest.requestNode.getStart() :
|
|
renameRequest.renamePosition.position;
|
|
return {fileName, position};
|
|
}
|