Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bc83fc1

Browse files
committed
feat(language-service): support converting to signal queries in VSCode extension (#58106)
This commit adds support for converting decorator queries to signal queries via the VSCode extension. Note that this is not fully finished as we still need to add better messaging when certain fields could not be migrated. In addition, it's worth noting that the migration is not as safe as the input migration because commonly query lists are passed around— this quickly can break the build— but is an acceptable trade-off for the work saved. A migration cannot be 100% correct in general; there are always edge-cases. PR Close #58106
1 parent 3748392 commit bc83fc1

File tree

16 files changed

+402
-121
lines changed

16 files changed

+402
-121
lines changed

‎packages/core/schematics/migrations/signal-migration/src/utils/grouped_ts_ast_visitor.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,7 @@ export class GroupedTsAstVisitor {
5353
for (const doneFn of this.doneFns) {
5454
doneFn();
5555
}
56+
57+
this.visitors = [];
5658
}
5759
}

‎packages/core/schematics/migrations/signal-queries-migration/BUILD.bazel

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ ts_library(
66
["**/*.ts"],
77
exclude = ["*.spec.ts"],
88
),
9-
visibility = ["//packages/core/schematics/ng-generate/signal-queries-migration:__pkg__"],
9+
visibility = [
10+
"//packages/core/schematics/ng-generate/signal-queries-migration:__pkg__",
11+
"//packages/language-service/src/refactorings:__pkg__",
12+
],
1013
deps = [
1114
"//packages/compiler",
1215
"//packages/compiler-cli",

‎packages/core/schematics/migrations/signal-queries-migration/convert_query_property.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,7 @@ export function computeReplacementsToMigrateQuery(
121121
}
122122

123123
const call = ts.factory.createCallExpression(newQueryFn, type ? [type] : undefined, args);
124-
const updated = ts.factory.updatePropertyDeclaration(
125-
node,
124+
const updated = ts.factory.createPropertyDeclaration(
126125
[ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword)],
127126
node.name,
128127
undefined,

‎packages/core/schematics/migrations/signal-queries-migration/known_queries.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export class KnownQueries
2525
InheritanceTracker<ClassFieldDescriptor>
2626
{
2727
private readonly classToQueryFields = new Map<ts.ClassLikeDeclaration, ClassFieldDescriptor[]>();
28-
private readonly knownQueryIDs = new Set<ClassFieldUniqueKey>();
28+
29+
readonly knownQueryIDs = new Map<ClassFieldUniqueKey, ClassFieldDescriptor>();
2930

3031
constructor(
3132
private readonly info: ProgramInfo,
@@ -55,7 +56,7 @@ export class KnownQueries
5556
key: id,
5657
node: queryField,
5758
});
58-
this.knownQueryIDs.add(id);
59+
this.knownQueryIDs.set(id, {key: id, node: queryField});
5960
}
6061

6162
attemptRetrieveDescriptorFromSymbol(symbol: ts.Symbol): ClassFieldDescriptor | null {

‎packages/core/schematics/migrations/signal-queries-migration/migration.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,39 @@ describe('signal queries migration', () => {
12061206
}
12071207
`);
12081208
});
1209+
1210+
it('should preserve existing property comments', async () => {
1211+
const {fs} = await runTsurgeMigration(new SignalQueriesMigration(), [
1212+
{
1213+
name: absoluteFrom('/app.component.ts'),
1214+
isProgramRootFile: true,
1215+
contents: dedent`
1216+
import {ViewChildren, QueryList, ElementRef, Component} from '@angular/core';
1217+
1218+
@Component({
1219+
template: '',
1220+
})
1221+
class MyComp {
1222+
/** works */
1223+
@ViewChildren('label') labels = new QueryList<ElementRef>();
1224+
}
1225+
`,
1226+
},
1227+
]);
1228+
1229+
const actual = fs.readFile(absoluteFrom('/app.component.ts'));
1230+
expect(actual).toMatchWithDiff(`
1231+
import {ElementRef, Component, viewChildren} from '@angular/core';
1232+
1233+
@Component({
1234+
template: '',
1235+
})
1236+
class MyComp {
1237+
/** works */
1238+
readonly labels = viewChildren<ElementRef>('label');
1239+
}
1240+
`);
1241+
});
12091242
});
12101243

12111244
function populateDeclarationTestCaseComponent(declaration: string): string {

‎packages/core/schematics/migrations/signal-queries-migration/migration.ts

Lines changed: 113 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
isHostBindingReference,
3636
isTemplateReference,
3737
isTsReference,
38+
Reference,
3839
} from '../signal-migration/src/passes/reference_resolution/reference_kinds';
3940
import {ReferenceResult} from '../signal-migration/src/passes/reference_resolution/reference_result';
4041
import {GroupedTsAstVisitor} from '../signal-migration/src/utils/grouped_ts_ast_visitor';
@@ -62,11 +63,17 @@ export interface CompilationUnitData {
6263
// potential problematic "class fields", but noting for later that those only would be
6364
// problematic if they end up being multi-result queries.
6465
potentialProblematicReferenceForMultiQueries: Record<ClassFieldUniqueKey, true>;
66+
67+
// NOTE: Not serializable — ONLY works when we know it's not running in batch mode!
68+
reusableAnalysisReferences: Reference<ClassFieldDescriptor>[] | null;
6569
}
6670

6771
export interface GlobalUnitData {
6872
knownQueryFields: Record<ClassFieldUniqueKey, {fieldName: string; isMulti: boolean}>;
6973
problematicQueries: Record<ClassFieldUniqueKey, true>;
74+
75+
// NOTE: Not serializable — ONLY works when we know it's not running in batch mode!
76+
reusableAnalysisReferences: Reference<ClassFieldDescriptor>[] | null;
7077
}
7178

7279
export class SignalQueriesMigration extends TsurgeComplexMigration<
@@ -79,8 +86,6 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
7986

8087
override async analyze(info: ProgramInfo): Promise<Serializable<CompilationUnitData>> {
8188
assert(info.ngCompiler !== null, 'Expected queries migration to have an Angular program.');
82-
// TODO: This stage for this migration doesn't necessarily need a full
83-
// compilation unit program.
8489

8590
// Pre-Analyze the program and get access to the template type checker.
8691
const {templateTypeChecker} = info.ngCompiler['ensureAnalyzed']();
@@ -93,13 +98,32 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
9398
knownQueryFields: {},
9499
potentialProblematicQueries: {},
95100
potentialProblematicReferenceForMultiQueries: {},
101+
reusableAnalysisReferences: null,
96102
};
97103
const groupedAstVisitor = new GroupedTsAstVisitor(sourceFiles);
98104
const referenceResult: ReferenceResult<ClassFieldDescriptor> = {references: []};
105+
const classesWithFilteredQueries = new WeakSet<ts.ClassLikeDeclaration>();
106+
const filteredQueriesForCompilationUnit = new Map<ClassFieldUniqueKey, {fieldName: string}>();
99107

100108
const findQueryDefinitionsVisitor = (node: ts.Node) => {
101109
const extractedQuery = extractSourceQueryDefinition(node, reflector, evaluator, info);
102110
if (extractedQuery !== null) {
111+
const descriptor = {
112+
key: extractedQuery.id,
113+
node: extractedQuery.node,
114+
};
115+
const containingFile = projectFile(descriptor.node.getSourceFile(), info);
116+
117+
if (
118+
this.config.shouldMigrateQuery === undefined ||
119+
this.config.shouldMigrateQuery(descriptor, containingFile)
120+
) {
121+
classesWithFilteredQueries.add(extractedQuery.node.parent);
122+
filteredQueriesForCompilationUnit.set(extractedQuery.id, {
123+
fieldName: extractedQuery.queryInfo.propertyName,
124+
});
125+
}
126+
103127
res.knownQueryFields[extractedQuery.id] = {
104128
fieldName: extractedQuery.queryInfo.propertyName,
105129
isMulti: extractedQuery.queryInfo.first === false,
@@ -108,6 +132,16 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
108132
};
109133

110134
groupedAstVisitor.register(findQueryDefinitionsVisitor);
135+
if (this.config.assumeNonBatch) {
136+
// In non-batch, we need to find queries before, so we can perform
137+
// improved reference resolution.
138+
this.config.reportProgressFn?.(20, 'Scanning for queries..');
139+
groupedAstVisitor.execute();
140+
this.config.reportProgressFn?.(30, 'Scanning for references..');
141+
} else {
142+
this.config.reportProgressFn?.(20, 'Scanning for queries and references..');
143+
}
144+
111145
groupedAstVisitor.register(
112146
createFindAllSourceFileReferencesVisitor(
113147
info,
@@ -116,21 +150,44 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
116150
info.ngCompiler['resourceManager'],
117151
evaluator,
118152
templateTypeChecker,
119-
// Eager, rather expensive tracking of all references.
120-
// We don't know yet if something refers to a different query or not, so we
121-
// eagerly detect such and later filter those problematic references that
122-
// turned out to refer to queries.
123-
// TODO: Consider skipping this extra work when running in non-batch mode.
124-
// TODO: Also consider skipping if we know this query cannot be part.
125153
{
126-
shouldTrackClassReference: (_class) => false,
127-
attemptRetrieveDescriptorFromSymbol: (s) => getClassFieldDescriptorForSymbol(s, info),
154+
// Note: We don't support cross-target migration of `Partial<T>` usages.
155+
// This is an acceptable limitation for performance reasons.
156+
shouldTrackClassReference: (node) => classesWithFilteredQueries.has(node),
157+
attemptRetrieveDescriptorFromSymbol: (s) => {
158+
const descriptor = getClassFieldDescriptorForSymbol(s, info);
159+
160+
// If we are executing in upgraded analysis phase mode, we know all
161+
// of the queries since there aren't any other compilation units.
162+
// Ignore references to non-query class fields.
163+
if (
164+
this.config.assumeNonBatch &&
165+
descriptor !== null &&
166+
!filteredQueriesForCompilationUnit.has(descriptor.key)
167+
) {
168+
return null;
169+
}
170+
171+
// TODO: Also consider skipping if we know this cannot be a query.
172+
// e.g. missing class decorators or some other checks.
173+
174+
// In batch mode, we eagerly, rather expensively, track all references.
175+
// We don't know yet if something refers to a different query or not, so we
176+
// eagerly detect such and later filter those problematic references that
177+
// turned out to refer to queries (once we have the global metadata).
178+
179+
return descriptor;
180+
},
128181
},
129-
null,
182+
// In non-batch mode, we know what inputs exist and can optimize the reference
183+
// resolution significantly (for e.g. VSCode integration)— as we know what
184+
// field names may be used to reference potential queries.
185+
this.config.assumeNonBatch
186+
? new Set(Array.from(filteredQueriesForCompilationUnit.values()).map((f) => f.fieldName))
187+
: null,
130188
referenceResult,
131189
).visitor,
132190
);
133-
134191
groupedAstVisitor.execute();
135192

136193
// Determine incompatible queries based on problematic references
@@ -152,13 +209,18 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
152209
checkForIncompatibleQueryListAccesses(ref, res);
153210
}
154211

212+
if (this.config.assumeNonBatch) {
213+
res.reusableAnalysisReferences = referenceResult.references;
214+
}
215+
155216
return confirmAsSerializable(res);
156217
}
157218

158219
override async merge(units: CompilationUnitData[]): Promise<Serializable<GlobalUnitData>> {
159220
const merged: GlobalUnitData = {
160221
knownQueryFields: {},
161222
problematicQueries: {},
223+
reusableAnalysisReferences: null,
162224
};
163225
for (const unit of units) {
164226
for (const [id, value] of Object.entries(unit.knownQueryFields)) {
@@ -167,6 +229,10 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
167229
for (const id of Object.keys(unit.potentialProblematicQueries)) {
168230
merged.problematicQueries[id as ClassFieldUniqueKey] = true;
169231
}
232+
if (unit.reusableAnalysisReferences !== null) {
233+
assert(units.length === 1, 'Expected migration to not run in batch mode');
234+
merged.reusableAnalysisReferences = unit.reusableAnalysisReferences;
235+
}
170236
}
171237

172238
for (const unit of units) {
@@ -184,7 +250,7 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
184250
assert(info.ngCompiler !== null, 'Expected queries migration to have an Angular program.');
185251

186252
// Pre-Analyze the program and get access to the template type checker.
187-
const {templateTypeChecker, metaReader} = await info.ngCompiler['ensureAnalyzed']();
253+
const {templateTypeChecker, metaReader} = info.ngCompiler['ensureAnalyzed']();
188254
const {program, sourceFiles} = info;
189255
const checker = program.getTypeChecker();
190256
const reflector = new TypeScriptReflectionHost(checker);
@@ -193,9 +259,9 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
193259
const importManager = new ImportManager();
194260
const printer = ts.createPrinter();
195261

196-
const filesWithMigratedQueries = new Map<ts.SourceFile, Set<QueryFunctionName>>();
262+
const filesWithSourceQueries = new Map<ts.SourceFile, Set<QueryFunctionName>>();
197263
const filesWithIncompleteMigration = new Map<ts.SourceFile, Set<QueryFunctionName>>();
198-
const filesWithUnrelatedQueryListImports = new WeakSet<ts.SourceFile>();
264+
const filesWithQueryListOutsideOfDeclarations = new WeakSet<ts.SourceFile>();
199265

200266
const knownQueries = new KnownQueries(info, globalMetadata);
201267
const referenceResult: ReferenceResult<ClassFieldDescriptor> = {references: []};
@@ -236,12 +302,14 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
236302
node.text === 'QueryList' &&
237303
ts.findAncestor(node, ts.isImportDeclaration) === undefined
238304
) {
239-
filesWithUnrelatedQueryListImports.add(node.getSourceFile());
305+
filesWithQueryListOutsideOfDeclarations.add(node.getSourceFile());
240306
}
241307

242308
ts.forEachChild(node, queryWholeProgramVisitor);
243309
};
244310

311+
this.config.reportProgressFn?.(40, 'Tracking query declarations..');
312+
245313
for (const sf of info.fullProgramSourceFiles) {
246314
ts.forEachChild(sf, queryWholeProgramVisitor);
247315
}
@@ -254,43 +322,56 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
254322

255323
// Find all references.
256324
const groupedAstVisitor = new GroupedTsAstVisitor(sourceFiles);
257-
groupedAstVisitor.register(
258-
createFindAllSourceFileReferencesVisitor(
259-
info,
260-
checker,
261-
reflector,
262-
info.ngCompiler['resourceManager'],
263-
evaluator,
264-
templateTypeChecker,
265-
knownQueries,
266-
fieldNamesToConsiderForReferenceLookup,
267-
referenceResult,
268-
).visitor,
269-
);
325+
326+
// Re-use previous reference result if available, instead of
327+
// looking for references which is quite expensive.
328+
if (globalMetadata.reusableAnalysisReferences !== null) {
329+
referenceResult.references = globalMetadata.reusableAnalysisReferences;
330+
} else {
331+
groupedAstVisitor.register(
332+
createFindAllSourceFileReferencesVisitor(
333+
info,
334+
checker,
335+
reflector,
336+
info.ngCompiler['resourceManager'],
337+
evaluator,
338+
templateTypeChecker,
339+
knownQueries,
340+
fieldNamesToConsiderForReferenceLookup,
341+
referenceResult,
342+
).visitor,
343+
);
344+
}
270345

271346
const inheritanceGraph = new InheritanceGraph(checker).expensivePopulate(info.sourceFiles);
272347
checkIncompatiblePatterns(inheritanceGraph, checker, groupedAstVisitor, knownQueries, () =>
273348
knownQueries.getAllClassesWithQueries(),
274349
);
350+
351+
this.config.reportProgressFn?.(60, 'Checking for problematic patterns..');
275352
groupedAstVisitor.execute();
276353

277354
// Check inheritance.
355+
this.config.reportProgressFn?.(70, 'Checking for inheritance patterns..');
278356
checkInheritanceOfKnownFields(inheritanceGraph, metaReader, knownQueries, {
279357
getFieldsForClass: (n) => knownQueries.getQueryFieldsOfClass(n) ?? [],
280358
isClassWithKnownFields: (clazz) => knownQueries.getQueryFieldsOfClass(clazz) !== undefined,
281359
});
282360

361+
this.config.reportProgressFn?.(80, 'Migrating queries..');
362+
283363
// Migrate declarations.
284364
for (const extractedQuery of sourceQueries) {
285365
const node = extractedQuery.node;
286366
const sf = node.getSourceFile();
287367
const descriptor = {key: extractedQuery.id, node: extractedQuery.node};
288368

289369
if (!isMigratedQuery(descriptor)) {
370+
updateFileState(filesWithSourceQueries, sf, extractedQuery.kind);
290371
updateFileState(filesWithIncompleteMigration, sf, extractedQuery.kind);
291372
continue;
292373
}
293-
updateFileState(filesWithMigratedQueries, sf, extractedQuery.kind);
374+
updateFileState(filesWithSourceQueries, sf, extractedQuery.kind);
294375

295376
replacements.push(
296377
...computeReplacementsToMigrateQuery(
@@ -329,7 +410,7 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
329410
}
330411

331412
// Remove imports if possible.
332-
for (const [file, types] of filesWithMigratedQueries) {
413+
for (const [file, types] of filesWithSourceQueries) {
333414
let seenIncompatibleMultiQuery = false;
334415

335416
for (const type of types) {
@@ -343,7 +424,7 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
343424
}
344425
}
345426

346-
if (!seenIncompatibleMultiQuery && !filesWithUnrelatedQueryListImports.has(file)) {
427+
if (!seenIncompatibleMultiQuery && !filesWithQueryListOutsideOfDeclarations.has(file)) {
347428
importManager.removeImport(file, 'QueryList', '@angular/core');
348429
}
349430
}

0 commit comments

Comments
 (0)