《JS学习》AST - 关于path/node/Scope/Binding属性和方法

文章介绍了Babel库中path和node的常用操作,包括path的常见方法(如toString,isXXX,parentPath,get,remove等),node的属性(如node,scope,parent,container等),以及Scope和Binding的属性和方法。还提供了获取源代码和节点操作的实例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

出处 @蔡老板 的文章

第1篇:简单介绍path和node

关于path的一些操作

path,翻译过来是路径,理解为路径就好了。比如有这么一段代码:

var a = 123;

在线网站解析的情况如下:
在这里插入图片描述
小技巧:鼠标点击 var 这三个字母中间,可以看到网站直接将相关代码标黄了。

可以看到,上面就是一个个的节点,这也是我们再熟悉不过的JSON结构的数据。

上图中红色方框处就是它的path类型。我们在遍历的时候就这样写插件:


const visitor = {
   VariableDeclaration(path)
   {
     //to do something;
   },
}

小疑问: VariableDeclaration 和 VariableDeclarator 有啥区别?

可以看到,VariableDeclaration 是 VariableDeclarator 的祖先节点,这两者肯定不一样的。我们将代码变成下面这样,再进行解析:

Declaration:声明
Declarator:声明人

var a = 123,b = 456;

解析的结果如下:
在这里插入图片描述
这样就明白了。原来 一个 VariableDeclarator 只对应一个 变量的定义,理解这些,在我们插入节点的时候非常的有帮忙,比如我们想要在

var a = 123;

代码后面插入

b = 456;

你就知道应该遍历 VariableDeclarator 路径,判断变量名是否为 “a”,然后构造节点插入即可。

我们注意到它的参数是:path,那path究竟有那些常见的方法呢?

  1. 当前路径所对应的源代码,使用的是toString这个方法,在我解混淆的时候会经常的用到,是比较重要的,示例代码如下:
const visitor = {
   VariableDeclaration(path)
   {
     console.log(path.toString());
   },
}
  1. 判断path是什么type,使用path.isXXX 这个方法,比如我们需要判断路径是否为 StringLiteral 类型:
if(path.isStringLiteral())
 {
 //do something;
}
  1. 获取path的上一级路径,你可以像这样来获取:
let parent = path.parentPath;
  1. 获取path的子路径,你可以是使用 get方法,比如定义了如下变量:
var a = 123;

当前访问的是 VariableDeclarator ,其结构如下:
在这里插入图片描述
现在需要访问 id 这个路径,你可以这么操作:

path.get('id');
  1. 删除path,使用remove方法,当你觉得你访问的路径已经完成了改完成的事,对代码已经没什么作用了,可以将其删除
path.remove()
  1. 替换path,单路径可以使用replaceWith方法,多路径则使用replaceWithMultiple方法,比如你想将 1 + 2 替换成 3,你可以像下面这样的代码进行替换:
path.replaceWith({type:"NumericLiteral",value:3});

当然,你也可以引入 const type = require(“@babel/types”);然后再进行替换:

path.replaceWith(type.NumericLiteral(3));

注意,这里的3是我口算出来的,在实际操作过程中,是需要获取 “+” 两边的值并计算其结果的。

如果你想获取更多path相关的方法,请直接阅读源码,阅读源码是你学习最快,也是最好的办法。path相关的源码在这里:

@babel\traverse\lib\path

关于node的一些操作

这里的node,其实是path的一个属性:

const node = path.node;

可以将其理解为节点,我们可以打印node,看看是些什么内容。这里还是以代码var a = 123;为例,访问 VariableDeclarator 路径,打印出当前的node:

console.log(path.node);

结果如图:

在这里插入图片描述
其实就是一个 json结构的数据。

  1. 如何获取当前节点所对应的源代码:

const generator = require("@babel/generator").default;
let {code} = generator(node);
  1. 如何删除节点,使用系统的 delete 方法。比如你想删除
    在这里插入图片描述

init这个节点,可以使用下面的方式:

delete path.node.init;

操作过后,代码 var a = 123;将会变成 var a;,即将初始化的值进行删除。

当然也可以直接赋值,比如:

path.node.init = undefined;
  1. 访问子节点,比如你想得到初始化的值,你可以这么操作:
const node = path.node;
const value = node.init.value;
console.log(value);

嗯,你把它理解为 JSON就好了,比如,它是可以这么操作的:

const node = path.node;
console.log(JSON.stringify(node));

第2篇:babel库中path常用属性总结

path常用属性总结:

path相关的源代码在这个js文件中,大家可以直接照着源码学习:


\node_modules\@babel\traverse\lib\path

在本文中,选出部分常用的属性大家参考,更多的知识请自行学习源码。

path的属性定义:


class NodePath {
  constructor(hub, parent) {
    this.contexts = [];
    this.state = null;
    this.opts = null;
    this._traverseFlags = 0;
    this.skipKeys = null;
    this.parentPath = null;
    this.container = null;
    this.listKey = null;
    this.key = null;
    this.node = null;
    this.type = null;
    this.parent = parent;
    this.hub = hub;
    this.data = null;
    this.context = null;
    this.scope = null;
  }
  ......
}

path.node

表示当前path下的node节点,通常我写插件,函数体的第一行代码就是:

let {node,scope} = path;

path.scope

表示当前path下的作用域,这个也是写插件经常会用到的。

path.parentPath

用于获取当前path下的父path,多用于判断节点类型。

path.parent

用于获取当前path下的父node,多用于判断节点类型。其中:

path.parent == path.parentPath.node;//这两者是等价的

path.container

用于获取当前path下的所有兄弟节点(包括自身),container翻译过来是容器的意思,它是一个Array类型,可以写个简单的插件来看看效果:


let jscode = "var a = 1,b = 2;var c = 3;";

const getcontainer  = 
{
  VariableDeclarator(path)
  {
    console.log(path.container.length);
  }
}

traverse(ast,getcontainer);

输出结果:
在这里插入图片描述

代码中a,b互为兄弟节点,c只有一个节点,所以结果显示2,2,1。

这里要注意,是获取所有的所有兄弟节点(包括自身),不是path类型。

path.type

用于获取当前path的节点类型,写个简单的插件来看看效果 :

let jscode = "var a = 1 + 2;";

const getType  = 
{
  "VariableDeclarator|BinaryExpression|Identifier"(path)
  {
    console.log(path.type);
    console.log(path.toString());
  }
}

traverse(ast,getType);

输出结果:

在这里插入图片描述

path.key

用于获取当前path的key值,key通常用于path.get函数,当然还有更多的用法等待大家去挖掘 ,写个简单的插件来看看效果 :


let jscode = "var a = 1 + 2;";

const getPathKey  = 
{
  "VariableDeclarator|BinaryExpression|Identifier"(path)
  {
    console.log(path.key);
    console.log(path.toString());
  }
}

traverse(ast,getPathKey);

输出结果:
在这里插入图片描述
输出结果可能大家看不懂,我在这里解释下。

第一个遍历的 path 是 VariableDeclarator 类型,对应源代码 是 a = 1+2 ;

它的父节点是 VariableDeclaration 类型,而它处在 declarations 的第0的位置,因此输出是 0;
在这里插入图片描述
第二个遍历的 path 是 Identifier类型,对应源代码 是 a ;它是 id 节点,因此它的输出是 id;

在这里插入图片描述
第三个遍历的 path 是 BinaryExpression 类型,对应源代码 是 1+2 ;它是 init 节点,因此它的输出是 init;

在这里插入图片描述
还有其他的属性,因为不常用,因此不再列举出来。大家可以自行看源码了解其功能。

第3篇:babel库中path常用方法总结

path源码学习:

path相关的源代码在这个js文件中,大家可以直接照着源码学习:

\node_modules\@babel\traverse\lib\path

当然,第一个需要学习的文件应该是这个:

node_modules\@babel\traverse\lib\path\index.js

在本文中,选出部分常用的方法大家参考,更多的知识请自行学习源码。

path常用方法介绍

path.toString

用来获取当前遍历path的js源代码,调用方式:


let sourceCode = path.toString();

这个非常有用,经常用于定于插件遍历的path,以便分析问题。如果想通过 path.node来获取源代码,可以使用 generator 函数来获取:


let sourceCode = generator(path.node).code;

path.replaceWith

(单)节点替换函数,调用方式:


path.replaceWith(newNode);

实参一般是 node 类型,即将当前遍历的path替换为 实参里的 新节点。

注意,它不能用于 Array 的替换,即实参不能是Array类型。

path.replaceWithMultiple

(多)节点替换函数,调用方式:


path.rreplaceWithMultiple(ArrayNode);

实参一般是 Array 类型,它只能用于 Array 的替换。即所有需要替换的节点在一个Array里面,常见的替换如 Block 类型节点里的 body.body。

path.remove

节点的删除,调用方式:

path.remove();

直接调用即可将当前遍历的所有符合条件的路径全部删除,所以使用的时候需要注意。

path.insertBefore

在当前节点前面插入新的节点。调用方式:

path.insertBefore(newNode);

path.insertAfter

在当前节点后面插入新的节点。调用方式:

path.insertAfter(newNode);

path.traverse

在当前节点下遍历其他的节点,比如有如下for循环:

for(;;)
{
  a = b;
  b = c;
  c =d;
}

想要遍历这个for循环下面的 赋值语句,可以借助该函数进行遍历:


const visitFor = 
{
  ForStatement(path)
  {
    ......
    
    path.traverse({
      AssignmentExpression(_path)
      {
        ......
      },
    }),
    
    ......
  },
}

path.get

获取当前路径下的子孙节点。调用方式:

let newPath = path.get(key)

形参key是一个字符串,以 . 隔开。 其源代码:


function get(key, context) {

  if (context === true) context = this.context;

  const parts = key.split(".");



  if (parts.length === 1) {

    return this._getKey(key, context);

  } else {

    return this._getPattern(parts, context);

  }

}

假如当前的代码如下:

var a = 1;

你遍历了 VariableDeclaration 类型,想要获取 右边的这个 1,可以写如下代码:

const visitVar = 
{
  VariableDeclaration(path)
  {
    let initPath = path.get('declarations.0.init');
    ......
  },
}

path.getSibling

根据key值来获取节点,这个方法我很少用,大家可以试试。

path.getPrevSibling

获取当前path的前一个兄弟节点,返回的是path类型。

path.getAllPrevSiblings

获取当前path的所有前兄弟节点,返回的是Array类型,其元素都是path类型。

path.getNextSibling

获取当前path的后一个兄弟节点,返回的是path类型。

path.getAllNextSiblings

获取当前path的所有后兄弟节点,返回的是Array类型,其元素都是path类型。

path.evaluate

用于计算表达式的值,大家可以参考 constantFold 插件的写法。

//常量折叠插件更新

const constantFold = {
    "BinaryExpression|UnaryExpression"(path) {
        if(path.isUnaryExpression({operator:"-"}) || 
    	   path.isUnaryExpression({operator:"void"}))
    	{
    		return;
    	}
        const {confident, value} = path.evaluate();
        if (!confident)
            return;
        if (typeof value == 'number' && (!Number.isFinite(value))) {
            return;
        }
        path.replaceWith(types.valueToNode(value));
    },
}

traverse(ast, constantFold);

path.findParent

向上查找满足回调函数特征的path,即判断上级路径是否包含有XXX类型的节点:


function findParent(callback) {
  let path = this;

  while (path = path.parentPath) {

    if (callback(path)) return path;

  }

  return null;

}

很简单,先从父级节点开始进行遍历,然后一直循环,直到满足回调函数的条件为止。如果找不到则返回null。回调函数怎么写,可以参考下面的代码:


function getFunctionParent() {

  return this.findParent(p => p.isFunction());

}

path.find

功能与 path.findParent 方法一样,只不过从当前path开始进行遍历。

path.getFunctionParent

获取函数类型父节点,如果不存在,返回 null。

path.getStatementParent

获取Statement类型父节点,这个基本上都会有返回值,如果当前遍历的是 Program 或者 File 节点,则会报错。

path.getAncestry

获取所有的祖先节点,没有实参,返回的是一个Array对象。源码也很简单:


function getAncestry() {
  let path = this;
  const paths = [];
  do {

    paths.push(path);

  } while (path = path.parentPath);

  return paths;

}

path.isAncestor

判断当前遍历的节点是否为实参的祖先节点,调用方式:

path.isAncestor(maybeDescendant)

实参传入 path 类型即可。

path.isDescendant

判断当前遍历的节点是否为实参的子孙节点,调用方式:

path.isDescendant(maybeAncestor)

实参传入 path 类型即可。

第4篇:Scope和Binding常用方法及属性总结

scope常用方法及属性总结:

scope相关的源代码在这个js文件中,大家可以直接照着源码学习:

node_modules\@babel\traverse\lib\scope\index.js

scope的属性定义:


class Scope {
  constructor(path) {
    this.uid = void 0;
    this.path = void 0;
    this.block = void 0;
    this.labels = void 0;
    this.inited = void 0;
    this.bindings = void 0;
    this.references = void 0;
    this.globals = void 0;
    this.uids = void 0;
    this.data = void 0;
    this.crawling = void 0;
    const {
      node
    } = path;

    const cached = _cache.scope.get(node);

    if ((cached == null ? void 0 : cached.path) === path) {
      return cached;
    }

    _cache.scope.set(node, this);

    this.uid = uid++;
    this.block = node;
    this.path = path;
    this.labels = new Map();
    this.inited = false;
  }
  ......
  }

在本文中,选出部分常用的属性和方法供大家参考,更多的知识请自行学习源码。

scope.block

表示当前作用域下的所有node,参考上面的 this.block = node;

scope.dump()

输出当前每个变量的作用域信息。调用后直接打印,不需要加打印函数

scope.crawl()

重构scope,在某种情况下会报错,不过还是建议在每一个插件的最后一行加上。

scope.rename(oldName, newName, block)

修改当前作用域下的的指定的变量名,oldname、newname表示替换前后的变量名,为字符串。注意,oldName需要有binding,否则无法重命名。

scope.traverse(node, opts, state)

遍历当前作用域下的某些(个)插件。和全局的traverse用法一样。

scope.getBinding(name)

获取某个变量的binding,可以理解为其生命周期。包含引用,修改之类的信息

binding常用方法及属性总结:

binding相关的源代码在这个js文件中,大家可以直接照着源码学习:

node_modules\@babel\traverse\lib\scope\binding.js

binding的属性定义:

class Binding {
  constructor({
    identifier,
    scope,
    path,
    kind
  }) {
    this.identifier = void 0;
    this.scope = void 0;
    this.path = void 0;
    this.kind = void 0;
    this.constantViolations = [];
    this.constant = true;
    this.referencePaths = [];
    this.referenced = false;
    this.references = 0;
    this.identifier = identifier;
    this.scope = scope;
    this.path = path;
    this.kind = kind;
    this.clearValue();
  }
  ......
  }

目前我看到的只有 变量定义 和 函数定义 拥有binding,其他的获取binding都是undefined。

let binding = scope.getBinding(name);
例如:
var a = 123; 这里的 a 就拥有 binding。

而 function test(a,b,c) {};

函数名test以及形参a,b,c均拥有 binding。

binding.path

用于定位初始拥有binding的path;

binding.constant

用于判断当前变量是否被更改,true表示未改变,false表示有更改变量值。

binding.referenced

用于判断当前变量是否被引用,true表示代码下面有引用该变量的地方,false表示没有地方引用该变量。注意,引用和改变是分开的。

binding.referencePaths

它是一个Array类型,包含所有引用的path,多用于替换。

binding.constantViolations

它是一个Array类型,包含所有改变的path,多用于判断。

在这里插入图片描述
你可以参考下面的这篇文章,学习更多scope相关的基础知识,感谢群友的总结:

https://evilrecluse.top/post/7389a59f/#%E4%BD%9C%E7%94%A8%E5%9F%9FScope-%E4%B8%8E-%E8%A2%AB%E7%BB%91%E5%AE%9A%E9%87%8FBinding

第5篇:如何获取path/node 节点的源代码。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值