【AST 基础】三、AST 的 Path 和 Node 对象

Path 对象

Path & Node 的区别

Path 是描述 父子节点之间的链接Path 除了下面这些属性外,还包含 添加更新移动删除 等方法

Node 对象 是 Path 对象 中的一个属性。path.node 能取出当前遍历到节点各种具体信息不能使用 Path 对象的各种方法

/*
NodePath {
    contexts : Array(1) [TraversalContext], // 上下文 内容嵌套自己
    state : undefined,
    opts : Object {Identifier: Object, _exploded: true, _verified: true},
    _traverseFlags : 0,
    skipKeys : null,
    parentPath : NodePath {contexts: Array(1), state: undefined, opts: Object, _traverseFlags: 0, skipKeys: null, ...}, // 父节点 Path对象
    container : Node {type: "VariableDeclarator", start: 4, end: 162, loc: SourceLocation, id: Node, ...}, // 容器 里面是兄弟节点,有 数组(有兄弟) 和 单个对象(没有兄弟)
    listKey : undefined, // 有兄弟节点时,兄弟节点的 容器的名字
    key : "id", // 有兄弟节点时,代表 兄弟容器索引,没有兄弟节点,代表 对象属性名
    node : Node {type: "Identifier", start: 4, end: 7, loc: SourceLocation, name: "obj"}, // Node 对象
    type : "Identifier",
    parent : Node {type: "VariableDeclarator", start: 4, end: 162, loc: SourceLocation, id: Node, ...}, // 父节点 Node对象
    hub : undefined,
    data : null,
    context : TraversalContext {queue: Array(1), priorityQueue: Array(0), parentPath: NodePath, scope: Scope, state: undefined, ...},
    scope : Scope {uid: 0, path: NodePath, block: Node, labels: Map(0), inited: true, ...}, // 作用域
}
 */

Path 对象 中的方法

获取当前节点

为了获取到 AST节点的属性值,需要先 访问到该 节点,然后用 path.node.xxx (xxx 相关属性) 获取

获取 Node 对象

let visitor = {
    Identifier(path) {
        console.log(path.node) // 获取当前 节点 Node 对象
        console.log(path.node.left) // 获取 Node 节点具体属性值
    }
}

获取 Path 对象,但是有些 属性值就没有必要包装 operatorname

let visitor = {
    BinaryExpression(path) {
        console.log(path.get('node')) // 会把 node 包装成  Path 对象返回
        console.log(path.get('node.left')) // 支持 多级访问
    }
}

判断 Path 类型

let visitor = {
    BinaryExpression(path) {
        console.log(path.get('left').isIdentifier())
        console.log(path.get('right').isNumericLiteral({
            value: 1000
        }))
        console.log(path.get('left').assertIdentifier())
    }
}
/*
false
true
报错
 */

节点转代码

代码比较复杂,需要动态调试,就可以使用该方法,方便查看某一小段代码,就不需要到最后才全部再转换成代码,再看

let visitor = {
    FunctionExpression(path) {
        console.log(generator(path.node).code)
        console.log(path.toString()) // 因为 Path 对象,重写了 toString() 方法
    }
}

/*
function (a, b) {     
  return a + b + 1000;
}
function (a, b) {     
  return a * b + 1000;
}
 */

替换节点属性

替换需要属性,需要符合实际逻辑,在允许的范围替换,不能随便替换,造成逻辑错误

visitor = {
    BinaryExpression(path) {
        path.node.left = type.identifier(name="x")
        path.node.right = type.identifier(name="y")
    }
}
/*
  add: function (a, b) {
    return x + y;
  },
  mul: function (a, b) {
    return x + y;
  }
 */

替换节点

replaceWith() 使用一个节点替换一个节点一对一的替换

visitor = {
    BinaryExpression(path) {
        path.replaceWith(type.valueToNode('mimi'))
    }
}
/*
  add: function (a, b) {
    return "mimi";
  },
  mul: function (a, b) {
    return "mimi";
  }
 */

replaceWithMultiple()多个节点替换一个节点

两点特别说明:

  1. 当替换的表达式替换后在一行时,最好使用 type.expressionStatement() 包裹一层
  2. 新替换的节点 traverse() 也会去遍历,所以 注意别陷入 不合理的递归死循环
    遍历 ReturnStatement() 替换,替换里 又有 ReturnStatement() ,陷入 递归死循环,所以使用 path.stop()停止
visitor = {
    ReturnStatement(path) {
        path.replaceWithMultiple([
            type.expressionStatement(type.valueToNode('ZZ')),
            type.expressionStatement(type.valueToNode('XX')),
            type.returnStatement(type.valueToNode('YY')),
        ])
        path.stop();
    }
}
traverse(ast, visitor)
/*
  add: function (a, b) {
    "ZZ";
    "XX";
    return "YY";
  },
 */

replaceInline(),接受一个参数,作用相对于是 replaceWith() replaceWithMultiple() 的综合

当接受的参数是 一个节点,就是 replaceWith()

当接受的参数是 一个节点数组,就是 replaceWithMultiple()

添加有 path.stop() 的原因与上面一样

visitor = {
    StringLiteral(path){
        path.replaceInline(
            type.valueToNode('Helle mimi')
        )
        path.stop()
    },
    ReturnStatement(path) {
        path.replaceInline([
            type.expressionStatement(type.valueToNode('ZZ')),
            type.expressionStatement(type.valueToNode('XX')),
            type.returnStatement(type.valueToNode('YY')),
        ])
        path.stop()
    }
}
traverse(ast, visitor)

replaceWithSourceString() 接受 字符串 作为参数,然后解析到 AST 里面

replaceWithSourceString() 替换后的节点也会被解析,所以也需要 path.stop() 防止陷入递归死循环

// 把函数 改为 `闭包形式`
visitor = {
    ReturnStatement(path) {
        let argumentPath = path.get('argument')
        argumentPath.replaceWithSourceString(
            'function(){ return ' + argumentPath + '}' // 字符串 拼接 会自动调用 toString()
        )
        path.stop()
    }
}
/*
  add: function (a, b) {
    return function () {
      return a + b + 1000;
    };
  },
 */

删除节点

visitor = {
    EmptyStatement(path){ // 空语句 也就是多余的分号
        path.remove()
    }
}
traverse(ast, visitor)

插入节点

插入兄弟节点

insertAfter() 当前节点insertBefore() 当前节点

如果要 操作ReturnStatement,可以先判断,直接 return,注意该是用 path.stop() 是不行的

visitor = {
    ReturnStatement(path) {
       path.insertAfter(type.valueToNode("XX"))
       path.insertBefore(type.valueToNode('MM'));
    }
}
traverse(ast, visitor)

父级 Path

在 Path 对象中有两个属性 parentPathparent ,其中 parentPath 是 NodePath 类型,是父级的 Path 对象parent父级的 Node 对象

path.findParent & path.find

参数都是一个 函数,返回值是 Path 对象 ,没有找到 返回 null

从当前节点,一直向上遍历,知道满足对应条件

  • path.findParent() 常用,查找范围不包含当前节点
  • path.find() 不常用,查找范围 包含 当前节点
    visitor = {
      ReturnStatement(path) {
          console.log(path.findParent((path)=>{return path.isObjectExpression()}))
      }
    }
    /*
    NodePath {...}
    */
    

path.getFunctionParent

向上查找 与 当前节点 最接近的 父函数节点,返回值是 Path 对象

visitor = {
    ReturnStatement(path) {
        console.log(path.getFunctionParent())
    }
}
traverse(ast, visitor)
/*
 NodePath {...}
 NodePath {...}
 */

path.getStatementParent

向上遍历找到 父语句节点, 如:return ,ifwhitchwhile

visitor = {
    ReturnStatement(path) {
        console.log(path.getStatementParent()) // 会从 当前节点开始
        // console.log(path.parentPath.getStatementParent()) // 取巧

    }
}
traverse(ast, visitor)

其他方法

替换

path.parentPath.replaceWith()
path.parentPath.replaceWithMultiple()
path.parentPath.replaceInline()
path.parentPath.replaceWithSourceString()

删除

path.parentPath.remove()

同级节点

了解同级节点,首要了解 Path 对象的这几个属性:containerlistKeykey

  • container: 兄弟节点时,是 兄弟节点 数组没有兄弟节点时 就是当前节点 Node 对象
  • listKey: 兄弟节点时,为容器名字没有时 是 undefined
  • key: 兄弟节点时,为当前节点,在容器中索引没有时,是 当前节点属性名

mark

相关属性

visitor = {
    ReturnStatement(path) {
        console.log(path.inList) // true
        console.log(path.listKey) // body
        console.log(path.key) // 0 
        console.log(path.container) // [Node {...}]

    }
}
traverse(ast, visitor)

mark

获取兄弟节点 path.getSibling

参数是 容器中节点的索引,

visitor = {
    ReturnStatement(path) {
        console.log(path.getSibling(path.key)) // Node {...}
    }
}
traverse(ast, visitor)

网容器添加节点

  • path.parentPath.unshiftContainer(): 在 容器头部 添加节点
  • path.parentPath.pushContainer(): 在 容器尾部 添加节点

添加之后的节点,也会再 进行遍历,所以还是需要path.stop() ,以防止进入 递归死循环

visitor = {
    Property(path) {
        path.parentPath.unshiftContainer(path.listKey, [
                type.objectProperty(
                    key = type.identifier(name = "unshift"),  // 第一个属性
                    value = type.stringLiteral(value = "OOO")) // 第一个属性值)
            ]
        )
        path.parentPath.pushContainer(path.listKey, [
                type.objectProperty(
                    key = type.identifier(name = "push"),  // 第一个属性
                    value = type.stringLiteral(value = "MMM")) // 第一个属性值)
            ]
        )
        path.stop()
    }
}
traverse(ast, visitor)
发表评论 / Comment

用心评论~