ESLint之所以能够检查代码规范,靠的不是字符串匹配,而是把源码解析成AST(抽象语法树)之后再遍历检查。理解了这一点,你就明白为什么它几乎能实现任何粒度的静态分析规则。本文将带你从AST基础开始,一步步用Node.js开发一个完整的自定义ESLint插件,并集成到真实项目中运行。

一、理解ESLint的工作原理与AST基础
ESLint的执行流程可以概括为三步:首先调用解析器(默认是Espree)把JavaScript源码转换成AST;然后根据配置中的启用的规则,为每条规则注册对应的节点访问器;最后深度优先遍历这棵语法树,每遇到匹配的节点就触发访问器回调,开发者在回调里通过context暴露的API上报问题。
AST是把代码结构化的产物。比如下面这行简单的赋值语句:
const count = 10;
解析后的AST顶层是一个VariableDeclaration节点,它内部的declarations数组里包含一个VariableDeclarator,左边的标识符是Identifier节点,右边的字面量是Literal节点。每条规则本质上就是在这些节点上做模式匹配。你可以用AST Explorer这个在线工具直观地查看任意代码片段对应的语法树结构,是插件开发过程中最常用的辅助手段。
ESLint规则的核心是一个包含meta和create两个字段的对象。meta描述规则的元信息,比如文档链接、类型(problem还是suggestion)、是否可自动修复等;create接收一个context参数,返回一个以选择器为键、回调函数为值的对象。选择器的写法和CSS选择器非常相似,例如CallExpression[callee.name='setTimeout']表示匹配所有调用setTimeout的表达式。
二、动手开发第一个自定义规则
假设团队规范禁止直接使用console.log调试线上代码,虽然ESLint内置了no-console规则,但我们需要更精细的控制,比如只允许log但不允许error之外的其他方法,同时要求必须来自特定模块。这里写一个禁止调用eval的规则作为入门示例,同时演示如何实现自动修复。
首先初始化插件工程,目录结构建议如下:
my-eslint-plugin/ ├── lib/ │ └── rules/ │ └── no-dangerous-eval.js ├── tests/ │ └── rules/ │ └── no-dangerous-eval.test.js ├── package.json └── index.js
规则的实现代码如下:
// lib/rules/no-dangerous-eval.js
module.exports = {
meta: {
type: 'problem',
docs: {
description: '禁止使用eval执行动态代码',
},
// 支持自动修复:把eval(xxx)替换成Function(xxx)
hasSuggestions: true,
messages: {
unexpected: '检测到eval调用,存在代码注入风险,请改用Function或JSON.parse。',
suggestFunction: '使用Function构造器替代eval',
},
},
create(context) {
return {
// 匹配名为eval的函数调用
"CallExpression > Identifier[name='eval']"(node) {
context.report({
node,
messageId: 'unexpected',
suggest: [
{
messageId: 'suggestFunction',
fix(fixer) {
return fixer.replaceText(node.parent.callee, 'Function');
},
},
],
});
},
};
},
};这段代码有几个关键点值得展开。第一,messageId配合meta.messages使用,把提示文案集中管理,方便国际化和测试。第二,context.report是上报问题的唯一入口,node参数决定了IDE里错误下划线画在哪个位置。第三,fix回调返回一个描述文本替换操作的对象,ESLint在执行--fix命令时会应用这些修改;而suggestions则提供了用户可手动确认的修复建议,适合没有十足把握的场景。
接着在index.js里把规则挂到插件上:
// index.js
const noDangerousEval = require('./lib/rules/no-dangerous-eval');
module.exports = {
rules: {
'no-dangerous-eval': noDangerousEval,
},
};三、编写单元测试与规则验证
ESLint官方提供了RuleTester工具,可以用声明式的方式定义合法用例和非法用例,不需要手动搭建测试环境。它的好处是把规则行为固化成文档,每次改动规则都能立刻发现是否影响了已有逻辑。
// tests/rules/no-dangerous-eval.test.js
const { RuleTester } = require('eslint');
const rule = require('../../lib/rules/no-dangerous-eval');
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2022 } });
ruleTester.run('no-dangerous-eval', rule, {
valid: [
'const obj = JSON.parse(str);',
'function eval() {} eval();', // 局部函数不算全局eval
],
invalid: [
{
code: 'eval("1 + 1");',
errors: [{ messageId: 'unexpected' }],
},
],
});
console.log('所有测试通过');RuleTester的valid数组里的代码如果触发了规则上报,测试就会失败;invalid数组则相反,每条用例必须真的产生指定数量的错误。这里有个容易踩的坑:选择器Identifier[name='eval']会匹配所有名为eval的标识符,包括局部定义的函数,所以实际项目中应该结合作用域分析,通过context.getScope()判断该标识符是否真的指向全局eval,否则会产生误报。这也是自定义规则和字符串匹配脚本的本质区别。
四、在真实项目中集成与发布
插件开发完成后,通过npm link或者在业务项目里以本地路径依赖的方式安装,然后在ESLint配置中启用即可。以扁平配置文件eslint.config.js为例:
// 业务项目的eslint.config.js
const myPlugin = require('my-eslint-plugin');
module.exports = [
{
plugins: {
'my-plugin': myPlugin,
},
rules: {
'my-plugin/no-dangerous-eval': 'error',
},
},
];发布到npm前要注意几点:package.json里把main字段指向index.js,files字段限制发布范围只包含lib目录,避免把测试代码带上去;给每个规则编写独立的Markdown文档,方便团队成员查阅规则的动机和替代方案;在CI流水线里加上npx eslint .步骤,配合--max-warnings 0把警告也视为失败,这样静态分析才真正卡住代码合并的关口。
另一个进阶方向是支持TypeScript项目的规则开发。方法是在插件中提供configs预设,把@typescript-eslint/parser声明为解析器,规则内部则通过context.parserServices.esTreeNodeToTSNodeMap访问类型信息,从而实现依赖类型推断的深度检查,例如校验函数返回值是否符合接口定义。掌握了这套流程后,团队的每一条编码规范都可以沉淀成一条规则,代码审查就能把精力集中在逻辑和设计层面,而不是反复纠正格式问题。