Skip to content

Commit 101d162

Browse files
devversionpkozlowski-opensource
authored andcommitted
refactor(migrations): support inserting TODOs for skipped input fields (angular#57898)
This is a helpful option to retrieve some insights on why certain inputs were not migrated. PR Close angular#57898
1 parent 5bb7050 commit 101d162

File tree

15 files changed

+195
-19
lines changed

15 files changed

+195
-19
lines changed

packages/core/schematics/migrations/signal-migration/src/batch/merge_unit_data.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,5 @@ export function mergeCompilationUnitData(
9090
}
9191
}
9292

93-
console.error(result);
94-
9593
return result;
9694
}

packages/core/schematics/migrations/signal-migration/src/batch/test_bin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ main().catch((e) => {
2222

2323
async function main() {
2424
const [mode, ...args] = process.argv.slice(2);
25-
const migration = new SignalInputMigration();
25+
const migration = new SignalInputMigration({insertTodosForSkippedFields: true});
2626

2727
if (mode === 'extract') {
2828
const analyzeResult = await executeAnalyzePhase(migration, path.resolve(args[0]));

packages/core/schematics/migrations/signal-migration/src/cli.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,26 @@ import assert from 'assert';
1212
import {SignalInputMigration} from './migration';
1313
import {writeMigrationReplacements} from './write_replacements';
1414

15-
main(path.resolve(process.argv[2]), process.argv.includes('--best-effort-mode')).catch((e) => {
15+
main(
16+
path.resolve(process.argv[2]),
17+
process.argv.includes('--best-effort-mode'),
18+
process.argv.includes('--insert-todos'),
19+
).catch((e) => {
1620
console.error(e);
1721
process.exitCode = 1;
1822
});
1923

2024
/**
2125
* Runs the signal input migration for the given TypeScript project.
2226
*/
23-
export async function main(absoluteTsconfigPath: string, bestEffortMode: boolean) {
27+
export async function main(
28+
absoluteTsconfigPath: string,
29+
bestEffortMode: boolean,
30+
insertTodosForSkippedFields: boolean,
31+
) {
2432
const migration = new SignalInputMigration({
2533
bestEffortMode,
34+
insertTodosForSkippedFields,
2635
upgradeAnalysisPhaseToAvoidBatch: true,
2736
});
2837
const baseInfo = migration.createProgram(absoluteTsconfigPath);

packages/core/schematics/migrations/signal-migration/src/input_detection/directive_info.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ export class DirectiveInfo {
5353
* then the member is as well.
5454
*/
5555
isInputMemberIncompatible(input: InputDescriptor): boolean {
56-
return this.incompatible !== null || this.memberIncompatibility.has(input.key);
56+
return this.getInputMemberIncompatibility(input) !== null;
57+
}
58+
59+
/** Get incompatibility of the given member, if it's incompatible for migration. */
60+
getInputMemberIncompatibility(
61+
input: InputDescriptor,
62+
): ClassIncompatibilityReason | InputMemberIncompatibility | null {
63+
return this.incompatible ?? this.memberIncompatibility.get(input.key) ?? null;
5764
}
5865
}

packages/core/schematics/migrations/signal-migration/src/input_detection/incompatibility_human.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export function getMessageForClassIncompatibility(reason: ClassIncompatibilityRe
101101
case ClassIncompatibilityReason.ClassManuallyInstantiated:
102102
return {
103103
short:
104-
'Class of this input is manually instantiated (`new Cmp()`). ' +
104+
'Class of this input is manually instantiated. ' +
105105
'This is discouraged and prevents migration',
106106
extra:
107107
'Signal inputs require a DI injection context. Manually instantiating ' +

packages/core/schematics/migrations/signal-migration/src/migration_config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ export interface MigrationConfig {
1515
*/
1616
bestEffortMode?: boolean;
1717

18+
/**
19+
* Whether to insert TODOs for skipped fields, and reasons on why they
20+
* were skipped.
21+
*/
22+
insertTodosForSkippedFields?: boolean;
23+
1824
/**
1925
* Whether the given input should be migrated. With batch execution, this
2026
* callback fires for foreign inputs from other compilation units too.

packages/core/schematics/migrations/signal-migration/src/passes/6_migrate_input_declarations.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,22 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {ImportManager} from '@angular/compiler-cli/src/ngtsc/translator';
10+
import assert from 'assert';
911
import ts from 'typescript';
10-
import {MigrationResult} from '../result';
12+
import {ProgramInfo, projectFile, Replacement, TextUpdate} from '../../../../utils/tsurge';
1113
import {convertToSignalInput} from '../convert-input/convert_to_signal';
12-
import assert from 'assert';
1314
import {KnownInputs} from '../input_detection/known_inputs';
14-
import {ImportManager} from '@angular/compiler-cli/src/ngtsc/translator';
15-
import {ProgramInfo, projectFile, Replacement, TextUpdate} from '../../../../utils/tsurge';
15+
import {MigrationHost} from '../migration_host';
16+
import {MigrationResult} from '../result';
17+
import {insertTodoForIncompatibility} from '../utils/incompatibility_todos';
1618

1719
/**
1820
* Phase that migrates `@Input()` declarations to signal inputs and
1921
* manages imports within the given file.
2022
*/
2123
export function pass6__migrateInputDeclarations(
24+
host: MigrationHost,
2225
checker: ts.TypeChecker,
2326
result: MigrationResult,
2427
knownInputs: KnownInputs,
@@ -30,13 +33,20 @@ export function pass6__migrateInputDeclarations(
3033

3134
for (const [input, metadata] of result.sourceInputs) {
3235
const sf = input.node.getSourceFile();
36+
const inputInfo = knownInputs.get(input)!;
3337

3438
// Do not migrate incompatible inputs.
35-
if (knownInputs.get(input)!.isIncompatible() || metadata === null) {
39+
if (inputInfo.isIncompatible()) {
40+
// Add a TODO for the incompatible input, if desired.
41+
if (host.config.insertTodosForSkippedFields) {
42+
result.replacements.push(...insertTodoForIncompatibility(input.node, info, inputInfo));
43+
}
44+
3645
filesWithIncompatibleInputs.add(sf);
3746
continue;
3847
}
3948

49+
assert(metadata !== null, `Expected metadata to exist for input isn't marked incompatible.`);
4050
assert(!ts.isAccessor(input.node), 'Accessor inputs are incompatible.');
4151

4252
filesWithMigratedInputs.add(sf);

packages/core/schematics/migrations/signal-migration/src/phase_migrate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export function executeMigrationPhase(
5454

5555
// Migrate passes.
5656
pass5__migrateTypeScriptReferences(referenceMigrationHost, result.references, typeChecker, info);
57-
pass6__migrateInputDeclarations(typeChecker, result, knownInputs, importManager, info);
57+
pass6__migrateInputDeclarations(host, typeChecker, result, knownInputs, importManager, info);
5858
pass7__migrateTemplateReferences(referenceMigrationHost, result.references);
5959
pass8__migrateHostBindings(referenceMigrationHost, result.references, info);
6060
pass9__migrateTypeScriptTypeReferences(
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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.io/license
7+
*/
8+
9+
import {KnownInputInfo} from '../input_detection/known_inputs';
10+
import {ProgramInfo, Replacement} from '../../../../utils/tsurge';
11+
import {isInputMemberIncompatibility} from '../input_detection/incompatibility';
12+
import {
13+
getMessageForClassIncompatibility,
14+
getMessageForInputIncompatibility,
15+
} from '../input_detection/incompatibility_human';
16+
import {insertPrecedingLine} from '../../../../utils/tsurge/helpers/ast/insert_preceding_line';
17+
import {InputNode} from '../input_detection/input_node';
18+
19+
/**
20+
* Inserts a TODO for the incompatibility blocking the given node
21+
* from being migrated.
22+
*/
23+
export function insertTodoForIncompatibility(
24+
node: InputNode,
25+
programInfo: ProgramInfo,
26+
input: KnownInputInfo,
27+
): Replacement[] {
28+
const incompatibility = input.container.getInputMemberIncompatibility(input.descriptor);
29+
if (incompatibility === null) {
30+
return [];
31+
}
32+
33+
const message = isInputMemberIncompatibility(incompatibility)
34+
? getMessageForInputIncompatibility(incompatibility.reason).short
35+
: getMessageForClassIncompatibility(incompatibility).short;
36+
const lines = cutStringToWordLimit(message, 70);
37+
38+
return [
39+
insertPrecedingLine(node, programInfo, `// TODO: Skipped for migration because:`),
40+
...lines.map((line) => insertPrecedingLine(node, programInfo, `// ${line}`)),
41+
];
42+
}
43+
44+
/**
45+
* Cuts the given string into lines basing around the specified
46+
* line length limit. This function breaks the string on a per-word basis.
47+
*/
48+
function cutStringToWordLimit(str: string, limit: number): string[] {
49+
const words = str.split(' ');
50+
const chunks: string[] = [];
51+
let chunkIdx = 0;
52+
53+
while (words.length) {
54+
// New line if we exceed limit.
55+
if (chunks[chunkIdx] !== undefined && chunks[chunkIdx].length > limit) {
56+
chunkIdx++;
57+
}
58+
// Ensure line is initialized for the given index.
59+
if (chunks[chunkIdx] === undefined) {
60+
chunks[chunkIdx] = '';
61+
}
62+
63+
const word = words.shift();
64+
const needsSpace = chunks[chunkIdx].length > 0;
65+
66+
// Insert word. Add space before, if the line already contains text.
67+
chunks[chunkIdx] += `${needsSpace ? ' ' : ''}${word}`;
68+
}
69+
70+
return chunks;
71+
}

packages/core/schematics/migrations/signal-migration/test/BUILD.bazel

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ integration_test(
3838
"//packages/core/schematics/migrations/signal-migration/test/golden-test:test_files",
3939
],
4040
commands = [
41-
"$(rootpath //packages/core/schematics/migrations/signal-migration/src:bin) ./golden-test/tsconfig.json",
41+
"$(rootpath //packages/core/schematics/migrations/signal-migration/src:bin) ./golden-test/tsconfig.json --insert-todos",
4242
"$(rootpath :golden_test_runner) ./golden-test ./golden.txt",
4343
],
4444
data = [
@@ -60,7 +60,7 @@ integration_test(
6060
"//packages/core/schematics/migrations/signal-migration/test/ts-versions:loader.mjs",
6161
],
6262
commands = [
63-
"$(rootpath //packages/core/schematics/migrations/signal-migration/src:bin) --node_options=--import=./ts-versions/loader.js ./golden-test/tsconfig.json",
63+
"$(rootpath //packages/core/schematics/migrations/signal-migration/src:bin) --node_options=--import=./ts-versions/loader.js ./golden-test/tsconfig.json --insert-todos ",
6464
"$(rootpath :golden_test_runner) ./golden-test ./golden.txt",
6565
],
6666
data = [
@@ -83,7 +83,7 @@ integration_test(
8383
"//packages/core/schematics/migrations/signal-migration/test/golden-test:test_files",
8484
],
8585
commands = [
86-
"$(rootpath //packages/core/schematics/migrations/signal-migration/src:bin) ./golden-test/tsconfig.json --best-effort-mode",
86+
"$(rootpath //packages/core/schematics/migrations/signal-migration/src:bin) ./golden-test/tsconfig.json --best-effort-mode --insert-todos",
8787
"$(rootpath :golden_test_runner) ./golden-test ./golden_best_effort.txt",
8888
],
8989
data = [

0 commit comments

Comments
 (0)