-
Notifications
You must be signed in to change notification settings - Fork 461
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add source code - AST Node interpolation
This adds three tagged templates to the core API: - jscodeshift.template.expression - jscodeshift.template.statement - jscodeshift.template.statements Their purpose is to make it easier to replace nodes with more complex constructs, without having the build the AST yourself. For example, the following replaces all matching identifiers with a function call: ``` let {expression} = jscodeshift.template; // replaces all `bar` identifiers with the function call `foo(bar)` jscodeshift(src) .find('Identifier', {name: 'bar'}) .replaceWith(path => expression`foo(${path.node})`); ``` The difference between the three function is just how the provided code snippet is parsed: - `expression` returns an expression node - `statement` returns a statement node - `statements` returns an array of statement nodes It is now also possible to replace a node with multiple nodes. This is especially useful for replacing a single statement with multiple statements (still have to experiment with expression -> multiple expressions replacements).
- Loading branch information
Showing
6 changed files
with
297 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"optional": ["runtime"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
/*global jest, defined, it, expect, beforeEach*/ | ||
|
||
jest.autoMockOff(); | ||
|
||
let jscodeshift = require('../core'); | ||
|
||
describe('Templates', () => { | ||
let statements; | ||
let statement; | ||
let expression; | ||
|
||
beforeEach(() => { | ||
({expression, statement, statements} = require('../template')); | ||
}); | ||
|
||
it('interpolates expression nodes with source code', () => { | ||
|
||
let input = | ||
`var foo = bar; | ||
if(bar) { | ||
console.log(42); | ||
}`; | ||
|
||
let expected = | ||
`var foo = alert(bar); | ||
if(alert(bar)) { | ||
console.log(42); | ||
}`; | ||
|
||
expect( | ||
jscodeshift(input) | ||
.find('Identifier', {name: 'bar'}) | ||
.replaceWith(path => expression`alert(${path.node})`) | ||
.toSource() | ||
).toEqual(expected); | ||
}); | ||
|
||
it('interpolates statement nodes with source code', () => { | ||
let input = | ||
`for (var i = 0; i < 10; i++) { | ||
console.log(i); | ||
console.log(i / 2); | ||
}`; | ||
|
||
let expected = | ||
`var i = 0; | ||
while (i < 10) { | ||
console.log(i); | ||
console.log(i / 2); | ||
i++; | ||
}`; | ||
|
||
expect( | ||
jscodeshift(input) | ||
.find('ForStatement') | ||
.replaceWith( | ||
({node}) => statements` | ||
${node.init}; | ||
while (${node.test}) { | ||
${node.body.body} | ||
${node.update} | ||
}` | ||
) | ||
.toSource() | ||
).toEqual(expected); | ||
}); | ||
|
||
describe('explode arrays', () => { | ||
|
||
it('explodes arrays in function definitions', () => { | ||
let input = `var foo = [a, b];`; | ||
let expected = `var foo = function foo(a, b, c) {};`; | ||
|
||
expect( | ||
jscodeshift(input) | ||
.find('ArrayExpression') | ||
.replaceWith( | ||
({node}) => expression`function foo(${node.elements}, c) {}` | ||
) | ||
.toSource() | ||
) | ||
.toEqual(expected); | ||
|
||
expected = `var foo = function(a, b, c) {};`; | ||
|
||
expect( | ||
jscodeshift(input) | ||
.find('ArrayExpression') | ||
.replaceWith( | ||
({node}) => expression`function(${node.elements}, c) {}` | ||
) | ||
.toSource() | ||
) | ||
.toEqual(expected); | ||
|
||
expected = `var foo = (a, b) => {};`; | ||
|
||
expect( | ||
jscodeshift(input) | ||
.find('ArrayExpression') | ||
.replaceWith( | ||
({node}) => expression`${node.elements} => {}` | ||
) | ||
.toSource() | ||
) | ||
.toEqual(expected); | ||
|
||
expected = `var foo = (a, b, c) => {};`; | ||
|
||
expect( | ||
jscodeshift(input) | ||
.find('ArrayExpression') | ||
.replaceWith( | ||
({node}) => expression`(${node.elements}, c) => {}` | ||
) | ||
.toSource() | ||
) | ||
.toEqual(expected); | ||
}); | ||
|
||
it('explodes arrays in variable declarations', () => { | ||
let input = `var foo = [a, b];`; | ||
let expected = `var foo, a, b;`; | ||
expect( | ||
jscodeshift(input) | ||
.find('VariableDeclaration') | ||
// Need to use a block here because the arrow doesn't seem to be | ||
// compiled with a line break after the return statement. Can't repro | ||
// outside here though | ||
.replaceWith(({node: {declarations: [node]}}) => { | ||
return statement`var ${node.id}, ${node.init.elements};`; | ||
}) | ||
.toSource() | ||
) | ||
.toEqual(expected); | ||
}); | ||
|
||
it('explodes arrays in array expressions', () => { | ||
let input = `var foo = [a, b];`; | ||
let expected = `var foo = [a, b, c];`; | ||
expect( | ||
jscodeshift(input) | ||
.find('ArrayExpression') | ||
.replaceWith(({node}) => expression`[${node.elements}, c]`) | ||
.toSource() | ||
) | ||
.toEqual(expected); | ||
}); | ||
|
||
it('explodes arrays in object expressions', () => { | ||
let input = `var foo = {a, b};`; | ||
let expected = /var foo = \{\s*a,\s*b,\s*c: 42\s*};/; | ||
expect( | ||
jscodeshift(input) | ||
.find('ObjectExpression') | ||
.replaceWith(({node}) => expression`{${node.properties}, c: 42}`) | ||
.toSource() | ||
) | ||
.toMatch(expected); | ||
}); | ||
|
||
it('explodes arrays in call expressions', () => { | ||
let input = `var foo = [a, b];`; | ||
let expected = `var foo = bar(a, b, c);`; | ||
|
||
expect( | ||
jscodeshift(input) | ||
.find('ArrayExpression') | ||
.replaceWith( | ||
({node}) => expression`bar(${node.elements}, c)` | ||
) | ||
.toSource() | ||
) | ||
.toEqual(expected); | ||
}); | ||
|
||
}); | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
let babel = require('babel'); | ||
|
||
function splice(arr, element, replacement) { | ||
arr.splice(arr.indexOf(element), 1, ...replacement); | ||
} | ||
|
||
function getPlugin(varName, nodes) { | ||
let counter = 0; | ||
|
||
return function({Plugin, types: t}) { | ||
return new Plugin('template', { | ||
visitor: { | ||
Identifier: { | ||
exit: function(node, parent) { | ||
if (node.name !== varName) { | ||
return node; | ||
} | ||
|
||
let replacement = nodes[counter++]; | ||
if (Array.isArray(replacement)) { | ||
// check whether we can explode arrays here | ||
if (t.isFunction(parent) && parent.params.indexOf(node) > -1) { | ||
// function foo(${bar}) {} | ||
splice(parent.params, node, replacement); | ||
} else if (t.isVariableDeclarator(parent)) { | ||
// var foo = ${bar}, baz = 42; | ||
splice( | ||
this.parentPath.parentPath.node.declarations, | ||
parent, | ||
replacement | ||
); | ||
} else if (t.isArrayExpression(parent)) { | ||
// var foo = [${bar}, baz]; | ||
splice(parent.elements, node, replacement); | ||
} else if (t.isProperty(parent) && parent.shorthand) { | ||
// var foo = {${bar}, baz: 42}; | ||
splice( | ||
this.parentPath.parentPath.node.properties, | ||
parent, | ||
replacement | ||
); | ||
} else if (t.isCallExpression(parent) && | ||
parent.arguments.indexOf(node) > -1) { | ||
// foo(${bar}, baz) | ||
splice(parent.arguments, node, replacement); | ||
} else if (t.isExpressionStatement(parent)) { | ||
this.parentPath.replaceWithMultiple(replacement); | ||
} else { | ||
this.replaceWithMultiple(replacement); | ||
} | ||
} else if (t.isExpressionStatement(parent)) { | ||
this.parentPath.replaceWith(replacement); | ||
} else { | ||
return replacement; | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
|
||
function replaceNodes(src, varName, nodes) { | ||
return babel.transform( | ||
src, | ||
{ | ||
plugins: [getPlugin(varName, nodes)], | ||
whitelist: [], | ||
code: false, | ||
} | ||
).ast; | ||
} | ||
|
||
function getRandomVarName() { | ||
return `$jscodeshift${Math.floor(Math.random() * 1000)}$`; | ||
} | ||
|
||
export function statements(template, ...nodes) { | ||
template = [...template]; | ||
let varName = getRandomVarName(); | ||
let src = template.join(varName); | ||
return replaceNodes(src, varName, nodes).program.body; | ||
} | ||
|
||
export function statement(template, ...nodes) { | ||
return statements(template, ...nodes)[0]; | ||
} | ||
|
||
export function expression(template, ...nodes) { | ||
// wrap code in `(...)` to force evaluation as expression | ||
template = [...template]; | ||
if (template.length > 1) { | ||
template[0] = '(' + template[0]; | ||
template[template.length - 1] += ')'; | ||
} else if (template.length === 0) { | ||
template[0] = '(' + template[0] + ')'; | ||
} | ||
|
||
return statement(template, ...nodes).expression; | ||
} |