Skip to content

Commit e71a72f

Browse files
committed
feat(@schematics/angular): generate detailed migration report for refactor-jasmine-vitest
The `refactor-jasmine-vitest` schematic will now generate a detailed migration report (`jasmine-vitest-<date>.md`). This report provides a summary of the migration process and lists all files requiring manual attention (TODOs), organized by file path and line number. This helps developers quickly identify and address manual migration tasks in large codebases. The report generation is enabled by default but can be disabled via the `report` option. For example: ``` ng generate jasmine-to-vitest --no-report ```
1 parent aeb3b3e commit e71a72f

File tree

8 files changed

+147
-25
lines changed

8 files changed

+147
-25
lines changed

packages/schematics/angular/refactor/jasmine-vitest/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ export default function (options: Schema): Rule {
130130
}
131131
}
132132

133+
if (options.report) {
134+
const reportContent = reporter.generateReportContent();
135+
tree.create(`jasmine-vitest-${new Date().toISOString()}.md`, reportContent);
136+
}
137+
133138
reporter.printSummary(options.verbose);
134139
};
135140
}

packages/schematics/angular/refactor/jasmine-vitest/schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
"type": "boolean",
3636
"description": "Whether the tests are intended to run in browser mode. If true, the `toHaveClass` assertions are left as is because Vitest browser mode has such an assertion. Otherwise they're migrated to an equivalent assertion.",
3737
"default": false
38+
},
39+
"report": {
40+
"type": "boolean",
41+
"description": "Whether to generate a summary report file (jasmine-vitest-<date>.md) in the project root.",
42+
"default": true
3843
}
3944
}
4045
}

packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-lifecycle.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export function transformPending(
8585
'Converted `pending()` to a skipped test (`it.skip`).',
8686
);
8787
const category = 'pending';
88-
reporter.recordTodo(category);
88+
reporter.recordTodo(category, sourceFile, bodyNode);
8989
addTodoComment(replacement, category);
9090
ts.addSyntheticLeadingComment(
9191
replacement,
@@ -412,7 +412,7 @@ export function transformDoneCallback(node: ts.Node, refactorCtx: RefactorContex
412412
`Found unhandled usage of \`${doneIdentifier.text}\` callback. Skipping transformation.`,
413413
);
414414
const category = 'unhandled-done-usage';
415-
reporter.recordTodo(category);
415+
reporter.recordTodo(category, sourceFile, node);
416416
addTodoComment(node, category);
417417

418418
return node;

packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-matcher.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,15 @@ export function transformSyntacticSugarMatchers(
5656

5757
if (matcherName === 'toHaveSpyInteractions') {
5858
const category = 'toHaveSpyInteractions';
59-
reporter.recordTodo(category);
59+
reporter.recordTodo(category, sourceFile, node);
6060
addTodoComment(node, category);
6161

6262
return node;
6363
}
6464

6565
if (matcherName === 'toThrowMatching') {
6666
const category = 'toThrowMatching';
67-
reporter.recordTodo(category);
67+
reporter.recordTodo(category, sourceFile, node);
6868
addTodoComment(node, category, { name: matcherName });
6969

7070
return node;
@@ -304,11 +304,11 @@ export function transformExpectAsync(
304304
if (matcherName) {
305305
if (matcherName === 'toBePending') {
306306
const category = 'toBePending';
307-
reporter.recordTodo(category);
307+
reporter.recordTodo(category, sourceFile, node);
308308
addTodoComment(node, category);
309309
} else {
310310
const category = 'unsupported-expect-async-matcher';
311-
reporter.recordTodo(category);
311+
reporter.recordTodo(category, sourceFile, node);
312312
addTodoComment(node, category, { name: matcherName });
313313
}
314314
}
@@ -418,7 +418,7 @@ export function transformArrayWithExactContents(
418418

419419
if (!ts.isArrayLiteralExpression(argument.arguments[0])) {
420420
const category = 'arrayWithExactContents-dynamic-variable';
421-
reporter.recordTodo(category);
421+
reporter.recordTodo(category, sourceFile, node);
422422
addTodoComment(node, category);
423423

424424
return node;
@@ -455,7 +455,7 @@ export function transformArrayWithExactContents(
455455
const containingStmt = ts.factory.createExpressionStatement(containingCall);
456456

457457
const category = 'arrayWithExactContents-check';
458-
reporter.recordTodo(category);
458+
reporter.recordTodo(category, sourceFile, node);
459459
addTodoComment(lengthStmt, category);
460460

461461
return [lengthStmt, containingStmt];
@@ -615,7 +615,7 @@ export function transformExpectNothing(
615615

616616
reporter.reportTransformation(sourceFile, node, 'Removed `expect().nothing()` statement.');
617617
const category = 'expect-nothing';
618-
reporter.recordTodo(category);
618+
reporter.recordTodo(category, sourceFile, node);
619619
addTodoComment(replacement, category);
620620
ts.addSyntheticLeadingComment(
621621
replacement,

packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-misc.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export function transformGlobalFunctions(
154154
`Found unsupported global function \`${functionName}\`.`,
155155
);
156156
const category = 'unsupported-global-function';
157-
reporter.recordTodo(category);
157+
reporter.recordTodo(category, sourceFile, node);
158158
addTodoComment(node, category, { name: functionName });
159159
}
160160

@@ -187,7 +187,7 @@ export function transformUnsupportedJasmineCalls(
187187
node,
188188
`Found unsupported call \`jasmine.${methodName}\`.`,
189189
);
190-
reporter.recordTodo(methodName);
190+
reporter.recordTodo(methodName, sourceFile, node);
191191
addTodoComment(node, methodName);
192192
}
193193

@@ -238,7 +238,7 @@ export function transformUnknownJasmineProperties(
238238
`Found unknown jasmine property \`jasmine.${propName}\`.`,
239239
);
240240
const category = 'unknown-jasmine-property';
241-
reporter.recordTodo(category);
241+
reporter.recordTodo(category, sourceFile, node);
242242
addTodoComment(node, category, { name: propName });
243243
}
244244
}

packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ export function transformSpies(node: ts.Node, refactorCtx: RefactorContext): ts.
153153
}
154154
default: {
155155
const category = 'unsupported-spy-strategy';
156-
reporter.recordTodo(category);
156+
reporter.recordTodo(category, sourceFile, node);
157157
addTodoComment(node, category, { name: strategyName });
158158
break;
159159
}
@@ -202,7 +202,7 @@ export function transformSpies(node: ts.Node, refactorCtx: RefactorContext): ts.
202202
'Found unsupported `jasmine.spyOnAllFunctions()`.',
203203
);
204204
const category = 'spyOnAllFunctions';
205-
reporter.recordTodo(category);
205+
reporter.recordTodo(category, sourceFile, node);
206206
addTodoComment(node, category);
207207

208208
return node;
@@ -236,7 +236,7 @@ export function transformCreateSpyObj(
236236

237237
if (node.arguments.length < 2 && hasBaseName) {
238238
const category = 'createSpyObj-single-argument';
239-
reporter.recordTodo(category);
239+
reporter.recordTodo(category, sourceFile, node);
240240
addTodoComment(node, category);
241241

242242
return node;
@@ -248,7 +248,7 @@ export function transformCreateSpyObj(
248248
properties = createSpyObjWithObject(methods, baseName);
249249
} else {
250250
const category = 'createSpyObj-dynamic-variable';
251-
reporter.recordTodo(category);
251+
reporter.recordTodo(category, sourceFile, node);
252252
addTodoComment(node, category);
253253

254254
return node;
@@ -259,7 +259,7 @@ export function transformCreateSpyObj(
259259
properties.push(...(propertiesArg.properties as unknown as ts.PropertyAssignment[]));
260260
} else {
261261
const category = 'createSpyObj-dynamic-property-map';
262-
reporter.recordTodo(category);
262+
reporter.recordTodo(category, sourceFile, node);
263263
addTodoComment(node, category);
264264
}
265265
}
@@ -486,7 +486,7 @@ export function transformSpyCallInspection(node: ts.Node, refactorCtx: RefactorC
486486
node.parent.name.text !== 'args'
487487
) {
488488
const category = 'mostRecent-without-args';
489-
reporter.recordTodo(category);
489+
reporter.recordTodo(category, sourceFile, node);
490490
addTodoComment(node, category);
491491
}
492492

packages/schematics/angular/refactor/jasmine-vitest/utils/refactor-reporter.ts

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { logging } from '@angular-devkit/core';
109
import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
1110
import { TodoCategory } from './todo-notes';
1211

@@ -15,8 +14,9 @@ export class RefactorReporter {
1514
private filesTransformed = 0;
1615
private readonly todos = new Map<string, number>();
1716
private readonly verboseLogs = new Map<string, string[]>();
17+
private readonly fileTodos = new Map<string, { category: TodoCategory; line: number }[]>();
1818

19-
constructor(private logger: logging.LoggerApi) {}
19+
constructor(private logger: { info(message: string): void; warn(message: string): void }) {}
2020

2121
get hasTodos(): boolean {
2222
return this.todos.size > 0;
@@ -30,14 +30,27 @@ export class RefactorReporter {
3030
this.filesTransformed++;
3131
}
3232

33-
recordTodo(category: TodoCategory): void {
33+
recordTodo(category: TodoCategory, sourceFile: ts.SourceFile, node: ts.Node): void {
3434
this.todos.set(category, (this.todos.get(category) ?? 0) + 1);
35+
36+
const { line } = ts.getLineAndCharacterOfPosition(
37+
sourceFile,
38+
ts.getOriginalNode(node).getStart(sourceFile),
39+
);
40+
const filePath = sourceFile.fileName;
41+
42+
let fileTodos = this.fileTodos.get(filePath);
43+
if (!fileTodos) {
44+
fileTodos = [];
45+
this.fileTodos.set(filePath, fileTodos);
46+
}
47+
fileTodos.push({ category, line: line + 1 });
3548
}
3649

3750
reportTransformation(sourceFile: ts.SourceFile, node: ts.Node, message: string): void {
3851
const { line } = ts.getLineAndCharacterOfPosition(
3952
sourceFile,
40-
ts.getOriginalNode(node).getStart(),
53+
ts.getOriginalNode(node).getStart(sourceFile),
4154
);
4255
const filePath = sourceFile.fileName;
4356

@@ -49,6 +62,79 @@ export class RefactorReporter {
4962
logs.push(`L${line + 1}: ${message}`);
5063
}
5164

65+
generateReportContent(): string {
66+
const lines: string[] = [];
67+
lines.push('# Jasmine to Vitest Refactoring Report');
68+
lines.push('');
69+
lines.push(`Date: ${new Date().toISOString()}`);
70+
lines.push('');
71+
72+
const summaryEntries = [
73+
{ label: 'Files Scanned', value: this.filesScanned },
74+
{ label: 'Files Transformed', value: this.filesTransformed },
75+
{ label: 'Files Skipped', value: this.filesScanned - this.filesTransformed },
76+
{ label: 'Total TODOs', value: [...this.todos.values()].reduce((a, b) => a + b, 0) },
77+
];
78+
79+
const firstColPad = Math.max(...summaryEntries.map(({ label }) => label.length));
80+
const secondColPad = 5;
81+
82+
lines.push('## Summary');
83+
lines.push('');
84+
lines.push(`| ${' '.padEnd(firstColPad)} | ${'Count'.padStart(secondColPad)} |`);
85+
lines.push(`|:${'-'.repeat(firstColPad + 1)}|${'-'.repeat(secondColPad + 1)}:|`);
86+
for (const { label, value } of summaryEntries) {
87+
lines.push(`| ${label.padEnd(firstColPad)} | ${String(value).padStart(secondColPad)} |`);
88+
}
89+
lines.push('');
90+
91+
if (this.todos.size > 0) {
92+
lines.push('## TODO Overview');
93+
lines.push('');
94+
const todoEntries = [...this.todos.entries()];
95+
const firstColPad = Math.max(
96+
'Category'.length,
97+
...todoEntries.map(([category]) => category.length),
98+
);
99+
const secondColPad = 5;
100+
101+
lines.push(`| ${'Category'.padEnd(firstColPad)} | ${'Count'.padStart(secondColPad)} |`);
102+
lines.push(`|:${'-'.repeat(firstColPad + 1)}|${'-'.repeat(secondColPad + 1)}:|`);
103+
for (const [category, count] of todoEntries) {
104+
lines.push(`| ${category.padEnd(firstColPad)} | ${String(count).padStart(secondColPad)} |`);
105+
}
106+
lines.push('');
107+
}
108+
109+
if (this.fileTodos.size > 0) {
110+
lines.push('## Files Requiring Manual Attention');
111+
lines.push('');
112+
// Sort files alphabetically
113+
const sortedFiles = [...this.fileTodos.keys()].sort();
114+
115+
for (const filePath of sortedFiles) {
116+
const relativePath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
117+
lines.push(`### [\`${relativePath}\`](./${relativePath})`);
118+
const todos = this.fileTodos.get(filePath);
119+
if (todos) {
120+
// Sort todos by line number
121+
todos.sort((a, b) => a.line - b.line);
122+
123+
for (const todo of todos) {
124+
lines.push(`- [L${todo.line}](./${relativePath}#L${todo.line}): ${todo.category}`);
125+
}
126+
}
127+
lines.push('');
128+
}
129+
} else {
130+
lines.push('## No Manual Changes Required');
131+
lines.push('');
132+
lines.push('All identified patterns were successfully transformed.');
133+
}
134+
135+
return lines.join('\n');
136+
}
137+
52138
printSummary(verbose = false): void {
53139
if (verbose && this.verboseLogs.size > 0) {
54140
this.logger.info('Detailed Transformation Log:');

packages/schematics/angular/refactor/jasmine-vitest/utils/refactor-reporter_spec.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,23 @@
77
*/
88

99
import { logging } from '@angular-devkit/core';
10+
import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
1011
import { RefactorReporter } from './refactor-reporter';
1112

1213
describe('RefactorReporter', () => {
1314
let logger: logging.LoggerApi;
1415
let reporter: RefactorReporter;
16+
let sourceFile: ts.SourceFile;
17+
let node: ts.Node;
1518

1619
beforeEach(() => {
1720
logger = {
1821
info: jasmine.createSpy('info'),
1922
warn: jasmine.createSpy('warn'),
2023
} as unknown as logging.LoggerApi;
2124
reporter = new RefactorReporter(logger);
25+
sourceFile = ts.createSourceFile('/test.spec.ts', 'statement;', ts.ScriptTarget.Latest);
26+
node = sourceFile.statements[0];
2227
});
2328

2429
it('should correctly increment scanned and transformed files', () => {
@@ -34,9 +39,9 @@ describe('RefactorReporter', () => {
3439
});
3540

3641
it('should record and count todos by category', () => {
37-
reporter.recordTodo('pending');
38-
reporter.recordTodo('spyOnAllFunctions');
39-
reporter.recordTodo('pending');
42+
reporter.recordTodo('pending', sourceFile, node);
43+
reporter.recordTodo('spyOnAllFunctions', sourceFile, node);
44+
reporter.recordTodo('pending', sourceFile, node);
4045
reporter.printSummary();
4146

4247
expect(logger.warn).toHaveBeenCalledWith('- 3 TODO(s) added for manual review:');
@@ -48,4 +53,25 @@ describe('RefactorReporter', () => {
4853
reporter.printSummary();
4954
expect(logger.warn).not.toHaveBeenCalled();
5055
});
56+
57+
it('should generate a markdown report with TODOs', () => {
58+
reporter.incrementScannedFiles();
59+
reporter.recordTodo('pending', sourceFile, node);
60+
61+
const report = reporter.generateReportContent();
62+
63+
expect(report).toContain('# Jasmine to Vitest Refactoring Report');
64+
expect(report).toContain('## Summary');
65+
expect(report).toContain('| | Count |');
66+
expect(report).toContain('|:------------------|------:|');
67+
expect(report).toContain('| Files Scanned | 1 |');
68+
expect(report).toContain('| Total TODOs | 1 |');
69+
expect(report).toContain('## TODO Overview');
70+
expect(report).toContain('| Category | Count |');
71+
expect(report).toContain('|:---------|------:|');
72+
expect(report).toContain('| pending | 1 |');
73+
expect(report).toContain('## Files Requiring Manual Attention');
74+
expect(report).toContain('### [`test.spec.ts`](./test.spec.ts)');
75+
expect(report).toContain('- [L1](./test.spec.ts#L1): pending');
76+
});
5177
});

0 commit comments

Comments
 (0)