From acf44d60fea29597e92693c225dcae2d9b7e1f0a Mon Sep 17 00:00:00 2001 From: Markus Wolf Date: Wed, 31 May 2017 11:17:36 +0200 Subject: [PATCH] feat: mocha to jest transform add mocha to jest transform (by example of jest-codemods). Also add get and forEach methods to collection --- package.json | 2 +- src/collection.test.ts | 16 ++++++ src/collection.ts | 84 ++++++++++++++++++++++---------- src/index.test.ts | 2 +- src/index.ts | 29 +---------- src/transform.ts | 29 +++++++++++ src/transforms/jest.test.ts | 37 ++++++++++++++ src/transforms/jest.ts | 97 +++++++++++++++++++++++++++++++++++++ src/types.ts | 2 +- 9 files changed, 241 insertions(+), 57 deletions(-) create mode 100644 src/transform.ts create mode 100644 src/transforms/jest.test.ts create mode 100644 src/transforms/jest.ts diff --git a/package.json b/package.json index 49b49f4..6f20147 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "fs-extra": "^3.0.1", "globby": "^6.1.0", "meow": "^3.7.0", - "ts-emitter": "^0.2.1" + "ts-emitter": "^0.2.2" }, "jest": { "transform": { diff --git a/src/collection.test.ts b/src/collection.test.ts index 7b38753..a315910 100644 --- a/src/collection.test.ts +++ b/src/collection.test.ts @@ -83,4 +83,20 @@ describe('Collection', () => { expect(actual).toBe(1); }); }); + describe('#get', () => { + it('should return a subtree of found nodes', () => { + const source = ` + function fn1(a: string): void {} + function fn2(a: string): void {} + `; + const collection = Collection.fromSource(source); + + const actual = collection + .find(ts.SyntaxKind.FunctionDeclaration) + .get(node => node.name) + .size(); + + expect(actual).toBe(2); + }); + }); }); diff --git a/src/collection.ts b/src/collection.ts index 69e8baf..ca5ba56 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -2,33 +2,45 @@ import * as emitter from 'ts-emitter'; import * as ts from 'typescript'; import { isPatternMatching } from './pattern-matcher'; -export class Collection { +export class Collection { - private root: T; + private _root: Collection|undefined; - private collected: T[]; + protected collected: T[]; - public static fromSource(source: string): Collection { + public static fromSource(source: string): Collection { const file = emitter.fromSource(source); - return new Collection([file], file); + return new Collection([file]); } - public static fromNode(node: T): Collection { - return new Collection([node], node); + public static fromNode(node: T): Collection { + return new Collection([node]); } - private constructor(collected: T[], root: T) { - this.root = root; + private constructor(collected: T[], root?: Collection) { + this._root = root; this.collected = collected; } - public find(kind: ts.SyntaxKind.Identifier, pattern?: IdentifierPattern): Collection; - public find(kind: ts.SyntaxKind.FunctionDeclaration): Collection; - public find(kind: ts.SyntaxKind.FunctionExpression): Collection; - public find(kind: ts.SyntaxKind.CallExpression, pattern?: CallExpressionPattern): Collection; - public find(kind: ts.SyntaxKind.VariableDeclarationList): Collection; - public find(kind: ts.SyntaxKind.VariableDeclaration): Collection; - public find(kind: ts.SyntaxKind, pattern?: any): Collection { + private get root(): Collection { + return this._root || (this as any); + } + + protected get rootNode(): R { + return this.root.collected[0]; + } + + protected set rootNode(node: R) { + this.root.collected[0] = node; + } + + public find(kind: ts.SyntaxKind.Identifier, pattern?: IdentifierPattern): Collection; + public find(kind: ts.SyntaxKind.FunctionDeclaration): Collection; + public find(kind: ts.SyntaxKind.FunctionExpression): Collection; + public find(kind: ts.SyntaxKind.CallExpression, pattern?: CallExpressionPattern): Collection; + public find(kind: ts.SyntaxKind.VariableDeclarationList): Collection; + public find(kind: ts.SyntaxKind.VariableDeclaration): Collection; + public find(kind: ts.SyntaxKind, pattern?: any): Collection { const marked: ts.Node[] = []; const visitor = (node: ts.Node) => { if (node.kind === kind) { @@ -36,6 +48,8 @@ export class Collection { marked.push(node); } else if (!pattern) { marked.push(node); + } else { + ts.forEachChild(node, visitor); } } else { ts.forEachChild(node, visitor); @@ -47,7 +61,18 @@ export class Collection { return new Collection(marked, this.root); } - public filter(fn: (node: T) => boolean): Collection { + public get(fn: (node: T) => U|undefined): Collection { + const marked: U[] = []; + this.collected.forEach(node => { + const result = fn(node); + if (result) { + marked.push(result); + } + }); + return new Collection(marked, this.root); + } + + public filter(fn: (node: T) => boolean): Collection { const marked: T[] = []; this.collected.forEach(node => { if (fn(node)) { @@ -57,15 +82,17 @@ export class Collection { return new Collection(marked, this.root); } - public replaceWith(fn: (node: T) => ts.Node): Collection { - const replacer = (context: ts.TransformationContext) => (rootNode: T) => { + public replaceWith(fn: (node: T) => ts.Node): this { + const replacer = (context: ts.TransformationContext) => (rootNode: R) => { const visitor = (node: ts.Node): ts.Node => { const markedNode = this.collected.find(item => item === node); if (markedNode) { const replaced = fn(markedNode); - (replaced as any).original = markedNode; - if ((replaced as any).text && (replaced as any).text !== (markedNode as any).text) { - (replaced as any).newText = (replaced as any).text; + if (replaced !== markedNode) { + (replaced as any).original = markedNode; + if ((replaced as any).text && (replaced as any).text !== (markedNode as any).text) { + (replaced as any).newText = (replaced as any).text; + } } return replaced; } @@ -73,7 +100,12 @@ export class Collection { }; return ts.visitNode(rootNode, visitor); }; - this.root = ts.transform(this.root, [replacer]).transformed[0]; + this.rootNode = ts.transform(this.rootNode, [replacer]).transformed[0]; + return this; + } + + public forEach(fn: (node: T) => void): this { + this.collected.forEach(node => fn(node)); return this; } @@ -82,11 +114,11 @@ export class Collection { } public toSource(): string { - if (this.root.kind !== ts.SyntaxKind.SourceFile) { + if (this.rootNode.kind !== ts.SyntaxKind.SourceFile) { throw new Error(`toSource() could only be called on collections of type ` - + `'ts.SourceFile' but this is of type '${ts.SyntaxKind[this.root.kind]}'`); + + `'SourceFile' but this is of type '${ts.SyntaxKind[this.rootNode.kind]}'`); } - return emitter.toSource(this.root as any); + return emitter.toSource(this.rootNode as any); } } diff --git a/src/index.test.ts b/src/index.test.ts index ab744d2..5baa796 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,6 +1,6 @@ import { stripIndent } from 'common-tags'; import * as ts from 'typescript'; -import { applyTransforms } from './index'; +import { applyTransforms } from './transform'; import * as types from './types'; test('run identifiers transform', () => { diff --git a/src/index.ts b/src/index.ts index 2887728..ac48580 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,20 +2,9 @@ import * as fs from 'fs-extra'; import globby = require('globby'); import meow = require('meow'); -import * as ts from 'typescript'; -import { Collection } from './collection'; +import { applyTransforms } from './transform'; import * as types from './types'; -const api = { - tscodeshift: (source: string|ts.Node) => { - if (typeof source === 'string') { - return Collection.fromSource(source); - } else { - return Collection.fromNode(source); - } - } -}; - async function main(cli: meow.Result): Promise { const transformPaths = [].concat( cli.flags['t'] || [], @@ -32,22 +21,6 @@ async function main(cli: meow.Result): Promise { }); } -/* @internal */ -export function applyTransforms(path: string, source: string, transforms: types.Transform[]): string { - const file: types.File = { - path, - source - }; - return transforms - .reduce((file, transform) => { - return { - path: file.path, - source: transform(file, api) - }; - }, file) - .source; -} - const cli = meow(` Usage: tscodeshift ... [options] diff --git a/src/transform.ts b/src/transform.ts new file mode 100644 index 0000000..6da1fce --- /dev/null +++ b/src/transform.ts @@ -0,0 +1,29 @@ +import * as ts from 'typescript'; +import { Collection } from './collection'; +import * as types from './types'; + +const api = { + tscodeshift: (source: string|ts.Node) => { + if (typeof source === 'string') { + return Collection.fromSource(source); + } else { + return Collection.fromNode(source); + } + } +}; + +/* @internal */ +export function applyTransforms(path: string, source: string, transforms: types.Transform[]): string { + const file: types.File = { + path, + source + }; + return transforms + .reduce((file, transform) => { + return { + path: file.path, + source: transform(file, api) + }; + }, file) + .source; +} diff --git a/src/transforms/jest.test.ts b/src/transforms/jest.test.ts new file mode 100644 index 0000000..f331c58 --- /dev/null +++ b/src/transforms/jest.test.ts @@ -0,0 +1,37 @@ +import { stripIndent } from 'common-tags'; +import { applyTransforms } from '../transform'; + +import transform from './jest'; + +test('Convert mocha test to jest test', () => { + const source = stripIndent` + before(() => { + // something + }); + + suite('Array', function() { + setup(() => { + }); + + specify.skip('Array', function(done) { + done(); + }); + }); + `; + const expected = stripIndent` + beforeAll(()=> { + // something + }); + + describe('Array', function() { + beforeEach(()=> { + }); + + it.skip('Array', function(done) { + done(); + }); + }); + `; + const actual = applyTransforms('path.ts', source, [transform]); + expect(actual).toBe(expected); +}); diff --git a/src/transforms/jest.ts b/src/transforms/jest.ts new file mode 100644 index 0000000..e96897b --- /dev/null +++ b/src/transforms/jest.ts @@ -0,0 +1,97 @@ +import * as ts from 'typescript'; +import { File, API } from '../types'; + +const methodMap: { [key: string]: string } = { + suite: 'describe', + context: 'describe', + specify: 'it', + test: 'it', + before: 'beforeAll', + beforeEach: 'beforeEach', + setup: 'beforeEach', + after: 'afterAll', + afterEach: 'afterEach', + teardown: 'afterEach', + suiteSetup: 'beforeAll', + suiteTeardown: 'afterAll' +}; + +// const jestMethodsWithDescriptionsAllowed = new Set(['it', 'describe']); + +const methodModifiers = ['only', 'skip']; + +// function hasBinding(name, scope) { +// if (!scope) { +// return false; +// } + +// const bindings = Object.keys(scope.getBindings()) || []; +// if (bindings.indexOf(name) >= 0) { +// return true; +// } + +// return scope.isGlobal ? false : hasBinding(name, scope.parent); +// } + +export default function mochaToJest(file: File, api: API): string { + const t = api.tscodeshift; + const ast = t(file.source); + + Object.keys(methodMap).forEach(mochaMethod => { + const jestMethod = methodMap[mochaMethod]; + + ast + .find(ts.SyntaxKind.CallExpression, { + expression: { + kind: ts.SyntaxKind.Identifier, + text: mochaMethod + } + }) + // .filter(({ scope }) => !hasBinding(mochaMethod, scope)) + .get(node => node.expression) + .replaceWith(() => { + // let args = node.arguments; + // if (!jestMethodsWithDescriptionsAllowed.has(jestMethod)) { + // args = args.filter(a => a.kind !== ts.SyntaxKind.LiteralType); + // } + return ts.createIdentifier(jestMethod); + }); + + methodModifiers.forEach(modifier => { + ast + .find(ts.SyntaxKind.CallExpression, { + expression: { + kind: ts.SyntaxKind.PropertyAccessExpression, + expression: { + kind: ts.SyntaxKind.Identifier, + text: mochaMethod + }, + name: { + kind: ts.SyntaxKind.Identifier, + text: modifier + } + } + }) + .get(node => (node.expression as ts.PropertyAccessExpression).expression) + .replaceWith(() => ts.createIdentifier(jestMethod)); + ast + .find(ts.SyntaxKind.CallExpression, { + expression: { + kind: ts.SyntaxKind.PropertyAccessExpression, + expression: { + kind: ts.SyntaxKind.Identifier, + text: mochaMethod + }, + name: { + kind: ts.SyntaxKind.Identifier, + text: modifier + } + } + }) + .get(node => (node.expression as ts.PropertyAccessExpression).name) + .replaceWith(() => ts.createIdentifier(modifier)); + }); + }); + + return ast.toSource(); +} diff --git a/src/types.ts b/src/types.ts index 2cc99b8..77b10af 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,5 +13,5 @@ export interface API { } export interface TSCodeShift { - (source: string|T): Collection; + (source: string|T): Collection; }