ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

如何为 fixmyjs 编写自定义修复规则:基于 AST 遍历架构的完整开发教程

如何为 fixmyjs 编写自定义修复规则:基于 AST 遍历架构的完整开发教程
  • 开发工具

【免费下载链接】fixmyjs

Automatically fix silly lint errors.

项目地址:https://gitcode.com/gh_mirrors/fi/fixmyjs
点击查看免费下载

fixmyjs 是一款自动修复 JavaScript lint 错误的开源工具,它能以"非破坏性"方式修复 eqeqeq、curly、命名风格等常见 JSHint 规则。本文深入剖析其 AST 遍历架构,手把手教你编写自定义修复规则:从规则文件结构、注册机制到测试验证,完整走通扩展流程,让你轻松扩展 fixmyjs 的修复能力。

📋 内置规则一览:了解 fixmyjs 能修什么

fixmyjs 的选项命名基本沿用 JSHint,所有规则实现在 lib/rules/ 目录下:

选项作用规则文件
delete移除变量删除语句delete.js
emptyStatement移除空语句emptyStatement.js
camelcase标识符转 camelCasecamelCase.js
curly为语句补全花括号curly.js
es3为parseInt补 radix 参数parseInt.js
eqeqeq强制严格相等eqeqeq.js
debug(false 时生效)移除debuggerdebugger.js
sub(false 时生效)方括号转点语法dotNotation.js

💡 每个规则文件都只有十几行代码,这正是 fixmyjs 架构的精髓:一条规则 = 一个按 AST 节点类型匹配的转换函数。

🔍 理解 AST 流水线:修复代码的 5 个步骤

核心入口是 lib/index.js 中的fix(code, config),整个修复流程只有 5 步:

  1. 校验选项——validateRules检查互斥配置(如camelcase与snakecase不能同时开启),见 lib/index.js
  2. 解析代码—— 用esprima.parse把源码解析为 AST,开启range、tokens、comment选项(lib/index.js),同时先剥离 shebang 行
  3. 挂载注释——escodegen.attachComments把注释挂到 AST 节点上,保证修复后注释不丢失
  4. 遍历并修复—— 核心步骤!递归遍历 AST,对每个节点应用所有已启用规则
  5. 生成代码——escodegen.generate把改过的 AST 打印回源码,再把 shebang 还原

其中最关键的是第 4 步的traverse函数(lib/index.js):

function traverse(o, f, p) { var k var self = function (x) { return traverse(x, f, p) } for (k in o) { if (toString.call(o[k]) == '[object Object]') { o[k] = traverse(o[k], f, o) } else if (Array.isArray(o[k])) { o[k] = fu.concatMap(self, o[k]) } } return f(o, p) }

它的逻辑非常直白:

  • 递归进入 AST 的每个对象与数组
  • 每访问一个节点,就调用f(node, parent)—— 把当前节点和父节点一起交给规则处理
  • 规则的返回值直接替换原节点

而f是怎么从一堆规则中挑出"该处理这个节点类型"的那条规则的呢?看 lib/index.js:

var modifiedTree = traverse(astWithComments, function (node, parent) { return fu.foldl(function (node, f) { return f.hasOwnProperty(node.type) ? fnode.type : node }, rules, node) })

这就是自定义规则的契约:规则文件导出一个对象,键是 AST 节点类型(如BinaryExpression、CallExpression),值是处理函数。节点类型匹配不上就原样返回——所以写规则时天然安全。

🧩 规则文件解剖:三个最小示例

阅读现有规则是最好的学习材料,下面三个示例覆盖了三种典型模式。

模式一:删除节点—— debugger.js

module.exports = { DebuggerStatement: rmDebuggerStmt } function rmDebuggerStmt() { return [] }

返回空数组[]表示"用零个节点替换当前节点",即删除。debugger;就这样被干掉了。

模式二:替换节点(带守卫条件)—— eqeqeq.js

module.exports = { BinaryExpression: toStrictEquality } function toStrictEquality(node) { if (node.operator != '==' && node.operator != '!=') { return node } return { type: 'BinaryExpression', operator: node.operator == '==' ? '===' : '!==', left: node.left, right: node.right } }

注意守卫条件:不是所有BinaryExpression都要改,只处理==/!=。不需要修改时原样返回node,这是所有规则的统一写法。

模式三:结合父节点判断—— emptyStatement.js

function rmEmpty(node, parent) { if (parent) { switch (parent.type) { case 'ForStatement': case 'IfStatement': case 'WhileStatement': return node } } return [] }

这里用了第二个参数parent:for (;;)和if (x);中的空语句是合法且必须保留的,所以要结合父节点类型决定是否删除。

返回值速查表

返回值效果
原node不修改
新节点对象原地替换
[]删除该节点
节点数组一个节点拆成多个(见下文 multiVar 技巧)

🛠️ 实战:编写你的第一条自定义规则 noVoidZero

我们以一个真实需求为例:把void 0重写为undefined(JSHint 的经典W073提示),全程 4 步。

第 1 步:创建规则文件

新建lib/rules/noVoidZero.js,参考 parseInt.js 的守卫写法:

module.exports = { UnaryExpression: rmVoidZero } function rmVoidZero(node) { if (node.operator !== 'void' || node.argument.type !== 'Literal' || node.argument.value !== 0) { return node } return { type: 'Identifier', name: 'undefined' } }

逻辑:只拦截void 0这一种一元表达式,其余一律原样返回。

第 2 步:注册规则

打开 lib/rules/index.js,规则分三个桶:

  • builtin:始终启用(如delete、isNaN)
  • aretrue:对应选项为true时启用(如curly: true)
  • arefalse:对应选项为false时启用(如debug: false才移除debugger)

我们的规则属于"显式开启"型,在aretrue中加入一行:

novoidzero: require('./noVoidZero'),

注册机制在 lib/index.js 的getRules中:它根据配置对象逐桶收集启用的规则,拼成最终规则数组。选项的取值逻辑由genHas提供——配置中未显式声明的选项默认不生效,这一点测试时容易踩坑。

第 3 步:编写测试

fixmyjs 使用testla作为测试框架,测试文件统一放在 test/ 目录、以-test.js结尾。test/dependencies.js 会给每个测试注入两个函数:

  • f—— 即fixmyjs.fix(AST 路径,我们测试的对象)
  • l—— legacy 模式入口

参考 test/eqeqeq-test.js 的写法,新建test/noVoidZero-test.js:

module.exports = function (f, assert) { var options = { novoidzero: true, _silent: true } return { 'void 0 is replaced by undefined': function () { assert.equal(f('x = void 0;', options), 'x = undefined;') }, 'other unary expressions stay': function () { var code = 'x = -1;' assert.equal(f(code, options), code) } } }

⚠️务必同时写正向用例和反向用例(不该改的不能改),这是本项目测试文件的通行惯例。

第 4 步:运行测试并验证

npm test

npm test会先跑jshint .做 lint,再用testla执行全部测试(见 package.json)。全部通过后,还可以用 CLI 做一次"干跑"验证:准备一份配置{"novoidzero": true},执行:

node lib/cli.js -r -c config.json demo.js

-r即 dry-run,只打印彩色 diff 而不写回文件(实现见 lib/cli.js),确认输出符合预期后再去掉-r真正落盘。

🚀 进阶技巧:让规则更聪明

1. 善用 parent 避免误伤。除 emptyStatement.js 外,newSideEffects.js 只在父节点是ExpressionStatement时才剥离new(避免破坏赋值场景);camelCase.js 则跳过对象属性与成员表达式,只转换普通标识符。

2. 一个节点拆成多个节点。multiVar.js 在处理var a = 1, b = 2时返回一个数组,把一条声明拆成多条var语句——处理器返回数组时,traverse会将其展开替换原节点。

3. 使用 fu 函数式小工具。项目依赖fu库,fu.concat、fu.map、fu.filter、fu.intoArray等在各规则中随处可见(如 isNaN.js),保持风格一致即可。

4. 注释与 shebang 已被核心层处理。注释通过attachComments+escodegen的comment: true自动保留;shebang 行在解析前剥离、生成后还原(lib/index.js)。规则作者无需操心,也不要在规则里手动处理它们。

🧪 测试规范与选项校验

  • 新增测试文件命名:test/xxx-test.js,导出function (f, assert),见 test/curly-test.js
  • 互斥选项的校验有专门测试:test/validate-rules-test.js 验证camelcase与snakecase同时开启会抛错。如果你的新规则与其他选项互斥,记得在validateRules中补充判断
  • 代码风格遵循 CONTRIBUTING.md:2 空格缩进、单引号、ASI(无分号)、行宽 80 字符

📚 延伸:Legacy 模式是另一套修复体系

lib/legacy.js 实现了旧版修复逻辑:它不解析 AST,而是依赖 JSHint 报告的错误码(如W033、W065),用正则与字符串替换逐行修复(错误码映射表见 lib/legacy.js)。CLI 中通过-l启用。

💡 官方在 README 中说明:AST 方案比字符串替换更容易编写、维护和支持更多能力,因此新规则建议一律写在 AST 路径(即前文的规则文件格式),legacy 仅用于兼容旧行为。

✅ 交付前自检清单

在提交自定义规则前,逐项确认:

  • 处理函数以node.type匹配的守卫开头,不相关节点原样返回
  • 规则注册到了正确的桶(builtin/aretrue/arefalse)
  • 测试同时覆盖"应修改"与"不应修改"两类用例
  • 选项未显式开启时规则不生效(genHas的 fallback 行为)
  • npm test(lint + testla)全部通过
  • 用node lib/cli.js -r干跑 diff 人工复核过输出

至此,你已经完整掌握 fixmyjs 的 AST 遍历架构与规则开发全流程:esprima解析 → 按节点类型分发 → 规则函数返回新节点 →escodegen生成。下一条规则,可能只需 15 行代码。

  • 开发工具

【免费下载链接】fixmyjs

Automatically fix silly lint errors.

项目地址:https://gitcode.com/gh_mirrors/fi/fixmyjs
点击查看免费下载

相关推荐

上一篇:Tilck 开源项目教程:构建你自己的小型 Linux 兼容内核
下一篇:biliLive-tools云存储同步教程:百度网盘、阿里云盘、Alist全攻略

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表