- 开发工具
【免费下载链接】fixmyjs
Automatically fix silly lint errors.
fixmyjs 是一款自动修复 JavaScript lint 错误的开源工具,它能以"非破坏性"方式修复 eqeqeq、curly、命名风格等常见 JSHint 规则。本文深入剖析其 AST 遍历架构,手把手教你编写自定义修复规则:从规则文件结构、注册机制到测试验证,完整走通扩展流程,让你轻松扩展 fixmyjs 的修复能力。
📋 内置规则一览:了解 fixmyjs 能修什么
fixmyjs 的选项命名基本沿用 JSHint,所有规则实现在 lib/rules/ 目录下:
| 选项 | 作用 | 规则文件 |
|---|---|---|
delete | 移除变量删除语句 | delete.js |
emptyStatement | 移除空语句 | emptyStatement.js |
camelcase | 标识符转 camelCase | camelCase.js |
curly | 为语句补全花括号 | curly.js |
es3 | 为parseInt补 radix 参数 | parseInt.js |
eqeqeq | 强制严格相等 | eqeqeq.js |
debug(false 时生效) | 移除debugger | debugger.js |
sub(false 时生效) | 方括号转点语法 | dotNotation.js |
💡 每个规则文件都只有十几行代码,这正是 fixmyjs 架构的精髓:一条规则 = 一个按 AST 节点类型匹配的转换函数。
🔍 理解 AST 流水线:修复代码的 5 个步骤
核心入口是 lib/index.js 中的fix(code, config),整个修复流程只有 5 步:
- 校验选项——
validateRules检查互斥配置(如camelcase与snakecase不能同时开启),见 lib/index.js - 解析代码—— 用
esprima.parse把源码解析为 AST,开启range、tokens、comment选项(lib/index.js),同时先剥离 shebang 行 - 挂载注释——
escodegen.attachComments把注释挂到 AST 节点上,保证修复后注释不丢失 - 遍历并修复—— 核心步骤!递归遍历 AST,对每个节点应用所有已启用规则
- 生成代码——
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 testnpm 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.
相关推荐
编写 PMD Java 自定义规则(Java Rules)完整指南:从 AST 遍历到生命周期管理
编写 PMD Java 自定义规则(Java Rules)完整指南:从 AST 遍历到生命周期管理 本文是一份面向 PMD 开发者的实战指南,系统讲解如何用 J
静态分析代码质量如何快速开发 Codemod 扩展:编写自定义重构规则的完整指南
如何快速开发 Codemod 扩展:编写自定义重构规则的完整指南 Codemod 是 Facebook 开发的开源工具库,专为大规模代码库重构设计,可部分自动化
如何为TYAlertController编写专属转场动画?基于TYBaseAnimation的完整自定义教程
如何为TYAlertController编写专属转场动画?基于TYBaseAnimation的完整自定义教程 TYAlertController 是一款强大的
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考