8000 refactor(language-service): initial reference and rename implementati… · angular/angular@fa27b76 · GitHub
[go: up one dir, main page]

Skip to content

Commit fa27b76

Browse files
crisbetoalxhub
authored andcommitted
refactor(language-service): initial reference and rename implementation for selectorless (#61240)
Adds an initial implementation for finding references and renaming to selectorless components/directives. Finding references should work everywhere, whereas renaming only currently works when initiated from the template. PR Close #61240
1 parent f074c30 commit fa27b76

File tree

3 files changed

+666
-24
lines changed

3 files changed

+666
-24
lines changed

packages/language-service/src/references_and_rename.ts

Lines changed: 117 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,25 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.dev/license
77
*/
8-
import {AST, TmplAstNode} from '@angular/compiler';
8+
import {AST, TmplAstComponent, TmplAstDirective, TmplAstNode} from '@angular/compiler';
99
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
1010
import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
1111
import {MetaKind, PipeMeta} from '@angular/compiler-cli/src/ngtsc/metadata';
1212
import {PerfPhase} from '@angular/compiler-cli/src/ngtsc/perf';
13-
import {SymbolKind, TemplateTypeChecker} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
13+
import {
14+
SelectorlessComponentSymbol,
15+
SelectorlessDirectiveSymbol,
16+
SymbolKind,
17+
TemplateTypeChecker,
18+
} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
1419
import ts from 'typescript';
1520

1621
import {
1722
convertToTemplateDocumentSpan,
1823
FilePosition,
1924
getParentClassMeta,
2025
getRenameTextAndSpanAtPosition,
26+
getSelectorlessTemplateSpanFromTcbLocations,
2127
getTargetDetailsAtTemplatePosition,
2228
TemplateLocationDetails,
2329
} from './references_and_rename_utils';
@@ -96,6 +102,7 @@ enum RequestKind {
96102
DirectFromTypeScript,
97103
PipeName,
98104
Selector,
105+
SelectorlessIdentifier,
99106
}
100107

101108
/** The context needed to perform a rename of a pipe name. */
@@ -127,6 +134,20 @@ interface SelectorRenameContext {
127134
renamePosition: FilePosition;
128135
}
129136

137+
/** The context needed to perform a rename of a selectorless component/directive. */
138+
interface SelectorlessIdentifierRenameContext {
139+
type: RequestKind.SelectorlessIdentifier;
140+
141+
/** Node defining the component/directive. */
142+
templateNode: TmplAstComponent | TmplAstDirective;
143+
144+
/** Identifier of the class defining the class. */
145+
identifier: ts.Identifier;
146+
147+
/** Location used for querying the TypeScript language service. */
148+
renamePosition: FilePosition;
149+
}
150+
130151
interface DirectFromTypescriptRenameContext {
131152
type: RequestKind.DirectFromTypeScript;
132153

@@ -151,14 +172,19 @@ type IndirectRenameContext = PipeRenameContext | SelectorRenameContext;
151172
type RenameRequest =
152173
| IndirectRenameContext
153174
| DirectFromTemplateRenameContext
154-
| DirectFromTypescriptRenameContext;
175+
| DirectFromTypescriptRenameContext
176+
| SelectorlessIdentifierRenameContext;
155177

156178
function isDirectRenameContext(
157179
context: RenameRequest,
158-
): context is DirectFromTemplateRenameContext | DirectFromTypescriptRenameContext {
180+
): context is
181+
| DirectFromTemplateRenameContext
182+
| DirectFromTypescriptRenameContext
183+
| SelectorlessIdentifierRenameContext {
159184
return (
160185
context.type === RequestKind.DirectFromTemplate ||
161-
context.type === RequestKind.DirectFromTypeScript
186+
context.type === RequestKind.DirectFromTypeScript ||
187+
context.type === RequestKind.SelectorlessIdentifier
162188
);
163189
}
164190

@@ -200,6 +226,16 @@ export class RenameBuilder {
200226
start: renameRequest.pipeNameExpr.getStart() + 1,
201227
},
202228
};
229+
} else if (renameRequest.type === RequestKind.SelectorlessIdentifier) {
230+
return {
231+
canRename: true,
232+
displayName: renameRequest.identifier.text,
233+
fullDisplayName: renameRequest.identifier.text,
234+
triggerSpan: {
235+
length: renameRequest.identifier.text.length,
236+
start: renameRequest.identifier.getStart(),
237+
},
238+
};
203239
} else {
204240
// TODO(atscott): Add support for other special indirect renames from typescript files.
205241
return this.tsLS.getRenameInfo(filePath, position);
@@ -299,18 +335,30 @@ export class RenameBuilder {
299335

300336
for (const location of locations) {
301337
if (this.ttc.isTrackedTypeCheckFile(absoluteFrom(location.fileName))) {
302-
const entry = convertToTemplateDocumentSpan(
303-
location,
304-
this.ttc,
305-
this.compiler.getCurrentProgram(),
306-
expectedRenameText,
307-
);
308-
// There is no template node whose text matches the original rename request. Bail on
309-
// renaming completely rather than providing incomplete results.
310-
if (entry === null) {
311-
return null;
338+
if (renameRequest.type === RequestKind.SelectorlessIdentifier) {
339+
const selectorlessEntries = getSelectorlessTemplateSpanFromTcbLocations(
340+
location,
341+
this.ttc,
342+
this.compiler.getCurrentProgram(),
343+
renameRequest.templateNode,
344+
);
345+
if (selectorlessEntries !== null) {
346+
entries.push(...selectorlessEntries);
347+
}
348+
} else {
349+
const entry = convertToTemplateDocumentSpan(
350+
location,
351+
this.ttc,
352+
this.compiler.getCurrentProgram(),
353+
expectedRenameText,
354+
);
355+
// There is no template node whose text matches the original rename request. Bail on
356+
// renaming completely rather than providing incomplete results.
357+
if (entry === null) {
358+
return null;
359+
}
360+
entries.push(entry);
312361
}
313-
entries.push(entry);
314362
} else {
315363
if (!isDirectRenameContext(renameRequest)) {
316364
// Discard any non-template results for non-direct renames. We should only rename
@@ -358,6 +406,17 @@ export class RenameBuilder {
358406
return null;
359407
}
360408
renameRequests.push(renameRequest);
409+
} else if (
410+
targetDetails.symbol.kind === SymbolKind.SelectorlessComponent ||
411+
targetDetails.symbol.kind === SymbolKind.SelectorlessDirective
412+
) {
413+
const renameRequest = this.buildSelectorlessRenameRequestFromTemplate(
414+
targetDetails.symbol,
415+
);
416+
if (renameRequest === null) {
417+
return null;
418+
}
419+
renameRequests.push(renameRequest);
361420
} else {
362421
const renameRequest: RenameRequest = {
363422
type: RequestKind.DirectFromTemplate,
@@ -413,6 +472,40 @@ export class RenameBuilder {
413472
},
414473
};
415474
}
475+
476+
private buildSelectorlessRenameRequestFromTemplate(
477+
symbol: SelectorlessComponentSymbol | SelectorlessDirectiveSymbol,
478+
): SelectorlessIdentifierRenameContext | null {
479+
if (symbol.tsSymbol === null || symbol.tsSymbol.valueDeclaration === undefined) {
480+
return null;
481+
}
482+
483+
const meta = this.compiler.getMeta(symbol.tsSymbol.valueDeclaration);
484+
if (meta === null || meta.kind !== MetaKind.Directive) {
485+
return null;
486+
}
487+
488+
const nameNode = meta.ref.node.name;
489+
const templateName =
490+
symbol.kind === SymbolKind.SelectorlessComponent
491+
? symbol.templateNode.componentName
492+
: symbol.templateNode.name;
493+
494+
// Do not rename aliased references.
495+
if (templateName !== nameNode.text) {
496+
return null;
497+
}
498+
499+
return {
500+
type: RequestKind.SelectorlessIdentifier,
501+
templateNode: symbol.templateNode,
502+
identifier: nameNode,
503+
renamePosition: {
504+
fileName: meta.ref.node.getSourceFile().fileName,
505+
position: nameNode.getStart(),
506+
},
507+
};
508+
}
416509
}
417510

418511
/**
@@ -444,6 +537,14 @@ function getExpectedRenameTextAndInitialRenameEntries(
444537
textSpan: {start: pipeNameExpr.getStart() + 1, length: pipeNameExpr.getText().length - 2},
445538
};
446539
entries.push(entry);
540+
} else if (renameRequest.type === RequestKind.SelectorlessIdentifier) {
541+
const {identifier} = renameRequest;
542+
expectedRenameText = identifier.text;
543+
const entry: ts.RenameLocation = {
544+
fileName: identifier.getSourceFile().fileName,
545+
textSpan: {start: identifier.getStart(), length: identifier.getWidth()},
546+
};
547+
entries.push(entry);
447548
} else {
448549
// TODO(atscott): Implement other types of special renames
449550
return null;

packages/language-service/src/references_and_rename_utils.ts

Lines changed: 108 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@ import {
2020
TmplAstReference,
2121
TmplAstTextAttribute,
2222
TmplAstVariable,
23+
TmplAstComponent,
24+
TmplAstDirective,
2325
} from '@angular/compiler';
2426
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
2527
import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
2628
import {DirectiveMeta, PipeMeta} from '@angular/compiler-cli/src/ngtsc/metadata';
2729
import {
2830
DirectiveSymbol,
31+
SelectorlessComponentSymbol,
32+
SelectorlessDirectiveSymbol,
2933
Symbol,
3034
SymbolKind,
3135
TcbLocation,
@@ -216,6 +220,17 @@ export function getTargetDetailsAtTemplatePosition(
216220
});
217221
break;
218222
}
223+
case SymbolKind.SelectorlessDirective:
224+
case SymbolKind.SelectorlessComponent:
225+
const dirPosition = getPositionForDirective(symbol);
226+
if (dirPosition !== null) {
227+
details.push({
228+
typescriptLocations: [dirPosition],
229+
templateTarget,
230+
symbol,
231+
});
232+
}
233+
break;
219234
}
220235
}
221236

@@ -228,17 +243,31 @@ export function getTargetDetailsAtTemplatePosition(
228243
function getPositionsForDirectives(directives: Set<DirectiveSymbol>): FilePosition[] {
229244
const allDirectives: FilePosition[] = [];
230245
for (const dir of directives.values()) {
231-
const dirClass = dir.tsSymbol.valueDeclaration;
232-
if (dirClass === undefined || !ts.isClassDeclaration(dirClass) || dirClass.name === undefined) {
233-
continue;
246+
const position = getPositionForDirective(dir);
247+
if (position !== null) {
248+
allDirectives.push(position);
234249
}
250+
}
251+
return allDirectives;
252+
}
235253

236-
const {fileName} = dirClass.getSourceFile();
237-
const position = dirClass.name.getStart();
238-
allDirectives.push({fileName, position});
254+
/** Gets the `FilePosition` for a single directive symbol. */
255+
function getPositionForDirective(
256+
directive: DirectiveSymbol | SelectorlessComponentSymbol | SelectorlessDirectiveSymbol,
257+
): FilePosition | null {
258+
const declaration = directive.tsSymbol?.valueDeclaration;
259+
260+
if (
261+
declaration !== undefined &&
262+
ts.isClassDeclaration(declaration) &&
263+
declaration.name !== undefined
264+
) {
265+
const {fileName} = declaration.getSourceFile();
266+
const position = declaration.name.getStart();
267+
return {fileName, position};
239268
}
240269

241-
return allDirectives;
270+
return null;
242271
}
243272

244273
/**
@@ -358,8 +387,10 @@ export function getRenameTextAndSpanAtPosition(
358387
span.length -= 2;
359388
}
360389
return {text, span};
361-
} else if (node instanceof TmplAstElement) {
390+
} else if (node instanceof TmplAstElement || node instanceof TmplAstDirective) {
362391
return {text: node.name, span: toTextSpan(node.startSourceSpan)};
392+
} else if (node instanceof TmplAstComponent) {
393+
return {text: node.componentName, span: toTextSpan(node.startSourceSpan)};
363394
}
364395

365396
return null;
@@ -380,3 +411,72 @@ export function getParentClassMeta(
380411
}
381412
return compiler.getMeta(parentClass);
382413
}
414+
415+
/**
416+
* Converts a given `ts.DocumentSpan` in a shim file into one or more spans in the template,
417+
* representing a selectorless component or directive. There can be more than one return value
418+
* when a component has a closing tag.
419+
*/
420+
export function getSelectorlessTemplateSpanFromTcbLocations(
421+
shimDocumentSpan: ts.DocumentSpan,
422+
templateTypeChecker: TemplateTypeChecker,
423+
program: ts.Program,
424+
node: TmplAstComponent | TmplAstDirective,
425+
): ts.DocumentSpan[] | null {
426+
const sf = program.getSourceFile(shimDocumentSpan.fileName);
427+
if (sf === undefined) {
428+
return null;
429+
}
430+
431+
let tcbNode = findTightestNode(sf, shimDocumentSpan.textSpan.start);
432+
if (tcbNode === undefined) {
433+
return null;
434+
}
435+
436+
// Variables in the typecheck block are generated with the type on the right hand
437+
// side: `var _t1 = null! as i1.DirA`. Finding references of DirA will return the type
438+
// assertion and we need to map it back to the variable identifier _t1.
439+
if (hasExpressionIdentifier(sf, tcbNode, ExpressionIdentifier.VARIABLE_AS_EXPRESSION)) {
440+
while (tcbNode && !ts.isVariableDeclaration(tcbNode)) {
441+
tcbNode = tcbNode.parent;
442+
}
443+
}
444+
445+
const mapping = getTemplateLocationFromTcbLocation(
446+
templateTypeChecker,
447+
absoluteFrom(shimDocumentSpan.fileName),
448+
/* tcbIsShim */ true,
449+
tcbNode.getStart(),
450+
);
451+
452+
if (mapping === null) {
453+
return null;
454+
}
455+
456+
const fileName = mapping.templateUrl;
457+
const {length} = node instanceof TmplAstComponent ? node.componentName : node.name;
458+
const spans: ts.DocumentSpan[] = [
459+
{
460+
fileName,
461+
textSpan: {
462+
// +1 because of the opening `<` or `@`.
463+
start: node.startSourceSpan.start.offset + 1,
464+
length,
465+
},
466+
},
467+
];
468+
469+
// If it's not a self-closing template tag, we need to rename the end tag too.
470+
if (node instanceof TmplAstComponent && node.endSourceSpan?.toString().startsWith('</')) {
471+
spans.push({
472+
fileName,
473+
textSpan: {
474+
// +2 because of the `</`.
475+
start: node.endSourceSpan.start.offset + 2,
476+
length,
477+
},
478+
});
479+
}
480+
481+
return spans;
482+
}

0 commit comments

Comments
 (0)
0