What is an Abstract Syntax Tree?

Many tools and libraries, such as WebPack and Lint, have an AST at their core, and use the concept of an AST syntax tree to examine code and analyze it. Of course, you can write your own similar tools if you have a good understanding of AST.Copy the code

The principle of these tools is that Javascript Parser transforms the code into an abstract syntax tree, which defines the structure of the code. By manipulating this tree, we can accurately locate the life statements, assignment statements, operation statements, etc., and realize the code analysis, optimization, and change operations

In computer science, the abstract syntax tree is a tree representation of the abstract syntactic structure of source code (the source code of a programming language).

Javascript syntax is designed to make programming easier for developers, but it is not well understood by programs. Therefore, it is necessary to convert the AST syntax tree to be more suitable for program analysis. The browser compiler usually converts the source code into the AST for analysis and other operations.

Purpose of Abstract Syntax Tree

  • Can be code grammar, style check; Formatting, highlighting, error prompts, auto-completion of code, such as JSLint/JSHint checks for code errors or styles.

  • Code obfuscation compression

UglifyJS2 etc.

  • Optimization changes code, changes code structure
  1. Code packaging tools webpack, rollup, and more
  2. CommonJS, AMD, CMD, UMD, etc
  3. CoffeeScript, TypeScript, JSX, etc

How to use ast

Uses Esprima, EscodeGen, and estraverse to implement ast operations

That’s what we did before Babel

  1. Parse our javascirpt syntax AST

Esprima: esprima.org/demo/parse….

astexplorer.net/

  1. Tree Traversal (Depth-first traversal) changes the tree structure

estraverse:

  1. Generate new content

escodegen

npm install esprima estraverse escodegen

Js statements

function ast() {}
Copy the code

Esprima compiled

{
  "type": "Program"."body": [{"type": "FunctionDeclaration"."id": {
        "type": "Identifier"."name": "ast"
      },
      "params": []."body": {
        "type": "BlockStatement"."body": []},"generator": false."expression": false."async": false}]."sourceType": "script"
}
Copy the code

Esprima Estraverse EscodeGen implementation steps

let esprima = require("esprima");
let estraverse = require('estraverse');
let escodegen = require('escodegen');
let code = `function ast() {}`
let tree = esprima.parseScript(code);
estraverse.traverse(tree, {
    enter(node) {
        console.log('enter', node.type);
        if(node.type === 'Identifier') {
            node.name = 'tom'; }}})let r = escodegen.generate(tree);
console.log(r);
//-----------------------------------
enter Program
enter FunctionDeclaration
enter Identifier
enter BlockStatement

function tom() {}Copy the code

The manipulation syntax tree is implemented directly with Babel

Implement babel-plugin-arrow-fucntion (arrow function converted to ES5)

The arrow function becomes a normal function

  • Arrow function ast type > ArrowFunctionExpression
  1. Turn the ast: Babel – core
  2. Change the ast: Babel – types

Ast of the arrow function


let sum = (a, b) = > { return a + b };
Copy the code
{
  "type": "Program"."body": [{"type": "VariableDeclaration"."declarations": [{"type": "VariableDeclarator"."id": {
            "type": "Identifier"."name": "sum"
          },
          "init": {
            "type": "ArrowFunctionExpression"."id": null."params": [{"type": "Identifier"."name": "a"
              },
              {
                "type": "Identifier"."name": "b"}]."body": {
              "type": "BlockStatement"."body": [{"type": "ReturnStatement"."argument": {
                    "type": "BinaryExpression"."operator": "+"."left": {
                      "type": "Identifier"."name": "a"
                    },
                    "right": {
                      "type": "Identifier"."name": "b"}}}]},"generator": false."expression": false."async": false}}]."kind": "let"}]."sourceType": "script"
}
        ` `Ast of es5 named functions (return values with code blocks)` `js
let babel = require('babel-core');
let t = require('babel-types');

let code = `let sum = (a, b) => { return a + b }`;

//.babelrc
let ArrowPlugin = {
    vistor: {
    //path: tree type
        ArrowFunctionExpresstion(path) {
            // Node matched by path
            let node = path.node;
            let params = path.params;
            let body = node.body;
            // Generate a function expression
            let funcs = t.functionExpression(null, params, body, false.false); path.replaceWith(funcs); }}}let r = babel.transform(code, {
    plugins: [
        ArrowPlugin
    ]
})

console.log(r.code);
/* codee: let sum = (a, b) => {return a + b}; Let sum = function(a, b) {return a + b}; * /

Copy the code

Next compatible generate ast that returns value without code block

let babel = require('bable-core');
let t = require('babel-types');

let code = `let sum = (a + b) => a + b`

let ArrowPlugin = {
    vistor: {
        // Find the type of tree
        ArrowFunctionExpression(path) {
            let node = path.node;
            let params = path.params;
            let body = node.body;
            // Generate a function expression
            // Check whether the body has a code block
            if(! t.isBlockStatement(body)){// If it is not a block of code, it is the case above
                let returnStatement = t.returnStatement(body);
                body = t.blockStatement([returnStatement]);
            }
            
            let func = t.functionExpression(null, params, body, false.false) path.replaceWith(funcs); }}}let r = babel.transform(code, {
    plugins: [ArrowPlugin]
})

/* let sum = function() { return a + b; } * /
Copy the code

Plugin ES6 > ES5

  • es5
function Person(name) {
    this.name = name;
}
Person.prototype.getName = function() {
    return this.name;
}
Copy the code
  • es5 asts off
{
  "type": "Program"."body": [{"type": "FunctionDeclaration"."id": {
        "type": "Identifier"."name": "Person"
      },
      "params": [{"type": "Identifier"."name": "name"}]."body": {
        "type": "BlockStatement"."body": [{"type": "ExpressionStatement"."expression": {
              "type": "AssignmentExpression"."operator": "="."left": {
                "type": "MemberExpression"."computed": false."object": {
                  "type": "ThisExpression"
                },
                "property": {
                  "type": "Identifier"."name": "name"}},"right": {
                "type": "Identifier"."name": "name"}}}]},"generator": false."expression": false."async": false
    },
    {
      "type": "ExpressionStatement"."expression": {
        "type": "AssignmentExpression"."operator": "="."left": {
          "type": "MemberExpression"."computed": false."object": {
            "type": "MemberExpression"."computed": false."object": {
              "type": "Identifier"."name": "Person"
            },
            "property": {
              "type": "Identifier"."name": "prototype"}},"property": {
            "type": "Identifier"."name": "getName"}},"right": {
          "type": "FunctionExpression"."id": null."params": []."body": {
            "type": "BlockStatement"."body": [{"type": "ReturnStatement"."argument": {
                  "type": "MemberExpression"."computed": false."object": {
                    "type": "ThisExpression"
                  },
                  "property": {
                    "type": "Identifier"."name": "name"}}}]},"generator": false."expression": false."async": false}}}]."sourceType": "script"
}
Copy the code
  • es6
class Person {
    constructor(name) {
        this.name = name;
    }
    getName() {
        return this.name; }}Copy the code
  • es6 ast
{
  "type": "Program"."body": [{"type": "ClassDeclaration"."id": {
        "type": "Identifier"."name": "Person"
      },
      "superClass": null."body": {
        "type": "ClassBody"."body": [{"type": "MethodDefinition"."key": {
              "type": "Identifier"."name": "constructor"
            },
            "computed": false."value": {
              "type": "FunctionExpression"."id": null."params": [{"type": "Identifier"."name": "name"}]."body": {
                "type": "BlockStatement"."body": [{"type": "ExpressionStatement"."expression": {
                      "type": "AssignmentExpression"."operator": "="."left": {
                        "type": "MemberExpression"."computed": false."object": {
                          "type": "ThisExpression"
                        },
                        "property": {
                          "type": "Identifier"."name": "name"}},"right": {
                        "type": "Identifier"."name": "name"}}}]},"generator": false."expression": false."async": false
            },
            "kind": "constructor"."static": false
          },
          {
            "type": "MethodDefinition"."key": {
              "type": "Identifier"."name": "getName"
            },
            "computed": false."value": {
              "type": "FunctionExpression"."id": null."params": []."body": {
                "type": "BlockStatement"."body": [{"type": "ReturnStatement"."argument": {
                      "type": "MemberExpression"."computed": false."object": {
                        "type": "ThisExpression"
                      },
                      "property": {
                        "type": "Identifier"."name": "name"}}}]},"generator": false."expression": false."async": false
            },
            "kind": "method"."static": false}]}}],"sourceType": "script"
}
Copy the code

Operation AST implementation

let babel = require('babel-core');
let t = require('babel-types');

let classPlugin = {
    vistor = {
        ClassDeclaration(path) {
            let node = path.node;
            let className = path.id.name;
            let classList = node.body.body;
            
            // Convert className to an identifier
            className = t.identifier(className);
            // Generate a function body
            let funcs = t.functionDeclaration(className, [], t.blockStatement([]), false.false);
            
            let es5Func = [];
            classList.forEach((item, index) = > {
                // The body of the function
                let body = classList[index].body;
                if(item.kind === 'constructor') {
                // If it is a constructor, generate a new function instead of the default empty function
                    let params = item.params.length ? item.params.map(item= > item.name) : [];
                    params = t.identifier(params);
                    funcs = t.functionDeclaration(className, [params], body, false.false);
                }else {
                // The prototype method
                    let protoObj = t.memberExpression(className, t.identifier("prototype"));
                    let left = t.merberExpression(protoObj, t.identifier(item.key.name));
                    let right = t.functionExpression(null, [], body, false.false);
                    let assign = t.assignmentExpression('=', left, right)
                    // Methods on multiple prototypesesFunc.push(assign); }});if(esFunc.length === 0) {
                path.replaceWith(funcs);
            }else {
                es5Func.push(funcs);
                path.replaceWithMultiple(esFuncs);
            }
            
        }
    }
}

babel.transform(code, {
    plugins: [
        classPlugin
    ]
})

/* function Person(name) { this.name = name; } Person.prototype.getName = function() { return this.name; } * /
Copy the code

conclusion

Now, if you are reading this carefully, you should know how to write a Babel and plugin. It is the process of manipulating the AST syntax tree, modifying it, and rendering it into a new AST syntax tree. The thinking and logic required for this process are the principles of Babel and Plugin.