babel: 将单行声明改为多行-AST

当我们遇到代码中有大量如下代码,有时候格式化不一样的时候,是非常头痛的,有没有办法处理?

let a,b,c,v,d;

var name,age,email, pageSize;

我们希望变成

let a;
let b;
let c;
let v;
let d;

var name;
var age;
var email;
var pageSize;

答: 有的

今天我们通过AST 的方式操作一下, 顺便学习一下 babel 操作 AST

分析

通过在线 AST explorer 我们编写简单的代码,尝试查看一下

babel: 将单行声明改为多行-AST

先看最外层,全局作用域的,其实就是 body 里面有3条 声明语句

// 单行的
let a, b,c = 1

// 跟下面的单行声明
let d
let k

babel: 将单行声明改为多行-AST

再单看第一条,查看里面的子节点, 也有3条,刚好对应 a,b,c = 1

我们再看看下面的 let d 这个节点

babel: 将单行声明改为多行-AST

你就是说,我们把第一个声明语句里面的的 除了第一个,其余的 N个子节点都拿出来,挨个遍历,创建同一个层级的 VariableDeclaration 类型节点,就实现了我们要的效果。

分析清楚了AST节点结构,我们现在讲一下主体代码思路

实现思路

  1. 拿到字符串(可以是当前ts/js中写的测试代码, 也可以是读取到的文件)
  2. 把字符串js代码或者 ts代码, 转成 AST
  3. 使用 traverse 遍历 AST, 根据 AST 特性,找到 VariableDeclaration 这个钩子,在这里做拦截处理
  4. 将多个变量声明变成同节点的单个变量声明(具体看下面代码实现), 需要手动构建相同的类型声明,比如之前使用let 或者const定义的, 拆分成多条声明语句,仍旧要保持一样的类型声明。

注意: 我们改变的是写法,并不是对原来代码做破坏性改动

  1. 使用 @babel/generatorAST还原成代码

环境安装

node: 20.10.0
pnpm init

安装babel相关的核心包

pnpm add @babel/generator @babel/parser @babel/traverse @babel/types -S
  • @babel/parser 把字符串解析成 AST
  • @babel/traverse 操作/新增/删除/修改 AST节点
  • @babel/types 手动构建AST节点
  • @babel/generator 把处理完成后的AST节点树,变成代码, 最终是一个字符串

安装ts, 不想使用ts, 可以不装

pnpm add typescript tslib ts-node -D
  • tslib 与 ts-node 搭档, ts-node依赖的
  • ts-node 直接运行ts文件

安装类型声明的包

pnpm add @types/node @types/babel-types @types/babel__generator @types/babel__traverse -D

注意:

  • 不要把 package.json 中的 type设置为 module, 直接不设置这个字段
  • 修改ts.config.json里面的 module: "NodeNext", 其他配置可以不动, 保证 ts-node 运行ts文件的时候不会有问题

package.json

"dependencies": {
    "@babel/generator": "^7.23.6",
    "@babel/parser": "^7.23.9",
    "@babel/traverse": "^7.23.9",
    "@babel/types": "^7.23.9"
  },
  "devDependencies": {
    "@types/babel-types": "^7.0.15",
    "@types/babel__core": "^7.20.5",
    "@types/babel__generator": "^7.6.8",
    "@types/babel__traverse": "^7.20.5",
    "@types/node": "^20.11.19",
    "ts-node": "^10.9.2",
    "tslib": "^2.6.2",
    "typescript": "^5.3.3",
  }

代码实现

测试代码:
demo/05.js

let a, b = 1

var c,d

const kk = 2

function test() {
  var b,g

  return () => {
    b = 10
    console.log(b);

    let n, m

    return {
      test() {
        var L1, L2
      }
    }
  }
}

demo.ts

import generate from '@babel/generator'
import { parse } from '@babel/parser'
import traverse from '@babel/traverse'
import * as types from '@babel/types'

import fs from 'node:fs'
import path from 'node:path'

const target = path.resolve(__dirname, '../demo/05.js')

const content = fs.readFileSync(target, {
    encoding: 'utf-8',
})

let ast = parse(content, {
    sourceType: 'module',
})

traverse(ast, {
    /**
     * 变量声明
     * @param path
     */
    VariableDeclaration(path) {
        let declarations = path.node.declarations
        // 只针对 path.node里面 声明有多个的情况处理,已经是单个的,不管
        if (declarations.length > 1) {
            const list = declarations.slice(1)

            const first = declarations.shift()
            // ts里面就需要这样做一下类型判断, 判断我们拿出来的节点是一个 类型声明节点
            if (types.isVariableDeclarator(first)) {
                // 把原来的多个声明改为一个
                path.node.declarations = [first]
            }

            // 注意: 
            // 这里要考虑2种情况,一是全局环境,也就是global 最外层的声明
            // 另一种是函数作用域里面的变量声明要改写
            if ((path.parent.type === 'Program' || path.parent.type === 'BlockStatement') && path.parent.body) {
                // 逐个遍历,把节点插入到父节点中
                list.forEach((item) => {
                    // 先拿到之前的声明,之前是 let, 改为多行声明仍旧用let,之前为 const,仍旧用const
                    let kind = path.node.kind
                    const currentNode = types.variableDeclaration(kind, [
                        types.variableDeclarator(
                            // 原来的名字
                            item.id,
                            // 原来的值
                            item.init
                        ),
                    ])

                    currentNode.declarations = [item]

                    // 在指定位置插入,不能直接  body.push
                    // body.splice(pos + 1, 0, currentNode)
                    path.insertAfter(currentNode)
                })
            }
        }
    }
})

console.log('----处理后的结果-----');

console.log(generate(ast).code)

运行:

ts-node demo.ts

结果:

let a;
let b = 1;
var c;
var d;
const kk = 2;        
function test() {    
  var b;
  var g;
  return () => {     
    b = 10;
    console.log(b);  
    let n;
    let m;
    return {
      test() {       
        var L1;      
        var L2;      
      }
    };
  };
}

效果杠杠的。

总结

从上面案例中,你可以学习到

  • 学习了使用ts-node 运行typescript代码
  • 操作代码的AST, 完整思路
  • AST还原成代码
  • @babel/types 手动构建 AST节点
  • AST节点分析

相关文章链接

如果觉得喜欢,点个赞吧 😊😊😊

原文链接:https://juejin.cn/post/7338349930149216271 作者:知了清语

(0)
上一篇 2024年2月24日 下午5:03
下一篇 2024年2月24日 下午5:13

相关推荐

发表回复

登录后才能评论