In the past year, our new open source code conversion tool GoGoCode has won the love and support of many friends in the community. In such a niche field, we have obtained 2.7K stars and enthusiastic user feedback, which makes us feel that “code conversion” has a much wider demand than we imagined. It’s just that it’s always been a hidden technology.

So in 2022, we spent some time writing a modern code-conversion tutorial from scratch, trying to live up to GoGoCode’s slogan: Code-conversion has never been so easy.

It’s a bit long, a bit full of examples, so be prepared.

The basic flow of a code conversion

The diagram above summarizes the four steps of a code transformation, which we will follow in the rest of the tutorial:

  1. Parsing code into abstract Syntax trees (AST)
  2. Find the code we want to change
  3. Make it the way we want it to be
  4. Regenerating it back into string code

The code is read and parsed by GoGoCode

First we install and import GoGoCode

npm install gogocode --save
Copy the code
import $ from 'gogocode';
// or for commonjs
const$=require('gogocode');
Copy the code

We borrowed jQuery’s $name to make the code easier to write!

Parsing different types of code using GoGoCode:

// source is a string of code to be parsed

// Parse JavaScript/TypScript files
const ast = $(source);

// Parsing HTML files requires specifying the language in the passed parseOptions
const ast = $(source, { parseOptions: { language: 'html'}});// Parse the Vue file
const ast = $(source, { parseOptions: { language: 'vue'}});Copy the code

Tips: The code snippets in this tutorial you can try on GoGoCode PlayGround right away!

The code type can be switched in the drop-down box shown in the figure, and the corresponding boilerplate code is provided on the right.

Select the code through the code selector

After parsing the code from the string into the AST, we move on to the second step, which is to look through the entire code to find exactly which AST node we want to modify.

Ast.find code selector

Unlike other transcoding tools that use AST types to match syntax tree nodes, GoGoCode provides a more intuitive way to “find code by code”. Like jQuery, you just need to write a code snippet as a “code selector.” GoGoCode will intelligently help you match the source code to the fragments that match it.

Suppose you wanted to pick a function named log in the following code:

function log(a) {
  console.log(a);
}

function alert(a) {
  alert(a);
}
Copy the code

Simply use the find method as follows:

const ast = $(source);
const test1 = ast.find('function log() {}');
Copy the code

Function log() {} will automatically match the function node named log, and return the child node that matches the condition.

Output the node as a code string using generate

Simply call.generate on finding the AST node to 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);
// }
Copy the code

Playgroup online demo

$_ $wildcards

Suppose you want to pick out the declaration and initialization statements for variable A in the following code:

const a = 123;
Copy the code

As previously introduced, we can simply write as follows:

const aDef = ast.find('const a = 123');
Copy the code

Const a = 123, const a = 456, const a = 123, const a = 456, const a = 123

const aDef = ast.find('const a = $_$0');
Copy the code

$_$0 instead of 123 will help you match all statements that initialize const a:

// Each of the following can be matched
const a = 123;
const a = b;
const a = () = > 1;
/ /...
Copy the code

$_$0 = 0; $_$0 = 0;

const aDef = ast.find('const a = $_$');
const match = aDef.match;
Copy the code

As shown in the following figure, match is a dictionary structure, and the number after $_$is the index of match. Match [0] can fetch the AST matching $_$0.

This set has only one element, corresponding to 123 of const a = 123. You can retrieve the original AST node via Node, or retrieve the fragment from the code directly via value.

Using the debugger to see the intermediate results is a good way to write code transformations

Playgroup online demo

Set operations

Back to this example:

function log(a) {
  console.log(a);
}

function alert(a) {
  alert(a);
}
Copy the code

If we use wildcards, we can match function definitions with all names, so the result of a.find query might be a collection

// FNS is a result set that contains function definitions with all names
const fns = ast.find(`function $_$0() {}`);
Copy the code

The result set FNS has exactly the same type as the AST and has exactly the same member methods. If there are more than one element in the set, using methods directly on it will only work on the first AST node.

We provide the each method to iterate over the result set, and the following example collects the function names from match 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);
});
Copy the code

Playgroup online demo

Use multiple wildcards

Sometimes we need more than one wildcard, you can write $_$0, $_$1, $_$2, $_$3 in the code selector… Get what you want.

Let’s say you want to match two arguments to the following function:

sum(a, b);
Copy the code
const sumFn = ast.find('sum($_$0, $_$1)');
const match = sumFn.match;
console.log(`${match[0] [0].value}.${match[1] [0].value}`); // a,b
Copy the code

Playgroup online demo

Matches multiple nodes of the same type

We learned how to use the $_$wildcard to do fuzzy queries, assuming the following code:

console.log(a);

console.log(a, b);

console.log(a, b, c);
Copy the code

Their argument lists are inconsistent in length. What happens if we use the following selection selectors to search for them?

ast.find(`console.log()`);
ast.find(`console.log($_$0)`);
// The above two statements find all three lines of code

ast.find(`console.log($_$0, $_$1)`);
// This statement finds the first two lines of code

ast.find(`console.log($_$0, $_$1, $_$2)`);
// This statement only finds the third line of code
Copy the code

You can see GoGoCode’s wildcard matching principle: The more you write, the more limited the query.

If you want to match any number of nodes of the same type, GoGoCode provides wildcards in the form of $$$, and you can use ast.find(‘console.log($$$0)’) to match any of the above unspecified statements.

Instead of ast.find(‘console.log()’), you can use $$$to capture all similar nodes in placeholders via the match attribute. 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']
Copy the code

$$$0 = params; $$$0 = params; $$$0 = params;

Playgroup online demo

In addition to matching variable length parameters, $$$can come into play in a number of ways:

Matches and prints all keys and values named dict dictionary

const dict = {
  a: 1.b: 2.c: 'f'};Copy the code
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
Copy the code

Playgroup online demo

Ast. has determines whether the code exists

We can use.has to determine whether a piece of code exists in the source code, for example:

if (ast.has(`import $_$0 from 'react'`)) {
  console.log('has React! ');
}
Copy the code

This code imports the React package.

if (ast.find(`import $_$0 from 'react'`).length) {
  console.log('has React! ');
}
Copy the code

That is, to determine whether at least one match has been found.

Replace the code

Now that you’ve seen how to find a particular statement in your code based on code selectors and wildcards, let’s move on to the third step, which is to change the found statement to what we want it to look like.

Everything the replace

Everyday we batch modify the code in the editor will often to use the find/replace function to do some basic operations, but they are based on a string or a regular expression, for different indentation, line breaks, and even add do not add a semicolon are not compatible, and use of GoGoCode code selector feature the replace method, Allows you to do AST level code substitution in a way that approximates string substitution.

The function name

Recall our first example:

function log(a) {
  console.log(a);
}

function alert(a) {
  alert(a);
}
Copy the code

If we want to rename the log function to record, using replace is very simple:

ast.replace('function log($$$0) { $$$1 }'.'function record($$$0) { $$$1 }');
Copy the code

Playgroup online demo

Replace takes two arguments. The first argument is the code selector, and the second argument is what we want to replace it with. We use $$$0 to match the argument list and $$$1 to match the statements in the function body.

Enumeration list property changed name

We often use enumerated lists like this:

const list = [
  {
    text: 'A strategy'.value: 1.tips: 'Atip'}, {text: 'strategy B'.value: 2.tips: 'Btip'}, {text: 'C strategies'.value: 3.tips: 'Ctip',},];Copy the code

One day, in order to unify the enumerations in the code, we need to rename the text attribute to name and the value attribute to ID, which is difficult to match accurately with the re and prone to accidental damage. With GoGoCode, we just need to replace it like this:

ast.replace(
  '{ text: $_$1, value: $_$2, $$$0 }'.'{ name: $_$1, id: $_$2, $$$0 }',);Copy the code

$_$1 = $_$2; $_$1 = $_$2; So this code matches the text and the value and gives the name and the ID, and puts the rest back.

Playgroup online demo

JSX attribute replacement

To take a more complex example, make this change to a piece of code:

  • Import from @alifd/next to ANTD
  • Change before translation to after translation
  • Button type: normal -> default, medium -> middle
  • Button type=”link”
  • Button warning parameter changed to DANGER
import * as React from 'react';
import * as styles from './index.module.scss';
import { Button } from '@alifd/next';

const Btn = () = > {
  return (
    <div>
      <h2>Before translation</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;
Copy the code
ast
  .replace(`import { $$$0 } from "@alifd/next"`.`import { $$$0 } from "antd"`)
  .replace('

before translation

'
.'

translated

'
) .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>`); Copy the code

Playgroup online demo

Make a more complicated substitution with a function

If you need more freedom in the substitution, you can also pass and a function to the second argument, which will receive the match dictionary as an argument and return a new 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';
Copy the code

Want to batch 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}`;
});
Copy the code

Playgroup online demo

replaceBy

Instead of replacing the code with.replace, you can replace the statement directly with.replaceby after finding the corresponding statement in.find. For example, we want to replace console.log(a) with alert(a) in the following log function without damaging the following statement:

function log(a) {
  console.log(a);
}

console.log(a);
Copy the code

You can find console.log(a) in the function body by using.find chain and replace it with.replaceby

const console = ast.find('function log($_$0) {}').find('console.log($_$0)');

console.replaceBy('alert(a)');
Copy the code

Playgroup online demo

Insert the code

Learning here, we can try to solve a bit more complex code conversion problem!

Here is the code for the React document:

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>); }}Copy the code

HandleClick = this.handleclick. bind(this); Delete and consider writing a conversion logic that uses GoGoCode to automatically recognize JSX’s onClick callback and fill in the bind statement from constructor.

Insert code skillfully with replace

The universal.replace is not only a simple replacement, but also a reasonable use of $$$to capture and fill the original content, and then fill the statement you want to insert to achieve the operation of inserting code, the following is the detailed operation steps:

const ast = $(source);

// Find the statement defined by reactClass
const reactClass = ast.find('class $_$0 extends React.Component {}');

// Find the JSX tag with the onClick attribute
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, so we use each to handle each tag
onClick.each((e) = > {
  // match[1][0] to find the handler node corresponding to the first onClick attribute matched by $_$1
  // Set value to the node name
  // handlerName = 'this.handleClick'
  const handlerName = e.match[1] [0].value;
  clickFnNames.push(handlerName);
});

// Replace the constructor statement with $$$. // Add the bind statement at the end
reactClass.replace(
  'constructor($$$0) { $$$1 }'.`constructor($$$0) { 
    $$$1;
    ${clickFnNames.map((name) => `${name} = ${name}.bind(this)`).join('; ')}} `,);Copy the code

Playgroup online demo

Append, prepend adds two lines to the function

You can also insert code using the.append method, which supports two arguments

The first parameter is the position you want to insert. You can fill in ‘params’ or ‘body’, corresponding to inserting a new function parameter and inserting it into the block surrounded by braces.

We do the same thing with.append:

const ast = $(source);

// Find the statement defined by reactClass
const reactClass = ast.find('class $_$0 extends React.Component {}');

// Find the JSX tag with the onClick attribute
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, so we use each to handle each tag
onClick.each((e) = > {
  // match[1][0] to find the handler node corresponding to the first onClick attribute matched by $_$1
  // Set value to the node name
  // handlerName = 'this.handleClick'
  const handlerName = e.match[1] [0].value;
  clickFnNames.push(handlerName);
});

/** Same code 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('; ')}
  `.);
Copy the code

Playgroup online demo

Using.prepend is exactly the same as using.append, except that the statement is added first.

Playgroup online demo

Insert the code before and after using before and after

For the React component example above, if you want to add a log to print the state before and after each setState, you can use the.before and.after methods, which will insert the parameters 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)`);
});
Copy the code

Playgroup online demo

Delete the code

As a result of our previous efforts, we wrote a conversion program that added.bind(this) to all the callbacks in the original code, and then you read half a page later and realize that you can write something like this:

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isToggleOn: true };
    // The next line is no longer needed
    // this.handleClick = this.handleClick.bind(this)
  }

  // Public class fields syntax is changed from member method
  handleClick = () = > {
    this.setState((prevState) = > ({
      isToggleOn: !prevState.isToggleOn,
    }));
  };

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>); }}Copy the code

First of all, it tells us that when learning a new tool, we must read all the documentation, or we will be left with regrets.

Second, we can consider writing another conversion tool to transform the code into this without regret!

HandleClick = () {}; handleClick = () {}; handleClick = () {};

const ast = $(source);

// Find the statement defined by reactClass
const reactClass = ast.find('class $_$0 extends React.Component {}');

// Find the JSX tag with the onClick attribute
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, so we use each to handle each tag
onClick.each((e) = > {
  // match[1][0] to find the handler node corresponding to the first onClick attribute matched by $_$1
  // Set value to the node name
  // handlerName = 'this.handleClick'
  const handlerName = e.match[1] [0].value;
  clickFnNames.push(handlerName);
});

clickFnNames.forEach((name) = > {
  // Discard the this. to get the pure function name
  const fnName = name.replace('this.'.' ');

  // change class method to public class fields syntax
  reactClass.replace(
    `${fnName}` () $$${0}.`${fnName}= () => {$$$0} ',); });Copy the code

Playgroup online demo

Then let’s see how to delete the.bind(this) statement.

Remove the code with replace

The easiest way to delete a statement is to replace it with empty:

clickFnNames.forEach((name) = > {
  // Discard the this. to get the pure function name
  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)`.` `);
});
Copy the code

Playgroup online demo

Remove code with remove

Alternatively, you can do the same thing by looking for the.remove method and then calling it:

clickFnNames.forEach((name) = > {
  // Discard the this. to get the pure function name
  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();
});
Copy the code

Playgroup online demo

Thank you for your patience. If you still have any questions, please refer to our API documentation and Cookbook. Good luck with your code conversion!

Finally ask for a wave of star support for our project: GoGoCode ^_^

If you have any questions or suggestions, please come to our nail group: 34266233