title | order |
---|---|
Basic Tutorial |
1 |
The above diagram outlines the four processes of a single code conversion, and our next tutorials will follow these four steps in sequence.
- Parse the code into an abstract syntax tree (AST)
- Find the code we want to change
- Change it to what we want it to look like
- Generate the code back into string form
First we install and introduce GoGoCode
npm install gogocode --save
import $ from 'gogocode';
// or for commonjs
const $ = require('gogocode');
We borrowed jQuery's $ naming to make the code easier to write!
To parse different types of code using GoGoCode.
// source is the string of the code to be parsed
// parse JavaScript/TypScript files
const ast = $(source);
// parsing html files requires the language to be specified in the parseOptions passed in
const ast = $(source, { parseOptions: { language: 'html' } });
// Parse the Vue file
const ast = $(source, { parseOptions: { language: 'vue' } });
Tips: The code snippets in this tutorial you can try right now in GoGoCode PlayGround!
You can switch the code type in the drop-down box as shown, and the corresponding sample code will be provided on the right side.
After parsing the code from a string into an AST, we move to the second step, finding the exact AST node we want to modify from an entire section of code.
Unlike other code conversion tools that match syntax tree nodes by AST type, GoGoCode provides a more intuitive way to "find code with code".
Suppose you want to pick the function named log in the following code.
function log(a) {
console.log(a);
}
function alert(a) {
alert(a);
}
Simply use the find method as follows.
const ast = $(source);
const test1 = ast.find('function log() {}');
GoGoCode will automatically match the function
node named log
based on function log() {}
and return the child node that meets the matching criteria.
Just call .generate
on the AST node you found, and you'll get the code string for that node.
const ast = $(source);
const test1 = ast.find('function log() {}');
const code = test1.generate()
// code is the following string.
// function log(a) {
// console.log(a);
// }
Suppose you want to pick out the declaration and initialization statements for the variable a
in the following code.
const a = 123;
As previously described, we can simply write it like the following.
const aDef = ast.find('const a = 123');
But this only matches to const a = 123
, not to const a = 456
. In real code matching, we are often not sure of the whole code, so GoGoCode supports fuzzy matching using wildcards:
const aDef = ast.find('const a = $_$0');
Replacing the original 123
with $_$0
will help you match all statements that initialize const a
:
// each of the following statements will be matched
const a = 123;
const a = b;
const a = () => 1;
// ......
The node at position $_$0
can be obtained by using the match
property of the query result.
const aDef = ast.find('const a = $_$');
const match = aDef.match;
As shown below, match
is a dictionary structure, the number after $_$
is the index of match
, and the collection of ASTs matched by $_$0
position can be retrieved by match[0]
.
This collection has only one element, corresponding to 123
in const a = 123
, and you can get the original AST node corresponding to it via node
, or the fragment of this node in the code directly via value
.
Tip: Using the debugger more often to see the intermediate results is a good way to write code conversions
Going back to this example.
function log(a) {
console.log(a);
}
function alert(a) {
alert(a);
}
If we use wildcards, we can match all function definitions by name, so the result of the .find
query could be a collection
// fns is a result set containing all function definitions by name
const fns = ast.find(`function $_$0() {}`);
This result set fns
has the same type as ast
and has exactly the same member methods; if there are multiple elements in the set, using the methods directly on them will only work on the first AST node.
We provide the each method to iterate through this result collection, and the following example collects the function names that are matched into an array named names
.
const fns = ast.find(`function $_$0() {}`);
const names = [];
fns.each((fnNode) => {
const fnName = fnNode.match[0][0].value;
names.push(fnName);
});
Sometimes we need more than one wildcard character, you can write $_$0
, $_$1
, $_$2
, $_$3
...... in the code selector to achieve your goal.
Let's say you want to match the two parameters of the following function.
sum(a, b);
const sumFn = ast.find('sum($_$0, $_$1)');
const match = sumFn.match;
console.log(`${match[0][0].value},${match[1][0].value}`); // a,b
Earlier we learned about using the $_$
wildcard to do fuzzy queries, suppose we have the following code.
console.log(a);
console.log(a, b);
console.log(a, b, c);
Their parameter lists are not the same length, what will be the result of our search with the following selectors respectively?
ast.find(`console.log()`);
ast.find(`console.log($_$0)`);
// The above two statements will find all three lines of code
ast.find(`console.log($_$0, $_$1)`);
// This statement will find the first two lines of code
ast.find(`console.log($_$0, $_$1, $_$2)`);
// This statement will only find the third line of code
You can see the principle of GoGoCode's wildcard matching: the more you write, the more restrictive the query will be.
If you want to match any number of nodes of the same type, GoGoCode provides wildcards of the form $$$
, and for the above statements with indefinite parameters you can uniformly use ast.find('console.log($$$0)')
to match.
Instead of ast.find('console.log()')
, you can use $$$
to catch all similar nodes in the placeholder by using the match property. For example, use it to match console.log(a, b, c)
.
const res = ast.find('console.log($$$0)');
const params = res.match['$$$0'];
const paramNames = params.map((p) => p.name);
// paramNames: ['a', 'b', 'c']
As before, we can get the array of nodes params
matched by the wildcard $$$0
from inside match
, the elements in this array correspond to the AST nodes a
, b
, c
respectively.
There is a lot more to $$$
than matching indefinite parameters:
Match all the keys and values of a dictionary named dict and print
const dict = {
a: 1,
b: 2,
c: 'f',
};
const res = ast.find('const dict = { $$$0 }');
const kvs = res.match['$$$0'];
kvs.map((kv) => `${kv.key.name}:${kv.value.value}`);
// a:1,b:2,c:f
We use .has
to determine if a piece of code is present in the source code, e.g.
if (ast.has(`import $_$0 from 'react'`)) {
console.log('has React!');
}
You can tell if this code imports the React package, which is actually equivalent to.
if (ast.find(`import $_$0 from 'react'`).length) {
console.log('has React!');
}
That is, determine if there is a lookup for at least one matching statement.
With the above tutorial, I'm sure you've learned how to find specific statements in your code based on code selectors and wildcards, so let's move on to step 3 and change the found statements to what we want.
We often use the "find/replace" function to do some basic operations when making bulk changes to our code in the editor, but they are all based on strings or regular expressions and are not compatible with different indents, line feeds and even with or without semicolons. AST-level code replacement can be done in a form close to string replacement.
Recall our first example: the
function log(a) {
console.log(a);
}
function alert(a) {
alert(a);
}
If we want to rename the log
function to record
, it's very simple to do it with replace
: ``
ast.replace('function log($$$0) { $$$1 }', 'function record($$$0) { $$$1 }');
replace
takes two arguments, the first is the code selector, the second is what we want to replace it with, we use $$$0
to match the list of arguments and $$$1
to match the statements inside the function body, putting them back in their original position in the second argument ensures that the only thing that has changed is the name of the function.
We often use enumeration lists such as
const list = [
{
text: 'A-strategy',
value: 1,
tips: 'Atip',
},
{
text: 'B-strategy',
value: 2,
tips: 'Btip',
},
{
text: 'C-strategy',
value: 3,
tips: 'Ctip',
},
];
One day, in order to unify the various enumerations in the code, we need to rename the text attribute to name and the value attribute to id, which is difficult to match exactly with a regular and easy to miss, with GoGoCode we just need to replace it like this
ast.replace(
'{ text: $_$1, value: $_$2, $$$0 }',
'{ name: $_$1, id: $_$2, $$$0 }',
);
where $_$1
and $_$2
match the value
node of the name, and $$
matches the rest of the nodes, kind of like ...' in es6.
, this code matches the text
and value
values and fills in the name
and id
, leaving the rest intact.
For a more complex example, make this change to a piece of code:
- import from antd instead of @alifd/next
- h2 text: Before to After
- change value of
type
in Button as follow: normal -> default,medium -> middle - change
text
in Button totype="link"
- change
warning
in Button todanger
import * as React from 'react';
import * as styles from './index.module.scss';
import { Button } from '@alifd/next';
const Btn = () => {
return (
<div>
<h2>Before</h2>
<div>
<Button type="normal">Normal</Button>
<Button type="primary">Prirmary</Button>
<Button type="secondary">Secondary</Button>
<Button type="normal" text>
Normal
</Button>
<Button type="primary" text>
Primary
</Button>
<Button type="secondary" text>
Secondary
</Button>
<Button type="normal" warning>
Normal
</Button>
</div>
</div>
);
};
export default Btn;
ast
.replace(`import { $$$0 } from "@alifd/next"`, `import { $$$0 } from "antd"`)
.replace(`<h2>转译前</h2>`, `<h2>转译后</h2>`)
.replace(
`<Button type="normal" $$$0></Button>`,
`<Button type="default" $$$0></Button>`,
)
.replace(
`<Button size="medium" $$$0></Button>`,
`<Button size="middle" $$$0></Button>`,
)
.replace(`<Button text $$$0></Button>`, `<Button type="link" $$$0></Button>`)
.replace(`<Button warning $$$0></Button>`, `<Button danger $$$0></Button>`);
If you need more freedom in the replacement, you can also pass a function to the second parameter that will take the match
dictionary as an argument and return a new piece of code to replace the matched code.
Suppose we have the following constant definition.
const stock_code_a = 'BABA';
const stock_code_b = 'JD';
const stock_code_c = 'TME';
Want to bulk change their variable names to uppercase strings:
ast.replace(`const $_$0 = $_$1`, (match, node) => {
const name = match[0][0].value;
const value = match[1][0].raw;
return `const ${name.toUpperCase()} = ${value}`;
});
In addition to replacing code with .replace
, you can also replace this statement directly with .replaceBy
after .find
has found the corresponding statement, for example, if we want to rewrite console.log(a)
inside the log
function to alert(a)
without accidentally hurting the outside statement.
function log(a) {
console.log(a);
}
console.log(a);
You can chain .find
to console.log(a)
inside the function and then replace it with .replaceBy
const console = ast.find('function log($_$0) {}').find('console.log($_$0)');
console.replaceBy('alert(a)');
Now that we've learned this, we can try to solve a more complex code conversion problem!
Here is a snippet of code from the React documentation:
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = { isToggleOn: true };
// This binding is necessary to make `this` work in the callback
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState((prevState) => ({
isToggleOn: !prevState.isToggleOn,
}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
The documentation tells us that the callback function for the React
event needs to be specially bound to this
in the constructor
, so let's remove the binding statement this.handleClick = this.handleClick.bind(this);
and consider writing a conversion logic that uses GoGoCode automatically recognizes the callback function onClick
in JSX and adds the binding statement in the constructor
for us.
The almighty .replace
is not just a simple replacement, use $$$
to capture and fill in the original content, then add the statement you want to insert to achieve the operation of inserting code, the following are the detailed steps.
const ast = $(source);
// find the statement defined by reactClass
const reactClass = ast.find('class $_$0 extends React.Component {}');
// find the tag with the onClick attribute in jsx
const onClick = reactClass.find('<$_$0 onClick={$_$1}></$_$0>');
// Create an array to collect the names of the hanlder corresponding to onClick
const clickFnNames = [];
// It is possible to find many tags with onClick, we use each to handle each one here
onClick.each((e) => {
// use match[1][0] to find the handler node corresponding
// to the first onClick attribute matched by $_$1
// take the value as the node name
// handlerName = 'this.handleClick'
const handlerName = e.match[1][0].value;
clickFnNames.push(handlerName);
});
// Replace the original constructor, but use $$$ to keep the original parameters and statements, just add the bind statement at the end
reactClass.replace(
'constructor($$$0) { $$$1 }',
`constructor($$$0) {
$$$1;
${clickFnNames.map((name) => `${name} = ${name}.bind(this)`).join(';')}
}`,
);
You can also use the .append
method to insert code, .append
supports two parameters
The first parameter is where you want to insert it, you can fill in 'params'
or 'body'
, which corresponds to inserting a new function parameter and inserting it into the block wrapped in curly braces, respectively.
Let's use .append
to achieve the same thing we just did: `.
const ast = $(source);
// find the statement defined by reactClass
const reactClass = ast.find('class $_$0 extends React.Component {}');
// find the tag with the onClick attribute in jsx
const onClick = reactClass.find('<$_$0 onClick={$_$1}></$_$0>');
// Create an array to collect the names of the hanlder corresponding to onClick
const clickFnNames = [];
// It is possible to find many tags with onClick, we use each to handle each one here
onClick.each((e) => {
// use match[1][0] to find the handler node corresponding
// to the first onClick attribute matched by $_$1
// take the value as the node name
// handlerName = 'this.handleClick'
const handlerName = e.match[1][0].value;
clickFnNames.push(handlerName);
});
/** The above code is the same as before ***/
// Find the constructor method
const constructorMethod = ast.find('constructor() {}');
// Add a bind statement to its function body
constructorMethod.append(
'body',
`
${clickFnNames.map((name) => `${name} = ${name}.bind(this)`).join(';')}
`,
);
Using .prepend
is exactly the same as using .append
, except that the statement is added to the top.
For the React component example above, if you want to add a log that prints state
before and after each setState
, you can use the .before
and .after
methods, which will insert the arguments passed in before or after the current ast node.
const ast = $(source);
const reactClass = ast.find('class $_$0 extends React.Component {}');
reactClass.find('this.setState()').each((setState) => {
setState.before(`console.log('before', this.state)`);
setState.after(`console.log('after', this.state)`);
});
After our previous efforts, we wrote a conversion program that added .bind(this)
to all the callback functions in the original code, and then you read back half a page of the documentation and found that you could write it like this
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = { isToggleOn: true };
// The following line is no longer needed
// this.handleClick = this.handleClick.bind(this)
}
// from class member method to public class fields syntax
handleClick = () => {
this.setState((prevState) => ({
isToggleOn: !prevState.isToggleOn,
}));
};
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
First, this tells us that to learn a new tool, the documentation must be read in its entirety, otherwise we will be left with regrets.
Secondly, we might consider writing another conversion tool to convert the code to this, without regrets!
Let's start by using the almighty replace
to convert the callback function handleClick() {}
to handleClick = () {}
.
const ast = $(source);
// find the statement defined by reactClass
const reactClass = ast.find('class $_$0 extends React.Component {}');
// find the tag with the onClick attribute in jsx
const onClick = reactClass.find('<$_$0 onClick={$_$1}></$_$0>');
// Create an array to collect the names of the hanlder corresponding to onClick
const clickFnNames = [];
// It is possible to find many tags with onClick, we use each to handle each one here
onClick.each((e) => {
// use match[1][0] to find the handler node corresponding
// to the first onClick attribute matched by $_$1
// take the value as the node name
// handlerName = 'this.handleClick'
const handlerName = e.match[1][0].value;
clickFnNames.push(handlerName);
});
clickFnNames.forEach((name) => {
// Eliminate the preceding this. Get the pure function name
const fnName = name.replace('this.', '');
// change class method to public class fields syntax
reactClass.replace(
`${fnName}() {$$$0}`,
`${fnName} = () => {
$$$0
}`,
);
});
Now let's see how to remove the original .bind(this)
statement.
The easiest way to delete a statement is to replace it with an empty one by replacing it with
clickFnNames.forEach((name) => {
// Get the pure function name by eliminating the preceding this.
const fnName = name.replace('this.', '');
// change class method to public class fields syntax
reactClass.replace(
`${fnName}() {$$$0}`,
`${fnName} = () => {
$$$0
}`,
);
// Remove the original bind
reactClass.replace(`this.${fnName} = this.${fnName}.bind(this)`, ``);
});
Alternatively, you can do the same thing by looking before calling the .remove
method:
clickFnNames.forEach((name) => {
// Get the pure function name by eliminating the preceding this.
const fnName = name.replace('this.', '');
// change class method to public class fields syntax
reactClass.replace(
`${fnName}() {$$$0}`,
`${fnName} = () => {
$$$0
}`,
);
// Remove the original bind
reactClass.find(`this.${fnName} = this.${fnName}.bind(this)`).remove();
});
The above is a basic tutorial on how to do code conversion with GoGoCode, thank you for your patience to see this, if you still have questions in the process, you can check our API documentation and Cookbook, good luck with your code conversion!