把所有诊断都变成红灯,门禁很快就会失去可信度。开发者不知道这次是会产生运行错误,还是只少了一个空格;管理员却开始习惯性地替大家点跳过。
问题不在于规则够不够“智能”,而在于每条规则有没有清楚的任务:它识别什么,证据在哪里,为什么需要阻断,又会漏掉什么。
规则性质与阻断级别是两条轴
ESLint 的规则元信息把性质分为 problem、suggestion、layout。严重程度则在配置中指定为 off、warn、error。不要把 meta.type 写成 problem,就以为它会自动阻断 CI;也不要认为 layout 类型不能配置成 error。性质与团队决策分别存放,才便于复查。Custom Rules
| 性质 | 讨论的问题 | 本文建议的处置 |
|---|---|---|
| problem | 代码中存在明确问题或约定禁止的能力 | 有足够证据时阻断 |
| suggestion | 有更好的写法,但是否适用有上下文 | 提醒并说明理由 |
| layout | 空白、分号等表现形式 | 优先交给格式化工具 |
最后一列是工程选择,不是 ESLint 官方对所有团队的要求。代码生成文件、兼容层、历史迁移文件都可能需要不同配置。
写一条窄规则,别宣称“消灭所有注入”
下面是可独立保存运行的规则实验。它禁止直接写出eval(...),不会把注释里的单词当调用。为了让范围容易看清,测试故意保留一个漏检反例。
// 依赖 eslint@10.0.0;保存为 lint-case.mjs import assert from 'node:assert/strict'; import { Linter } from 'eslint'; const noDirectEval = { meta: { type: 'problem', schema: [], messages: { forbidden: '此模块禁止直接调用 eval。' } }, create(context) { return { CallExpression(node) { if (node.callee.type === 'Identifier' && node.callee.name === 'eval') { context.report({ node, messageId: 'forbidden' }); } } }; } }; const config = [{ languageOptions: { ecmaVersion: 2022, sourceType: 'module' }, plugins: { local: { rules: { 'no-direct-eval': noDirectEval } } }, rules: { 'local/no-direct-eval': 'error' } }]; const linter = new Linter(); const diagnose = code => linter.verify(code, config); assert.equal(diagnose('eval("1 + 1")').length, 1); assert.equal(diagnose('const text = "eval(x)";').length, 0); // 当前规则只匹配直接调用。这个结果暴露范围,不代表代码安全。 assert.equal(diagnose('(0, eval)("1 + 1")').length, 0); const warning = [{ ...config[0], rules: { 'local/no-direct-eval': 'warn' } }]; assert.equal(linter.verify('eval("1")', warning)[0].severity, 1); console.log('narrow rule checks passed');把测试里的第三个结果藏起来,再写“AST百分百拦住动态执行”,就是在拿读者的信任换标题。AST 给的是结构化语法证据,完整的数据流、别名、跨模块行为还要靠其他分析。
同理,找到了addEventListener却没找到removeEventListener,也不能立即断言泄漏。监听可能使用 AbortSignal 清理,可能由封装层管理,也可能刻意跟随页面生命周期。规则必须说明自己覆盖了哪些清理模式,以及哪些情况只能给建议。
阻断规则需要反例,而不只是一个红灯样例
一条规则至少要准备三组输入:
第一组是应当阻断的代码,能定位到具体语法节点。第二组是相似但合法的写法,防止字符串、注释和不同作用域被误伤。第三组是已知超出能力范围的写法,明确留在边界说明中。
修复建议也有级别。删除一个尾随空格与改写一段异步控制流,不能共享同一种“自动修复”权限。涉及生命周期或业务分支时,生成一个待审差异通常比直接修改更合适。
这个例子没有实现类型分析,没有处理 TypeScript、Vue SFC 或所有动态执行方式。引入这些语法后,应使用相应解析器和真实工程样本重新测试,不能把一份 JavaScript 规则直接挂到所有文件上。
CI 才是团队共同遵守的检查点
本地诊断可以缩短反馈时间,服务端仍要保留必需检查。保护分支的检查名称、管理员是否可绕过、谁能修改规则,都属于门禁有效性的一部分。GitHub 文档还提示,工作流里重复的 job 名称会造成状态检查歧义。保护分支说明
不要用“有一个脚本”代替这些配置证据。也别把一次规则通过解释成整个系统没有安全问题。
规则示例的完整行为验证依赖 ESLint 10。没有执行依赖测试的环境,只能核对代码语法和预期样例,不能写成运行通过;生产误报率、漏报率与性能开销也都没有在本文测量。文档未标首发日,按现行接口说明使用。