Babel 删除未被使用的 function 和由 var,let,const 定义的未使用变量

需求

删除未被使用的无用函数、变量,简化代码

代码样例(encode.js)

var a = 1, b = 2, d, aa = 11;
let c = b + 3;
const f = 5;
console.log(aa);

function test_1() {
    console.log('I\'m test_1.');
}

function test_2() {
    console.log('I\'m test_2.');
}

function g() {
    var g = 1;
    g += 1;
    g += 1;
    console.log('g is ' + g)
}

test_2();

处理后代码(decode.js)

var b = 2,
    aa = 11;
console.log(aa);

function test_2() {
  console.log('I\'m test_2.');
}

test_2();

思路

  1. 通过 ast explorer 在线解析网站对比可发现
    • function 函数都是 FunctionDeclaration
    • var,let,const 定义的变量节点类型都是 VariableDeclarator
  2. 有被使用/引用的函数、变量不做删除处理

编写 babel 插件

目标节点为 FunctionDeclarationVariableDeclarator
函数、变量的 binding 关系类似以下结构

{
  identifier: node,
  scope: scope,
  path: path,
  kind: 'var',

  referenced: true,
  references: 3,
  referencePaths: [path, path, path],

  constant: false,
  constantViolations: [path]
}

显然 referencedtrue 则代表函数、变量被使用/引用

故插件 visitor 可以写成如下

visitor =
{
  'VariableDeclarator|FunctionDeclaration'(path) {
    const { id } = path.node;
    let binding = path.scope.getBinding(id.name);
    if (binding.referenced) {
      return;
    }
    path.remove();
  }
}

但是,有这么一种特殊情况,若是函数中存在变量与函数名相同,且该变量被使用/引用
那么按照上述的处理方式该函数若是未被使用是不会被删除
使用 path.scope.dump() 可以看到按此方式获取 binding 显然 g 函数是被“使用”了的
path-scope-dump
因为我们现在所处的作用域是函数作用域
所以我们要判断该函数是否被使用应该从程序(program)作用域出发
如下吗,即可正确获取函数是否被使用情况
path-scope-dump

故,最终、完整插件代码如下

// decrypt.js
const fs = require('fs');
var util = require('util');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const types = require('@babel/types');
const generator = require('@babel/generator').default;

// 程序启动时间
var time_start = new Date().getTime()
// 读取文件
process.argv.length > 2 ? encode_file = process.argv[2] : encode_file = 'encode.js';
process.argv.length > 3 ? decode_file = process.argv[3] : decode_file = 'decode.js';

let jscode = fs.readFileSync(encode_file, { encoding: 'utf-8' });
console.log(util.format('Reading the file [%s] is complete.', encode_file))
// 转换为 ast 树
let ast = parser.parse(jscode);

const visitor =
{
  VariableDeclarator(path) {
    const { id } = path.node;
    let binding = path.scope.getBinding(id.name);

    if (binding.referenced) {
      return;
    }
    path.remove();
    path.scope.crawl();
  },
  FunctionDeclaration(path) {
    const { id } = path.node;
    // 防止函数中存在变量与函数名相同,且该变量在函数中使用,导致未去除未使用函数
    let binding = path.scope.parent.getBinding(id.name);

    if (binding.referenced) {
      return;
    }
    path.remove();
    // 手动更新 scope ,防止影响下个插件使用
    path.scope.crawl();
  }
}

//调用插件,处理待处理 js ast 树
traverse(ast, visitor);
console.log('AST traverse completed.')

// 生成处理后的 js
let { code } = generator(ast);
console.log('AST generator completed.')
fs.writeFile(decode_file, code, (err) => { });
console.log(util.format('The javascript code in [%s] has been processed.', encode_file))
console.log(util.format('The processing result has been saved to [%s].', decode_file))
// 程序结束时间
var time_end = new Date().getTime()
console.log(util.format('The program runs to completion, time-consuming: %s s', (time_end - time_start) / 1000))

推荐阅读

参考

Table of Contents