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.