preface

After reading a blog about how Babel works and how to write a plug-in for Babel, I wrote a simple plug-in for Babel. The function of the plug-in is to convert the expressions in the code string directly into the corresponding calculation results. For example, const code = const result = 1 + 1 converts to const code = const result = 2. Of course, this article is very superficial, but it is enough to understand the principles of Babel and the basic concepts of AST.

A link to the

  • Babel plug-in documentation
  • You can view the AST abstract syntax tree at any time

Plugin source code


const t = require('babel-types')

const visitor = {
  // Visitors to nodes of binary expression type
  BinaryExpression(path) { 
    / / child nodes
    // The visitor walks through the AST abstract syntax tree layer by layer, traversing the AST nodes of type BinaryExpression
    const childNode = path.node
    let result = null
    if (
      t.isNumericLiteral(childNode.left) &&
      t.isNumericLiteral(childNode.right)
    ) {
      const operator = childNode.operator
      switch (operator) {
        case '+':
          result = childNode.left.value + childNode.right.value
          break
        case The '-':
          result = childNode.left.value - childNode.right.value
          break
        case '/':
          result = childNode.left.value / childNode.right.value
          break
        case The '*':
          result = childNode.left.value * childNode.right.value
          break}}if(result ! = =null) {
      // Replace this object with a numeric type
      path.replaceWith(
        t.numericLiteral(result)
      )
      if (path.parentPath) {
        const parentType = path.parentPath.type
        if (visitor[parentType]) {
          visitor[parentType](path.parentPath)
        }
      }
    }
  },
  // Attribute expression
  MemberExpression(path) {
    const childNode = path.node
    let result = null
    if (
      t.isIdentifier(childNode.object) &&
      t.isIdentifier(childNode.property) &&
      childNode.object.name === 'Math'
    ) {
      result = Math[childNode.property.name]
    }
    if(result ! = =null) {
      const parentType = path.parentPath.type
      if(parentType ! = ='CallExpression') {
        // Replace this object with a numeric type
        path.replaceWith(
          t.numericLiteral(result)
        )
        if (visitor[parentType]) {
          visitor[parentType](path.parentPath)
        }
      }
    }
  },
  // unary expression
  UnaryExpression (path) {
    const childNode = path.node
    let result = null
    if (
      t.isLiteral(childNode.argument)
    ) {
      const operator = childNode.operator
      switch (operator) {
        case '+':
          result = childNode.argument.value
          break
        case The '-':
          result = -childNode.argument.value
          break}}if(result ! = =null) {
      // Replace this object with a numeric type
      path.replaceWith(
        t.numericLiteral(result)
      )
      if (path.parentPath) {
        const parentType = path.parentPath.type
        if (visitor[parentType]) {
          visitor[parentType](path.parentPath)
        }
      }
    }
  },
  // The function executes the expression
  CallExpression(path) {
    const childNode = path.node
    / / the result
    let result = null
    // A collection of parameters
    let args = []
    // Get the set of arguments to the function
    args = childNode.arguments.map(arg= > {
      if (t.isUnaryExpression(arg)) {
        return arg.argument.value
      }
    })
    if (
      t.isMemberExpression(childNode.callee)
    ) {
      if( t.isIdentifier(childNode.callee.object) && t.isIdentifier(childNode.callee.property) && childNode.callee.object.name = = ='Math'
      ) {
        result = Math[childNode.callee.property.name].apply(null, args)
      }
    }
    if(result ! = =null) {
      // Replace this object with a numeric type
      path.replaceWith(
        t.numericLiteral(result)
      )
      if (path.parentPath) {
        const parentType = path.parentPath.type
        if (visitor[parentType]) {
          visitor[parentType](path.parentPath)
        }
      }
    }
  }
}

module.exports = function () {
  return {
    visitor
  }
}
Copy the code

The basic concept

I recommend reading this document first

How Babel works

Babel transforms the code, converting THE JS code into an AST abstract syntax tree (parsing), statically analyzing the tree (transforming), and then converting the syntax tree into JS code (generating). Each layer of the tree is called a node. Each layer node has a type attribute that describes the type of node. Other attributes are used to further describe the type of node.

// Generate the corresponding abstract syntax tree

/ / code
const result = 1 + 1

// Code generated AST
{
  "type": "Program"."start": 0."end": 20."body": [{"type": "VariableDeclaration"."start": 0."end": 20."declarations": [{"type": "VariableDeclarator"."start": 6."end": 20."id": {
            "type": "Identifier"."start": 6."end": 12."name": "result"
          },
          "init": {
            "type": "BinaryExpression"."start": 15."end": 20."left": {
              "type": "Literal"."start": 15."end": 16."value": 1."raw": "1"
            },
            "operator": "+"."right": {
              "type": "Literal"."start": 19."end": 20."value": 1."raw": "1"}}}]."kind": "const"}]."sourceType": "module"
}
Copy the code

parsing

Parsing is divided into lexical parsing, which generates token streams from code strings, and parsing, which converts token streams into AST abstract syntax trees

conversion

A node’s path object exposes many apis for adding, deleting, and modifying the AST. You can modify the AST by manipulating these apis

generate

Generation generates new source code by traversing the modified AST

traverse

The AST is a tree structure, and the transformation steps of the AST are realized through the traversal of the AST by visitors. Visitors define methods to handle different node types. When traversing the tree structure, the corresponding node type is encountered and the corresponding method is executed.

The visitor

Visitors is an object in itself, and different attributes on the object correspond to different AST node types. For example, The AST has nodes of type BinaryExpression. If you define a method of the BinaryExpression property name on the visitor, that method will be executed when it encounters a node of type BinaryExpression. The BinaryExpression method takes the path to the object. Note that each node is traversed twice, once to enter and once to exit the node


const visitors = {
  enter (path) {
    // Enter the node
  },
  exit (path) {
    // Exit the node}}Copy the code

The path

Each node has its own path object (the visitor’s argument is the node’s path object) with different properties and methods defined on the path object. For example, path.node represents the child node of the node, and path.parent represents the parent node of the node. The path.replaceWithMultiple method defines the method to replace the node.

Paths in visitors

The node’s path information is contained in the visitor’s parameter, which is the node’s path object by default

The first plug-in

Let’s write a simple plug-in that parses a const result = 1 + 1 string to const result = 2. Let’s first look at the AST of this code, as follows.

We can see that the node of type BinaryExpression defines the body of the expression (1 + 1). 1 is the child of the BinaryExpression node left, and the child of the BinaryExpression node right, respectively. The plus sign is the child of the operator of the BinaryExpression node


// After simplification
{
  "type": "Program"."body": [{"type": "VariableDeclaration"."declarations": [{"type": "VariableDeclarator"."id": {
            "type": "Identifier"."name": "result"
          },
          "init": {
            "type": "BinaryExpression"."left": {
              "type": "Literal"."value": 1
            },
            "operator": "+"."right": {
              "type": "Literal"."value": 1}}}]}Copy the code

Next we deal with nodes of this type as follows


const t = require('babel-types')


const visitor = {
  BinaryExpression(path) { 
    // BinaryExpression child of the node
    const childNode = path.node
    let result = null
    if (
      // isNumericLiteral is a method defined on babel-types to determine the node type
      t.isNumericLiteral(childNode.left) &&
      t.isNumericLiteral(childNode.right)
    ) {
      const operator = childNode.operator
      Left. Value, right. Value are processed to different results depending on the operator
      switch (operator) {
        case '+':
          result = childNode.left.value + childNode.right.value
          break
        case The '-':
          result = childNode.left.value - childNode.right.value
          break
        case '/':
          result = childNode.left.value / childNode.right.value
          break
        case The '*':
          result = childNode.left.value * childNode.right.value
          break}}if(result ! = =null) {
      // Calculate the result
      // Replace its own node with a numeric node
      path.replaceWith(
        t.numericLiteral(result)
      )
    }
  }
}
Copy the code

We define a visitor method on which to define the attribute of BinaryExpression. As expected, const result = 1 + 1 is treated as const result = 2. But we changed our code to const result = 1 + 2 + 3 and found that it was const result = 3 + 3. Why? Let’s look at the 1 + 2 + 3 AST abstract syntax tree.


// Simplified AST

type: 'BinaryExpression'
  - left
    - left
      - left
        type: 'Literal'
        value: 1
      - opeartor: '+'
      - right
        type: 'Literal'
        value: 2
    - opeartor: '+'
    - right
      type: 'Literal'
      value: 3


Copy the code

The criteria for our code above is. T.isnumericliteral (childNode.left) &&t.isnumericLiteral (childNode.right), where only the innermost AST is eligible. Because the entire AST structure is similar, (1 + 2) + 3 => (left + rigth) + right.

The solution is to replace the internal 1 + 2 nodes with the numeric node 3, and then re-execute the BinaryExpression method (the numeric 3 nodes and the right node) on the parentPath of the numeric node 3 to replace all nodes ina recursive way. The modified code looks like this.


BinaryExpression(path) { 
  const childNode = path.node
  let result = null
  if (
    t.isNumericLiteral(childNode.left) &&
    t.isNumericLiteral(childNode.right)
  ) {
    const operator = childNode.operator
    switch (operator) {
      case '+':
        result = childNode.left.value + childNode.right.value
        break
      case The '-':
        result = childNode.left.value - childNode.right.value
        break
      case '/':
        result = childNode.left.value / childNode.right.value
        break
      case The '*':
        result = childNode.left.value * childNode.right.value
        break}}if(result ! = =null) {
    // Replace this object with a numeric type
    path.replaceWith(
      t.numericLiteral(result)
    )
    BinaryExpression(path.parentPath)
  }
}
Copy the code

Const result = 1 + 2 + 3 resolves as expected. However, the plugin does not yet have processing for math.abs (), math.pi, and signed numbers, and we need to define more attributes on visitors. Finally, you can refer to the source code above for handling the math. abs function.