AST相关API详解

一、准备工作

  • 在线工具网站

  • NodeJS安装:

  • Babel相关组件安装

    • 首先在项目目录下进行初始化

      npm init -y
    • 然后安装babel相关组件

      npm i @babel/parser @babel/traverse @babel/types @babel/generator @babel/template
  • 代码基本结构

    cost fs = require('fs')
    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default
    const types = require('@babel/types')
    // const t = require('@babel/types')  对于types,导包后的名字通常是types或者t
    const generator = require('@babel/generator').default
    
    // 读取js文件
    const code = fs.readFileSync('./demo.js', {encoding: 'utf-8'})
    
    // 解析为AST抽象语法树(解析)
    const ast = parser.parse(code)
    
    // 对AST进行一系列操作(转化)
    // traverse(ast, visitor)
    // ...
    
    // 根据转化后的AST,生成目标代码(生成)
    const newCode = generator(ast).code
    
    // 将目标代码写入新文件
    fs.writeFile('./newDemo.js', newCode, err => {})

二、Babel中的组件

  • parser与generator

    • parser组件用来将JS转换成AST(即File节点)

      • 需要注意parser.parse()方法有第二个参数
        const ast = parser.parse(code, {
            sourceType: 'moudle', // 默认为script,当解析的代码中含有'import', 'export'等关键字时,需要指定为moudle,否则会报错
        })
    • generator组件用来将AST转换为JS

      • generator()方法返回的是一个对象,其code属性才是代码
      • generator()方法可通过第二个参数设置一些选项来影响输出结果
        const newCode = generator(ast, {
            retainLines: false, // 是否使用与源代码相同的行号,默认false
            comments: false, // 是否保留注释,默认true
            compact: true, // 是否压缩代码
            jsescOption: {minimal: true} // 能够还原unicode与十六进制字符串
        }).code;
  • traverse与visitor 

    • traverse组件用来遍历AST,通常需要配合visitor来使用

    • visitor是一个对象,可以定义一些方法来过滤节点

      • 定义visitor的3种方式:
        const visitor1 = {
            FunctionExpression: function (path){
                console.log('test visitor1')
            }
        }
        
        // 最常用
        const visitor2 = {
            FunctionExpression(path){
                console.log('test visitor2')
            }
        }
        
        const visitor3 = {
            FunctionExpression: {
                enter(path){
                    console.log('test visitor3')
                },
                // exit(path){
                //     console.log('test visitor3 exit...')
                // }
            }
        }

        在遍历节点的过程中,有两次机会访问一个节点,即进入(enter)与退出(exit)节点时,traverse默认是在enter时处理,如果要在exit时处理,必须在visitor中声明

    • traverse与visitor使用示例:

      • 示例1
        const visitorFunction = {
            FunctionExpression(path){
                console.log('test...')
            }
        }
        traverse(ast, visitorFunction)

        首先声明visitor对象,名字可自定义(visitorFunction);该对象的方法名是traverse需要遍历处理的节点类型(FunctionExpression),遍历过程中,当节点类型匹配时,会执行相应的方法(比如有多个FunctionExpression,则代码会执行相应次),如果需要处理其它节点类型,可继续定义;visitor中的方法接收一个参数path,它指的是当前节点的Path对象,而非节点(Node)

      • 示例2:同一个函数作用于多个节点
        const visitorFunction = {
            'FunctionExpression|BinaryExpression'(path){
                console.log('test...')
            }
        }
        traverse(ast, visitorFunction)

        方法名采用字符串形式,多个节点类型之间用'|'分隔

      • 示例3:多个函数作用于同一个节点
        let func1 = function (path){
            console.log('执行func1...')
        }
        
        let func2 = function (path){
            console.log('执行func2...')
        }
        
        const visitorFunction = {
            FunctionExpression: {
                enter: [func1, func2]
            }
        }
        traverse(ast, visitorFunction)

        将赋值给enter的函数改为函数数组,执行时会按照顺序依次执行

      • 示例4:对指定节点再次进行遍历
        const updateParamNameVisitor = {
            Identifier(path){
                if(path.node.name === this.paramName){
                    path.node.name = 'new_' + path.node.name // 修改当前节点的名称
                }
            }
        }
        
        const visitor = {
            FunctionExpression(path){
                if(path.node.params[0]){
                    const paramName = path.node.params[0].name // 函数第一个参数名
                    // path.traverse()方法,第一个参数是visitor对象,第二个参数指定visitor对象中的this
                    path.traverse(updateParamNameVisitor, {paramName})
                }
            }
        }
        traverse(ast, visitor)

        traverse先遍历所有节点,然后根据visitor过滤FunctionExpress节点,调用path.traverse()方法,根据updateParamNameVisitor来遍历该节点下所有节点,如果函数有参数,则修改第一个参数名。path.node才是当前节点,所以path.node.params[0].name表示函数第一个参数名

  • types组件

    • types组件主要用来判断节点类型、生成新的节点等

      • 判断节点类型
        • t.isIdentifier(path.node)  <=>  path.node.type === 'Identifier'
        • 传入第二个参数,可以进一步筛选节点:t.isIdentifier(path.node, {name: 'a'})   <=>  path.node.type === 'Identifier' && path.node.name === 'a'
          // 把所有标识符a改名为aaa
          const visitor = {
              enter(path){
                  if(path.node.type === 'Identifier' && path.node.name === 'a'){
                      path.node.name = 'aaa'
                  }
              }
          }
          traverse(ast, visitor)
        • t.isLiteral(path.node)
          • 判断字面量(包括字符串、数值、布尔值字面量)
      • 生成节点(Node)
        • 以生成如下代码为例
          let obj = {
              name: 'eliwang',
              add: function (a,b){
                  return a + b + 1000;
              }
          }
        • 可以将如上代码放到在线AST解析网站查看节点结构,要生成这些节点,需要从内而外依次构造
          • 生成节点时,注意节点名首字母小写,样式:t.首字母小写节点名(参数)
          • 首先看到代码对应的节点是VariableDeclaration,可通过t.variableDeclaration(kind, declarations)方法生成,通过代码提示查看到源码:
            declare function variableDeclaration(kind: "var" | "let" | "const" | "using", declarations: Array<VariableDeclarator>): VariableDeclaration;

            kind的取值可以是"var" | "let" | "const" | "using",declarations是由VariableDeclarator组成的数组,因为有时一次性声明多个变量,返回值为VariableDeclaration

          • 接着需要实现VariableDeclarator节点,通过t.variableDeclarator(id, init)方法,源码:
            declare function variableDeclarator(id: LVal, init?: Expression | null): VariableDeclarator;

            id为Identifier对象,init为Expression对象,默认为null

          • Identifier对象实现:t.identifier(name),源码:
            declare function identifier(name: string): Identifier;
          • 该示例中的Expression对象为ObjectExpression对象,可通过t.objectExpression(properties)方法生成,源码:
            declare function objectExpression(properties: Array<ObjectMethod | ObjectProperty | SpreadElement>): ObjectExpression;

            对象的属性可以是多个,所以是数组

          • 该示例中properties为2个ObjectProperty对象,可通过t.objectProperty(key, value, [computed, [shorthand,[decorators)方法生成,源码:
            declare function objectProperty(key: Expression | Identifier | StringLiteral | NumericLiteral | BigIntLiteral | DecimalLiteral | PrivateName, value: Expression | PatternLike, computed?: boolean, shorthand?: boolean, decorators?: Array<Decorator> | null): ObjectProperty;

            key和value为必选参数,其它可选

          • 该示例中,第一个属性的值为字符串字面量(StringLiteral),通过t.stringLiteral(value)方法生成,源码:
            declare function stringLiteral(value: string): StringLiteral;
          • 第2个属性的值为函数表达式(FunctionExpression),通过t.functionExpression(id, params, body, [generator,[async)方法生成,源码:
            declare function functionExpression(id: Identifier | null | undefined, params: Array<Identifier | Pattern | RestElement>, body: BlockStatement, generator?: boolean, async?: boolean): FunctionExpression;

            id表示函数名,该示例中是一个匿名函数,所以为null,params表示参数列表,body是BlockStatement对象,其它参数可选

          • BlockStatement对象可通过t.blockStatement(body, [directives)方法生成,源码:
            declare function blockStatement(body: Array<Statement>, directives?: Array<Directive>): BlockStatement;

            body是由Statement对象组成的数组

          • 该示例中的Statement对象为ReturnStatement,通过t.returnStatement([argument)方法生成,源码:
            declare function returnStatement(argument?: Expression | null): ReturnStatement;

            argument是可选参数

          • 该示例中ReturnStatement对象的argument属性是二项式(BinaryExpression),可通过t.binaryExpression(operator, left, right)方法生成,源码:
            declare function binaryExpression(operator: "+" | "-" | "/" | "%" | "*" | "**" | "&" | "|" | ">>" | ">>>" | "<<" | "^" | "==" | "===" | "!=" | "!==" | "in" | "instanceof" | ">" | "<" | ">=" | "<=" | "|>", left: Expression | PrivateName, right: Expression): BinaryExpression;

            operator即操作符,left表示操作符左侧的表达式,right表示操作符右侧的表达式

          • 该示例中,BinaryExpression对象的left属性继续是BinaryExpression对象,其left和right属性是Identifier对象,right是数值字面量(NumericLiteral),可通过t.numericLiteral(value)方法生成,源码:
            declare function numericLiteral(value: number): NumericLiteral;
          • 梳理清楚后,我们从内到外逐层构造出示例中VariableDeclaration类型节点:
            const t = require('@babel/types')
            const generator = require('@babel/generator').default
            
            let argLeft = t.binaryExpression('+', t.identifier('a'), t.identifier('b'))
            let argRight =  t.numericLiteral(1000)
            let argument = t.binaryExpression('+', argLeft, argRight)
            let retStatement = t.returnStatement(argument)
            let funcBody = t.blockStatement([retStatement])
            let funcParams = [t.identifier('a'), t.identifier('b')]
            let objPro2 = t.objectProperty(t.identifier('add'), t.functionExpression(null, funcParams, funcBody))
            let objPro1 = t.objectProperty(t.identifier('name'), t.stringLiteral('eliwang'))
            let varDeclarator = t.variableDeclarator(t.identifier('obj'), t.objectExpression([objPro1, objPro2]))
            let varDeclaration = t.variableDeclaration('let', [varDeclarator])
            let genCode = generator(varDeclaration).code
            console.log(genCode)
        • 当生成较多字面量时,可通过t.valueToNode()方法来快速生成节点(Node):
          console.log(JSON.stringify(t.valueToNode('hello'), null, 2));
          console.log(JSON.stringify(t.valueToNode(['hello', 1, null, undefined, /\d+/g, true, {name: 'eliwang'}]), null ,2));

          它支持各种类型,包括undefined, null, string, boolean, number, RegExp, ReadonlyArray, object等

  • template与eval语句还原

    • template.statement.ast('字符串'):将字符串替换成单条语句(ExpressionStatement)- Node

    • template.statements.ast('字符串'):将字符串替换成多条语句(ExpressionStatement)- Node

    • 示例:

      var b = 20
      eval('var a = 10;')
      eval(String.fromCharCode(99, 111, 110, 115, 111, 108, 101,  46, 108, 111, 103,  40, 97,  32,  42,  32,  98,  41))
      console.log(a)
    • 还原:

      traverse(ast, {
          CallExpression(path) {
              let {callee, arguments} = path.node
              if (!t.isIdentifier(callee, {name: 'eval'}) || arguments.length != 1) return
              if(t.isStringLiteral(arguments[0])){
                  path.replaceInline(template.statements.ast(arguments[0].value))
              }else{
                  let code = generator(arguments[0]).code
                  path.replaceInline(template.statements.ast(eval(code)))
              }
          }
      })
      /* 还原结果
      var b = 20;
      var a = 10;
      console.log(a * b);
      console.log(a);
      */

三、Path对象

  • Path与Node的区别

    const visitor = {
        FunctionExpression(path){
            console.log(path.node) // 当前Node节点对象
            console.log(path) // NodePath对象
        }
    }
    traverse(ast, visitor)
  • path.stop()、path.skip()以及return之间的区别

    • path.stop()

      • 将本次遍历执行完毕后,停止后续节点遍历(会将本次遍历代码完整执行
    • path.skip()

      • 执行节点替换操作后,traverse依旧能够遍历,使用path.skip()可以跳过替换后的节点遍历,避免不合理的递归调用,不影响后续节点遍历(skip位置后面代码会执行
      • 对于单次替换多个节点的情况,考虑是否可以使用path.stop()
    • return

      • 跳过当前遍历的后续代码,不影响后续节点遍历(相当于continue
    • 示例代码

      let a = 10;
      let b = 20;
      let c = 30;
      let d = 40;
    • AST处理步骤

      const visitorTestStop = {
          Identifier(path){
              if(path.node.name == 'c'){
                  path.stop();
              }
              console.log(path.node.name) // 会依次打印:a b c
          }
      }
      
      const visitorTestReturn = {
          Identifier(path) {
              if(path.node.name == 'c'){
                  return;
              }
              console.log(path.node.name) // 会依次打印:a b d
          }
      }
      
      const visitorTestSkip = {
          NumericLiteral(path){
              path.replaceWith(t.numericLiteral(100));
              path.skip() // 不使用,则会无限递归造成死循环
          }
      }
      
      traverse(ast, visitorTestStop)
      console.log('=====================')
      traverse(ast, visitorTestReturn)
      console.log('=====================')
      traverse(ast, visitorTestSkip)
      console.log(generator(ast).code)
      /*
      a
      b
      c
      =====================
      a
      b
      d
      =====================
      let a = 100;
      let b = 100;
      let c = 100;
      let d = 100;
      */
  • Path中的属性及方法

    • 获取子节点/Path

      console.log(path.node) // 获取当前节点
      console.log(path.node.right) // 获取节点right属性值或Node
      // path.get()方法,可以传入节点中的属性值字符串,也可以通过'.'连接进行多级访问,返回的是包装后的Path对象
      console.log(path.get('right')) // 获取该节点right属性值对应的Path对象(会将属性值包装成Path对象)
      console.log(path.get('left.left')) // 可以通过'.'的形式多级访问
      // get()字符串参数的写法同节点操作后续一致,下面以节点值为数组类型为例
      console.log(path.get('body.body')[0]); // NodePath
      console.log(path.get('body.body')); // [NodePath, NodePath, NodePath]
    • 判断Path类型

      // 判断path类型,返回true 或者 false
      console.log(path.isBinaryExpression()) // 不带参数形式
      console.log(path.isBinaryExpression({ operator: '+', start: 140})) // 传入参数,注意是node节点中的属性-值
      console.log(path.isLiteral()) // 判断字面量,包括字符串、数值、布尔值字面量 等价于path.isStringLiteral() || path.isNumericLiteral() || path.isBooleanLiteral()

      用法类似于types组件中的类型判断,比如:path.isBinaryExpression({ operator: '+', start: 140})   <=>  t.isBinaryExpression(path.node, {operator: '+', start: 140})

    • 计算节点的值

      • path.evaluate()
        /*还原前代码
        let a = 3;
        let b = 7;
        let c = a + b;
        let d = "hello" + "world";
        let e = 777 ^ 888
        console.log(!![])
        */
        
        const visitorRestorValue = {
            "BinaryExpression|UnaryExpression"(path){
                // path.evaluate()返回的是一个对象,confident属性是一个布尔值,表示该节点是否可计算,value是计算的结果
                let {confident, value} = path.evaluate()
                confident && path.replaceWith(t.valueToNode(value))
            }
        }
        traverse(ast, visitorRestorValue)
        /* 还原后代码
        let a = 3;
        let b = 7;
        let c = 10;
        let d = "helloworld";
        let e = 113;
        console.log(true);
        */
    • 节点转代码

      // 节点转代码
      // 通常会利用该方法来排查错误,对节点遍历过程中的调试很有帮助以下3种方式均可以
      console.log(generator(path.node).code)
      console.log(path.toString())
      console.log(path + '')
    • 替换节点属性

      const visitor = {
          BinaryExpression(path){
              path.node.left = t.valueToNode('x')
              path.node.right = t.identifier('y')
              // path.node.right.name = 'y' 修改属性值中的name属性
          }
      }
      traverse(ast, visitor)

      需要注意,替换的类型要在允许的类型范围之内

    • 替换整个节点

      • replaceWith():节点换节点(一换一)
        const visitor = {
            BinaryExpression(path){
                path.replaceWith(t.valueToNode('hello world'))
            }
        }
        traverse(ast, visitor)
      • replaceWithMultiple():节点换节点(多换一)
        const visitor = {
            ReturnStatement(path){
                path.replaceWithMultiple([
                    // 当表达式语句单独在一行时(没有赋值),最好使用expressionStatement包裹
                    t.expressionStatement(t.stringLiteral('hello world!')),
                    t.expressionStatement(t.numericLiteral(1000)),
                    t.returnStatement()
                ])
                // 替换后的节点,traverse也是能遍历到的(Babel会更新Path对象),上述return语句替换后会陷入死循环,所以使用path.stop()来停止遍历
                path.stop()
            }
        }
        traverse(ast, visitor)
      • replaceInline():节点换节点,根据参数类型,单个参数等同于replaceWith(),数组类型,则等同于replaceWithMultiple()
        const visitor1 = {
            StringLiteral(path){
                path.replaceInline(t.stringLiteral('Hello AST!'));
                path.stop()
            }
        }
        const visitor2 = {
             ReturnStatement(path){
                path.replaceInline([
                    // 当表达式语句单独在一行时(没有赋值),最好使用expressionStatement包裹
                    t.expressionStatement(t.stringLiteral('hello world!')),
                    t.expressionStatement(t.numericLiteral(1000)),
                    t.returnStatement()
                ])
                // 替换后的节点,traverse也是能遍历到的,上述return语句替换后会陷入死循环,所以使用path.stop()来停止遍历
                path.stop()
            }
        }
        traverse(ast, visitor1)
        traverse(ast, visitor2)
      • replaceWithSourceString():使用字符串源码替换节点
        const visitor = {
             ReturnStatement(path){
                 // 获取Path对象
                const argumentPath = path.get('argument');
                // 格式串默认调用了argumentPath.toString()方法,即节点转代码
                argumentPath.replaceWithSourceString(`function(){return ${argumentPath}}()`);
                // 因为内部有return语句,所以需要path.stop()
                path.stop()
            }
        }
        traverse(ast, visitor)
    • 删除节点

      const visitor = {
           EmptyStatement(path){
               // 删除节点
               path.remove();
           }
      }
      traverse(ast, visitor)

      删除多余的分号

    • 插入节点

      • insertBefore():当前节点前插入
      • insertAfter():当前节点后插入
        const visitor = {
             ReturnStatement(path){
                 // 节点前插入
                 path.insertBefore(t.expressionStatement(t.stringLiteral('Before')));
                 // 节点后插入
                 path.insertAfter(t.expressionStatement(t.stringLiteral('After')))
             }
        }
        traverse(ast, visitor)
    • 父级Path

      • 属性
        • path.parentPath:父级Path,类型为NodePath
        • path.parent:父节点,类型为Node,等同于parent.parentPath.node
      • 方法
        • path.findParent():向上遍历语法树,直到满足相应条件,返回NodePath(不含当前节点
          const visitor = {
               ReturnStatement(path){
                   // 向上遍历语法树,参数p为每一级父级Path对象,直到满足相应的条件,返回该Path对象
                   let objProPath = path.findParent(p => p.isObjectProperty())
                   console.log(objProPath)
               }
          }
          traverse(ast, visitor)
        • path.find():向上遍历语法树(含当前节点
        • path.getFunctionParent():查找最近的父函数Path
          const visitor = {
               BlockStatement(path){
                   // 查找最近的父函数
                   let parentFunPath = path.getFunctionParent()
                   console.log(parentFunPath.node.params)
               }
          }
          traverse(ast, visitor)
        • path.getStatementParent():向上遍历,查找最近的父语句Path(含当前节点),比如声明语句、return语句、if语句、while语句等
          const visitor = {
               ReturnStatement(path){
                   // 查找最近的父语句(含当前节点),return语句需要从parentPath中去调用
                   let a = path.parentPath.getStatementParent()
                   console.log(a.node.type)
               }
          }
          traverse(ast, visitor)
    • 同级Path

      • 需要先了解容器(container),一般只有容器为数组时,才有同级节点
      • 属性:
        • path.inList:是否有同级节点
        • path.container:获取容器(包含所有同级节点的数组),如果没有同级节点,则返回Node对象
        • path.key:获取当前节点在容器中的索引,如果没有同级节点,则返回该节点对应的属性名(字符串)
        • path.listKey:获取容器名,如果没有容器,则返回undefined
          const visitor = {
               ReturnStatement(path){
                   console.log(path.inList); // 判断是否有同级节点:true
                   console.log(path.container); // 获取容器:[Node{type: 'ReturnStatement'...}]
                   console.log(path.listKey) // 获取容器名:body
                   console.log(path.key) // 获取当前节点在容器中的索引:0
               }
          }
          traverse(ast, visitor)
      • 方法:
        • path.getSibling(index):根据容器数组中的索引,来获取同级Path,index可以通过path.key来获取
        • path.unshiftContainer()与path.pushContainer():此处的path必须是容器,比如body
          • 往容器最前面或者后面加入节点
          • 第一个参数为listKey
          • 第二个参数为Node或者Node数组
          • 返回值是一个数组,里面元素是刚加入的NodePath对象
            const visitor = {
                 ReturnStatement(path){
                     console.log(path.getSibling(path.key - 1)) // 根据索引来获取同级Path
                     // 容器前面插入单个节点
                     console.log(path.parentPath.unshiftContainer('body', t.expressionStatement(t.stringLiteral('Before...')))); // [NodePath{...}]
                     // 容器后面插入2个节点
                     console.log(path.parentPath.pushContainer('body', [t.expressionStatement(t.stringLiteral('After1...')), t.expressionStatement(t.stringLiteral('After2...'))])); // [NodePath{...}, NodePath{...}]
                 }
            }
            traverse(ast, visitor)

四、Scope对象

  • scope提供了一些属性和方法,可以方便地查找标识符的作用域,获取并修改标识符的所有引用,以及判断标识符是否为参数或常量

  • Identifier下某标识符的path.scope 大多数情况下可使用path.scope.getBinding('某标识符').scope来代替

    • 注意两者目标节点不同,前者主要针对Identifier节点,而后者则针对诸如FunctionDeclaration这种内部含Identifier节点的节点
  • 本部分以下面代码为例:

    const a = 1000;
    let b = 2000;
    let obj = {
        name: 'eliwang',
        add: function (a) {
            a = 400;
            b = 300;
            let e = 700;
            function demo(){
                let d = 600;
            }
            demo();
            return a + b + 1000 + obj.name
        }
    }
  • 获取标识符作用域

    • path.scope.block

      • 该属性可以获取标识符作用域,返回Node对象
    • 标识符分为变量和函数:

      • 标识符为变量:
        const visitor = {
            Identifier(path) {
                if(path.node.name === 'e'){
                    console.log(generator(path.scope.block).code)
                }
            }
        }
        traverse(ast, visitor)
        /*
        function (a) {
          a = 400;
          ...
          return a + b + 1000 + obj.name;
        }
        */
      • 标识符为函数:
        const visitor = {
            FunctionDeclaration(path) {
                if(path.node.id.name === 'demo'){
                    // 该示例中scope.block只能获取到demo函数,而实际作用域应该是父级函数
                    console.log(generator(path.scope.parent.block).code)
                }
            }
        }
        traverse(ast, visitor)
  • path.scope.getBinding('标识符')

    • 获取当前节点下能够引用到的标识符的绑定(含父级作用域中定义的标识符),返回Binding对象,引用不到则返回undefined

    • 获取Binding对象:

      traverse(ast, {
          FunctionDeclaration(path) {
              let binding = path.scope.getBinding('a');
              console.log(binding);
          }
      });
      /*
       Binding {
        identifier: Node {type: 'Identifier',...name: 'a'},
        scope: Scope {uid: 1,path: NodePath {...},block: Node {...type: 'FunctionExpression'}...},
        path: NodePath {...},
        kind: 'param',
        constantViolations: [...],
        constant: false,
        referencePaths: [NodePath {...}],
        referenced: true,
        references: 1
      }
      */

      Binding中的关键属性:

      • identifier:a标识符的Node对象
      • path:a标识符的NodePath对象
      • scope:a标识符的scope,其中的block节点转为代码后,就是它的作用域范围add,假如获取的是函数标识符,也可以获取其作用域(获取作用域
      • kind:表明标识符的类型,本例中是一个参数(判断是否为参数
      • constant:是否常量
      • referencePaths:所有引用该标识符的节点Path对象数组(元素type为Identifier
      • constantViolations:存放所有修改该标识符节点的Path对象数组(长度不为0,表示该标识符有被修改,元素type为AssignmentExpression,即赋值表达式
        /*
            function test(){
                return 100
            }
            test = 10
        */
        traverse(ast, {
            FunctionDeclaration(path){
                let name = path.node.id.name
                let binding = path.scope.getBinding(name)
                if(binding && binding.constantViolations.length > 0){
                    console.log(binding.constantViolations[0].node.type) // AssignmentExpression
                    console.log(generator(binding.constantViolations[0].node).code) // test = 10
                }
            }
        })
      • referenced:是否被引用
      • references:被引用的次数
    • 获取函数作用域:

      traverse(ast, {
          FunctionExpression(path) {
              let bindingA = path.scope.getBinding('a'); // 获取当前节点下a的Binding对象
              let bindingDemo = path.scope.getBinding('demo'); // 获取当前节点下demo函数的Binding对象
              console.log(bindingA.referenced); // true -- a是否被引用
              console.log(bindingA.references); // 1 -- a被引用次数
              console.log(generator(bindingA.constantViolations[0].node).code) // a = 400 -- 被重新赋值处的代码
              console.log(generator(bindingA.referencePaths[0].node).code) // a -- 被引用处的代码
              console.log(bindingDemo.references) // 1 -- demo函数被引用的次数
              console.log(generator(bindingA.scope.block).code); // 变量a作用域返回的是add方法,即function (a) {...}
              console.log(generator(bindingDemo.scope.block).code); // 函数demo作用域返回的也是add方法
          }
      });

      可通过Binding对象.scope.block来获取标识符作用域Node

  • path.scope.getOwnBinding('标识符')

    • 获取当前节点的标识符绑定(不含父级作用域中定义的标识符、子函数中定义的标识符

      const TestOwnBindingVisitor = {
          Identifier(p) {
              let name = p.node.name;
              // this.path获取的是传递过来的FunctionExpression节点NodePath
              console.log( name, !!this.path.scope.getOwnBinding(name) );
          }
      }
      traverse(ast, {
          FunctionExpression(path){
              path.traverse(TestOwnBindingVisitor,{path});
          }
      });
      /*
      a true
      a true
      b false
      e true
      demo true
      d false
      demo true
      a true
      b false
      obj false
      name false
      */
    • 通过path.scope.getBinding()方法 + 标识符作用域是否与当前函数一致,来获取当前节点的标识符绑定

      const TestOwnBindingVisitor = {
          Identifier(p) {
              let name = p.node.name;
              // this.path获取的是传递过来的FunctionExpression节点NodePath
              let binding = this.path.scope.getBinding(name)
              // 判断当前节点下定义的标识符:判断标识符作用域是否与当前函数一致
              if (binding && generator(binding.scope.block).code == this.path.toString()){
                  console.log(name)
              }
          }
      }
      traverse(ast, {
          FunctionExpression(path){
              path.traverse(TestOwnBindingVisitor,{path});
          }
      });
      /*
      a
      a
      e
      demo
      demo
      a
      */
  •  遍历作用域中的节点

    • scope.traverse():【path.scope.traverse() 或者 path.scope.getBinding('xx').scope.traverse()】

      • 既可以使用Path对象中的scope,也可以使用Binding对象中的scope,推荐使用Binding中的
        traverse(ast, {
            FunctionDeclaration(path) {
                let binding = path.scope.getBinding('a');
                // binding.scope.block就是add方法函数,因此下面就是遍历add方法中的AssignmentExpression节点
                binding && binding.scope.traverse(binding.scope.block, {
                    AssignmentExpression(p) {
                        if (p.node.left.name == 'a')
                            // 替换a变量的值
                            p.node.right = t.numericLiteral(600);
                            // p.node.right.value = 600;
                    }
                });
            }
        });
  • 标识符重命名

    • scope.rename(原名,新名):会同时修改所有引用该标识符的地方

      • 使用binding.scope()操作
        const visitor = {
            FunctionExpression(path){
                // 将add函数中的a标识符更名为'x'
                let binding = path.scope.getBinding('a')
                binding && binding.scope.rename('a','x')
            }
        }
        traverse(ast, visitor)
        /*
        const a = 1000;
        let b = 2000;
        let obj = {
          name: 'eliwang',
          add: function (x) {
            x = 400;
            b = 300;
            let e = 700;
            function demo() {
              let d = 600;
            }
            demo();
            return x + b + 1000 + obj.name;
          }
        };
        */
      • 使用path.scope()操作
        const visitor = {
            Identifier(path){
                // path.scope.generateUidIdentifier('xxx').name    =>  可以生成一个与现有标识符名不冲突的标识符
                path.scope.rename(path.node.name, path.scope.generateUidIdentifier('uid').name)
            }
        }
        traverse(ast, visitor)
        /*
        const _uid = 1000;
        let _uid14 = 2000;
        let _uid15 = {
          name: 'eliwang',
          add: function (_uid13) {
            _uid13 = 400;
            _uid14 = 300;
            let _uid9 = 700;
            function _uid12() {
              let _uid11 = 600;
            }
            _uid12();
            return _uid13 + _uid14 + 1000 + _uid15.name;
          }
        };
        */
  • scope的其他方法

    • scope.hasBinding('a')

      • 是否有标识符a的绑定,返回true或者false,返回false时等同于scope.getBindings('a')的值为undefined
    • scope.hasOwnBinding('a')

      • 当前节点是否有自己标识符a的绑定,返回true或则false
    • scope.getAllBindings()

      • 获取当前节点的所有绑定,返回一个对象,该对象以标识符为属性名,对应的Bingding对象为属性值
    • scope.hasReference('a')

      • 查询当前节点中是否有a标识符的引用,返回true或则false
    • scope.getBindingIdentifier('a')

      • 获取当前节点中绑定的a标识符,返回Identifier的Node对象,等同于scope.getBinding('a').identifier

五、遍历总结

  • 1、遍历整个ast

    traverse(ast, visitor) => 根据visitor中的节点类型进行遍历整个ast
  • 2、遍历指定NodePath

    path.traverse(visitor, {xx: yy}) => 遍历当前path,第二个参数指定func中的this指针
  • 遍历指定标识符作用域

    let binding = path.scope.getBinding('xxx');
    binding && binding.scope.traverse(binding.scope.block, visitor);

六、练习

  • 1、使用types组件生成如下代码:

    function test(a) {
      return a + 1000;
    }
    • // 思路:将代码放到AST Explorer网站中解析,然后由内到外构建节点
      
      let left = t.identifier('a')
      let right = t.numericLiteral(1000)  
      let binaryExpress = t.binaryExpression('+', left, right)
      let returnStatement = t.returnStatement(binaryExpress)
      let funcBlockStatement = t.blockStatement([returnStatement])
      let funcIdenti = t.identifier('test')
      let funcParams = [t.identifier('a')]
      let functionDeclaration = t.functionDeclaration(funcIdenti, funcParams, funcBlockStatement)
      console.log(generator(functionDeclaration).code)
  • 2、使用Babel的API将题1代码改为如下形式:

    function test(a) {
        return function () {
            a + 1000;
        }()
    }
    • const visitor = {
          BinaryExpression(path) {
              let newExpressionCode = `
              function () {
                  a + 1000;
              }()`
              path.replaceWithSourceString(newExpressionCode) // 直接使用源码进行替换
              path.stop() // 停止遍历,否则会无限调用
          }
      }
      traverse(ast,visitor)
  • 3、以题2代码为源码,使用Babel的API将其改写为如下形式:

    function test(a) {
        b = 2000;
        return function () {
            a + b, a + 1000;
        }();
    }
    • const visitorBeforeReturn = {
          ReturnStatement(path){
              // 在return前面添加:b = 2000
              let assignLeft = t.identifier('a');
              let assignRight = t.numericLiteral(2000);
              let assignmentExpression = t.assignmentExpression('=', assignLeft, assignRight);
              path.insertBefore(t.expressionStatement(assignmentExpression))
          }
      }
      
      const visitorReplaceToSequnenceExpression = {
          BinaryExpression(path){
              // 将 a + 1000替换为:a + b, a + 1000
              let binaryExpression1 = t.binaryExpression('+', t.identifier('a'), t.identifier('b'))
              let binaryExpression2 = t.binaryExpression('+', t.identifier('a'), t.numericLiteral(1000))
              let sequenceExpression = t.sequenceExpression([binaryExpression1, binaryExpression2])
              path.replaceWith(sequenceExpression)
              path.stop()
          }
      }
      traverse(ast, visitorBeforeReturn)
      traverse(ast, visitorReplaceToSequnenceExpression)
  • 4、使用Babel的API将题3中的变量a改为x

    // 方式一
    const visitorReplaceA = {
        FunctionDeclaration(path){
            let binding = path.scope.getBinding('a');
            binding && binding.scope.rename('a','x')
        }
    }
    
    /*方式二
    const visitorReplaceA = {
        Identifier(path){
            // 替换节点属性
           if(path.node.name == 'a'){
               path.scope.rename(path.node.name, 'x')
               // path.node.name = 'x'   方式三
           }
        }
    }
    */
    
    traverse(ast, visitorReplaceA)
  • 5、编写一个visitor,判断标识符是否为当前函数参数

    let d = 1000
    function test(a){
        let b,c
        d = 2000
    }
    • visitor = {
          FunctionDeclaration(path) {
              // 继续遍历当前节点
              path.traverse({
                  Identifier(p) {
                      let name = p.node.name;
                      let binding = this.path.scope.getBinding(name);
                      if(binding == undefined) return
                      if(binding.kind == 'param'){ // 判断是否参数
                          console.log(`参数:${name}`)
                      }else{
                          console.log(`非参数:${name}`)
                      }
                  }
              }, {path})
          }
      }
      traverse(ast, visitor)
      /*
      非参数:test
      参数:a
      非参数:b
      非参数:c
      非参数:d
      */

七、AST附录

  • 常见类型对照表

序号类型原名称中文名称描述
1 Program 程序主体 整段代码的主体
2 VariableDeclaration 变量声明 声明一个变量,例如 var、let、const
3 FunctionDeclaration 函数声明 声明一个函数,例如 function
4 EmptyStatement 空语句 函数声明后面的分号';'
5 ExpressionStatement 表达式语句 通常是调用一个函数,例如 console.log()
6 BlockStatement 块语句 包裹在 {} 块内的代码,例如 if (condition){var a = 1;}
7 BreakStatement 中断语句 通常指 break
8 ContinueStatement 持续语句 通常指 continue
9 ReturnStatement 返回语句 通常指 return
10 SwitchStatement Switch 语句 通常指 Switch Case 语句中的 Switch
11 IfStatement If 控制流语句 控制流语句,通常指 if(condition){}else{}
12 Identifier 标识符 标识,例如声明变量时 var identi = 5 中的 identi
13 CallExpression 调用表达式 通常指调用一个函数,例如 console.log()
14 BinaryExpression 二进制表达式 通常指运算,例如 1+2
15 MemberExpression 成员表达式 通常指调用对象的成员,例如 console 对象的 log 成员
16 ArrayExpression 数组表达式 通常指一个数组,例如 [1, 3, 5]
17 NewExpression New 表达式 通常指使用 New 关键词
18 AssignmentExpression 赋值表达式 通常指将函数的返回值赋值给变量
19 UpdateExpression 更新表达式 通常指更新成员值,例如 i++
20 Literal 字面量 字面量
21 BooleanLiteral 布尔型字面量 布尔值,例如 true false
22 NumericLiteral 数字型字面量 数字,例如 100
23 StringLiteral 字符型字面量 字符串,例如 vansenb
24 SwitchCase Case 语句 通常指 Switch 语句中的 Case
  • Path常见属性及方法

posted @ 2023-03-08 14:19  eliwang  阅读(1748)  评论(0编辑  收藏  举报