Skip to content

Commit 6342bef

Browse files
committed
feat(language-service): support migrating full classes to signal queries (#58263)
Adds a similar code action as for inputs, where users can migrate full classes to signal queries. PR Close #58263
1 parent 15ca29f commit 6342bef

File tree

4 files changed

+384
-6
lines changed

4 files changed

+384
-6
lines changed

packages/language-service/src/refactorings/convert_to_signal_queries/apply_query_refactoring.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export async function applySignalQueriesRefactoring(
106106
} else if (incompatibilityMessages.size > 0) {
107107
const queryPlural = incompatibilityMessages.size === 1 ? 'query' : `queries`;
108108
message = `${incompatibilityMessages.size} ${queryPlural} could not be migrated.\n`;
109-
message += `For more details, click on the skipped inputs and try to migrate individually.\n`;
109+
message += `For more details, click on the skipped queries and try to migrate individually.\n`;
110110
}
111111

112112
// Only suggest the "force ignoring" option if there are actually
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {CompilerOptions} from '@angular/compiler-cli';
10+
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
11+
import {MigrationConfig} from '@angular/core/schematics/migrations/signal-migration/src';
12+
import {ApplyRefactoringProgressFn, ApplyRefactoringResult} from '@angular/language-service/api';
13+
import ts from 'typescript';
14+
import {isTypeScriptFile} from '../../utils';
15+
import {findTightestNode, getParentClassDeclaration} from '../../utils/ts_utils';
16+
import type {ActiveRefactoring} from '../refactoring';
17+
import {isDecoratorQueryClassField, isDirectiveOrComponentWithQueries} from './decorators';
18+
import {applySignalQueriesRefactoring} from './apply_query_refactoring';
19+
20+
/**
21+
* Base language service refactoring action that can convert decorator
22+
* queries of a full class to signal queries.
23+
*
24+
* The user can click on an class with decorator queries and ask for all the queries
25+
* to be migrated. All references, imports and the declaration are updated automatically.
26+
*/
27+
abstract class BaseConvertFullClassToSignalQueriesRefactoring implements ActiveRefactoring {
28+
abstract config: MigrationConfig;
29+
30+
constructor(private project: ts.server.Project) {}
31+
32+
static isApplicable(
33+
compiler: NgCompiler,
34+
fileName: string,
35+
positionOrRange: number | ts.TextRange,
36+
): boolean {
37+
if (!isTypeScriptFile(fileName)) {
38+
return false;
39+
}
40+
41+
const sf = compiler.getCurrentProgram().getSourceFile(fileName);
42+
if (sf === undefined) {
43+
return false;
44+
}
45+
46+
const start = typeof positionOrRange === 'number' ? positionOrRange : positionOrRange.pos;
47+
const node = findTightestNode(sf, start);
48+
if (node === undefined) {
49+
return false;
50+
}
51+
52+
const classDecl = getParentClassDeclaration(node);
53+
if (classDecl === undefined) {
54+
return false;
55+
}
56+
const {reflector} = compiler['ensureAnalyzed']();
57+
if (!isDirectiveOrComponentWithQueries(classDecl, reflector)) {
58+
return false;
59+
}
60+
61+
const parentClassElement = ts.findAncestor(node, (n) => ts.isClassElement(n) || ts.isBlock(n));
62+
if (parentClassElement === undefined) {
63+
return true;
64+
}
65+
// If we are inside a body of e.g. an accessor, this action should not show up.
66+
if (ts.isBlock(parentClassElement)) {
67+
return false;
68+
}
69+
return isDecoratorQueryClassField(parentClassElement, reflector);
70+
}
71+
72+
async computeEditsForFix(
73+
compiler: NgCompiler,
74+
compilerOptions: CompilerOptions,
75+
fileName: string,
76+
positionOrRange: number | ts.TextRange,
77+
reportProgress: ApplyRefactoringProgressFn,
78+
): Promise<ApplyRefactoringResult> {
79+
const sf = compiler.getCurrentProgram().getSourceFile(fileName);
80+
if (sf === undefined) {
81+
return {edits: []};
82+
}
83+
84+
const start = typeof positionOrRange === 'number' ? positionOrRange : positionOrRange.pos;
85+
const node = findTightestNode(sf, start);
86+
if (node === undefined) {
87+
return {edits: []};
88+
}
89+
90+
const containingClass = getParentClassDeclaration(node);
91+
if (containingClass === null) {
92+
return {edits: [], errorMessage: 'Could not find a class for the refactoring.'};
93+
}
94+
95+
return await applySignalQueriesRefactoring(
96+
compiler,
97+
compilerOptions,
98+
this.config,
99+
this.project,
100+
reportProgress,
101+
(queryID) => queryID.node.parent === containingClass,
102+
/** allowPartialMigration */ true,
103+
);
104+
}
105+
}
106+
107+
export class ConvertFullClassToSignalQueriesRefactoring extends BaseConvertFullClassToSignalQueriesRefactoring {
108+
static id = 'convert-full-class-to-signal-queries-safe-mode';
109+
static description = 'Full class: Convert all decorator queries to signal queries (safe)';
110+
override config: MigrationConfig = {};
111+
}
112+
export class ConvertFullClassToSignalQueriesBestEffortRefactoring extends BaseConvertFullClassToSignalQueriesRefactoring {
113+
static id = 'convert-full-class-to-signal-queries-best-effort-mode';
114+
static description =
115+
'Full class: Convert all decorator queries to signal queries (forcibly, ignoring errors)';
116+
override config: MigrationConfig = {bestEffortMode: true};
117+
}

packages/language-service/src/refactorings/refactoring.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import {
2222
ConvertFieldToSignalQueryBestEffortRefactoring,
2323
ConvertFieldToSignalQueryRefactoring,
2424
} from './convert_to_signal_queries/individual_query_refactoring';
25+
import {
26+
ConvertFullClassToSignalQueriesBestEffortRefactoring,
27+
ConvertFullClassToSignalQueriesRefactoring,
28+
} from './convert_to_signal_queries/full_class_query_refactoring';
2529

2630
/**
2731
* Interface exposing static metadata for a {@link Refactoring},
@@ -80,4 +84,6 @@ export const allRefactorings: Refactoring[] = [
8084
// Queries migration
8185
ConvertFieldToSignalQueryRefactoring,
8286
ConvertFieldToSignalQueryBestEffortRefactoring,
87+
ConvertFullClassToSignalQueriesRefactoring,
88+
ConvertFullClassToSignalQueriesBestEffortRefactoring,
8389
];

0 commit comments

Comments
 (0)