Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import {
transformSpyCallInspection,
transformSpyReset,
} from './transformers/jasmine-spy';
import { transformJasmineTypes } from './transformers/jasmine-type';
import { getVitestAutoImports } from './utils/ast-helpers';
import { RefactorContext } from './utils/refactor-context';
import { RefactorReporter } from './utils/refactor-reporter';

Expand Down Expand Up @@ -78,11 +80,13 @@ export function transformJasmineToVitest(
ts.ScriptKind.TS,
);

const pendingVitestImports = new Set<string>();
const transformer: ts.TransformerFactory<ts.SourceFile> = (context) => {
const refactorCtx: RefactorContext = {
sourceFile,
reporter,
tsContext: context,
pendingVitestImports,
};

const visitor: ts.Visitor = (node) => {
Expand Down Expand Up @@ -149,6 +153,8 @@ export function transformJasmineToVitest(
break;
}
}
} else if (ts.isQualifiedName(transformedNode) || ts.isTypeReferenceNode(transformedNode)) {
transformedNode = transformJasmineTypes(transformedNode, refactorCtx);
}

// Visit the children of the node to ensure they are transformed
Expand All @@ -163,12 +169,22 @@ export function transformJasmineToVitest(
};

const result = ts.transform(sourceFile, [transformer]);
if (result.transformed[0] === sourceFile && !reporter.hasTodos) {
let transformedSourceFile = result.transformed[0];

if (transformedSourceFile === sourceFile && !reporter.hasTodos && !pendingVitestImports.size) {
return content;
}

const vitestImport = getVitestAutoImports(pendingVitestImports);
if (vitestImport) {
transformedSourceFile = ts.factory.updateSourceFile(transformedSourceFile, [
vitestImport,
...transformedSourceFile.statements,
]);
}

const printer = ts.createPrinter();
const transformedContentWithPlaceholders = printer.printFile(result.transformed[0]);
const transformedContentWithPlaceholders = printer.printFile(transformedSourceFile);

return restoreBlankLines(transformedContentWithPlaceholders);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

/**
* @fileoverview This file contains a transformer that migrates Jasmine type definitions to
* their Vitest equivalents. It handles the conversion of types like `jasmine.Spy` and
* `jasmine.SpyObj` to Vitest's `Mock` and `MockedObject` types, and ensures that the
* necessary `vitest` imports are added to the file.
*/

import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
import { addVitestAutoImport } from '../utils/ast-helpers';
import { RefactorContext } from '../utils/refactor-context';

export function transformJasmineTypes(
node: ts.Node,
{ sourceFile, reporter, pendingVitestImports }: RefactorContext,
): ts.Node {
const typeNameNode = ts.isTypeReferenceNode(node) ? node.typeName : node;
if (
!ts.isQualifiedName(typeNameNode) ||
!ts.isIdentifier(typeNameNode.left) ||
typeNameNode.left.text !== 'jasmine'
) {
return node;
}

const jasmineTypeName = typeNameNode.right.text;

switch (jasmineTypeName) {
case 'Spy': {
const vitestTypeName = 'Mock';
reporter.reportTransformation(
sourceFile,
node,
`Transformed type \`jasmine.Spy\` to \`${vitestTypeName}\`.`,
);
addVitestAutoImport(pendingVitestImports, vitestTypeName);

return ts.factory.createIdentifier(vitestTypeName);
}
case 'SpyObj': {
const vitestTypeName = 'MockedObject';
reporter.reportTransformation(
sourceFile,
node,
`Transformed type \`jasmine.SpyObj\` to \`${vitestTypeName}\`.`,
);
addVitestAutoImport(pendingVitestImports, vitestTypeName);

if (ts.isTypeReferenceNode(node)) {
return ts.factory.updateTypeReferenceNode(
node,
ts.factory.createIdentifier(vitestTypeName),
node.typeArguments,
);
}

return ts.factory.createIdentifier(vitestTypeName);
}
case 'Any':
reporter.reportTransformation(
sourceFile,
node,
`Transformed type \`jasmine.Any\` to \`any\`.`,
);

return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
case 'ObjectContaining': {
const typeArguments = ts.isTypeReferenceNode(node) ? node.typeArguments : undefined;
if (typeArguments && typeArguments.length > 0) {
reporter.reportTransformation(
sourceFile,
node,
`Transformed type \`jasmine.ObjectContaining\` to \`Partial\`.`,
);

return ts.factory.createTypeReferenceNode('Partial', typeArguments);
}

reporter.reportTransformation(
sourceFile,
node,
`Transformed type \`jasmine.ObjectContaining\` to \`object\`.`,
);

return ts.factory.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword);
}
case 'DoneFn':
reporter.reportTransformation(
sourceFile,
node,
'Transformed type `jasmine.DoneFn` to `() => void`.',
);

return ts.factory.createFunctionTypeNode(
undefined,
[],
ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword),
);
}

return node;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { expectTransformation } from '../test-helpers';

describe('Jasmine to Vitest Transformer', () => {
describe('transformJasmineTypes', () => {
const testCases = [
{
description: 'should transform a variable with a jasmine.Spy type',
input: `let mySpy: jasmine.Spy;`,
expected: `
import type { Mock } from 'vitest';
let mySpy: Mock;
`,
},
{
description: 'should transform a variable with a jasmine.SpyObj type',
input: `let mySpy: jasmine.SpyObj<MyService>;`,
expected: `
import type { MockedObject } from 'vitest';
let mySpy: MockedObject<MyService>;
`,
},
{
description: 'should handle multiple jasmine types and create a single import',
input: `
let mySpy: jasmine.Spy;
let mySpyObj: jasmine.SpyObj<MyService>;
`,
expected: `
import type { Mock, MockedObject } from 'vitest';

let mySpy: Mock;
let mySpyObj: MockedObject<MyService>;
`,
},
{
description: 'should not add an import if no jasmine types are used',
input: `let mySpy: any;`,
expected: `let mySpy: any;`,
},
{
description: 'should transform jasmine.Any to any',
input: `let myMatcher: jasmine.Any;`,
expected: `let myMatcher: any;`,
},
{
description: 'should transform jasmine.ObjectContaining<T> to Partial<T>',
input: `let myMatcher: jasmine.ObjectContaining<MyService>;`,
expected: `let myMatcher: Partial<MyService>;`,
},
{
description: 'should transform jasmine.ObjectContaining to object',
input: `let myMatcher: jasmine.ObjectContaining;`,
expected: `let myMatcher: object;`,
},
{
description: 'should transform jasmine.DoneFn to () => void',
input: `let myDoneFn: jasmine.DoneFn;`,
expected: `let myDoneFn: () => void;`,
},
];

testCases.forEach(({ description, input, expected }) => {
it(description, async () => {
await expectTransformation(input, expected);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,32 @@

import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript';

export function addVitestAutoImport(imports: Set<string>, importName: string): void {
imports.add(importName);
}

export function getVitestAutoImports(imports: Set<string>): ts.ImportDeclaration | undefined {
if (!imports?.size) {
return undefined;
}

const importNames = [...imports];
importNames.sort();
const importSpecifiers = importNames.map((i) =>
ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(i)),
);

return ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(
ts.SyntaxKind.TypeKeyword,
undefined,
ts.factory.createNamedImports(importSpecifiers),
),
ts.factory.createStringLiteral('vitest'),
);
}

export function createViCallExpression(
methodName: string,
args: readonly ts.Expression[] = [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export interface RefactorContext {

/** The official context from the TypeScript Transformer API. */
readonly tsContext: ts.TransformationContext;

/** A set of Vitest type imports to be added to the file. */
readonly pendingVitestImports: Set<string>;
}

/**
Expand Down