ESLint no-restricted-syntax 规则详解:用 AST 选择器精准禁用任意语法
【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint
no-restricted-syntax是 ESLint 提供的一条通用"语法禁用"规则:它允许开发者通过配置ESTree 节点类型或AST 选择器,一次性禁用任意不希望出现在代码库中的语法结构(如try-catch、class、with、in运算符等),从而避免为每一个想禁用的特性单独编写规则。读完本文,你将掌握该规则的字符串/对象两种配置格式、AST 选择器的完整语法、常见实战拦截场景,以及其底层基于 esquery 的实现原理,可直接在自己的 ESLint 配置中投入使用。
规则定位:为什么需要一条"通用禁用"规则
JavaScript 语言特性众多,不同团队对语言特性的偏好差异巨大。有些项目会明确禁止某些语法结构的出现——例如禁用try-catch、禁用class、禁用in运算符,甚至禁用未加花括号的if语句体。
如果为每个想禁用的特性单独创建一条 ESLint 规则,规则数量将难以维护。no-restricted-syntax的设计目标正是用一条规则覆盖所有"禁用语法"诉求:你只需把要禁用的语法元素以AST 选择器的形式配置进去即可。
在 JavaScript 语境下,每个语法元素都对应一个 ESTree 节点类型,例如函数声明对应FunctionDeclaration,with语句对应WithStatement。你可以借助代码解析工具(如 ESLint 官方 Code Explorer)查看一段代码会被解析成哪些节点,从而确定需要禁用的节点类型。
在 lib/rules/no-restricted-syntax.js 的规则元信息中可以看到,该规则的type为"suggestion"(建议类),默认不加入recommended配置,属于按需开启的可选规则:
meta: { type: "suggestion", docs: { description: "Disallow specified syntax", recommended: false, }, schema: { /* ... */ }, defaultOptions: [], }从源码结构看,该规则不提供自动修复(fix),因为"禁用某类语法"通常需要人工改写代码,无法机械替换。
核心概念:ESTree 节点与 AST 选择器
no-restricted-syntax的配置项本质上是一系列AST 选择器(AST Selector)。选择器是一种用于匹配抽象语法树(AST)中节点的字符串,其语法与 CSS 选择器高度相似,熟悉 CSS 的开发者可以很快上手。
最简单的选择器就是节点类型本身。例如选择器Identifier会匹配程序中的所有标识符节点;选择器WithStatement只匹配with语句节点。而更高级的组合选择器可以精确描述"某种特定形态的语法":
VariableDeclarator > Identifier:匹配直接父节点为VariableDeclarator的Identifier;FunctionDeclaration[params.length>2]:匹配参数个数大于 2 的函数声明;CallExpression[callee.name='setTimeout'][arguments.length!=2]:匹配setTimeout调用但参数个数不为 2 的调用表达式。
关于选择器的完整语法,可参考 AST 选择器文档,本文后续章节也会系统展开。
配置方式一:字符串形式(节点类型)
该规则接受一个字符串列表,每个字符串就是一个 AST 选择器。命中任一选择器的语法都会触发报错:
{ "rules": { "no-restricted-syntax": ["error", "FunctionExpression", "WithStatement", "BinaryExpression[operator='in']"] } }上述配置的含义是:禁用函数表达式(FunctionExpression)、with语句(WithStatement)以及使用in运算符的二元表达式(BinaryExpression[operator='in'])。
错误代码示例
以下代码分别使用了with语句、函数表达式和in运算符,均会被规则拦截:
/* eslint no-restricted-syntax: ["error", "FunctionExpression", "WithStatement", "BinaryExpression[operator='in']"] */ with (me) { dontMess(); } const doSomething = function () {}; foo in bar;正确代码示例
改用等价的替代写法后,代码可以通过检查:
/* eslint no-restricted-syntax: ["error", "FunctionExpression", "WithStatement", "BinaryExpression[operator='in']"] */ me.dontMess(); function doSomething() {}; foo instanceof bar;注意:with语句在严格模式下本身就不被允许,因此示例配置需要配合非严格模式(script 模式)才能体现出with的拦截效果。
配置方式二:对象形式(选择器 + 自定义消息)
除字符串外,规则还接受对象形式的配置项,对象包含selector(必填)和message(可选)两个属性:
{ "rules": { "no-restricted-syntax": [ "error", { "selector": "FunctionExpression", "message": "Function expressions are not allowed." }, { "selector": "CallExpression[callee.name='setTimeout'][arguments.length!=2]", "message": "setTimeout must always be invoked with two arguments." } ] } }当通过message属性指定了自定义消息后,ESLint 在报告该选择器命中的语法时会使用这条自定义消息,而不是默认消息。这一能力在需要向团队传达"为什么禁用、应该怎么写"时非常有用——报错信息可以直接承载规范说明。
字符串和对象两种格式可以在配置中自由混用,例如前两条用字符串、后两条用对象。
Schema 约束
从 lib/rules/no-restricted-syntax.js 的 schema 定义可以确认配置项的约束规则:
schema: { type: "array", items: { oneOf: [ { type: "string" }, { type: "object", properties: { selector: { type: "string" }, message: { type: "string" }, }, required: ["selector"], additionalProperties: false, }, ], }, uniqueItems: true, minItems: 0, }对应地:
- 配置必须是数组,允许为空数组(
minItems: 0); - 数组元素只能是字符串或
{ selector, message }对象; - 对象格式中
selector为必填,且不允许出现selector、message之外的额外属性; uniqueItems: true要求所有配置项互不重复。
默认消息与自定义消息的生成逻辑
在规则实现中,create(context)函数会对context.options数组做reduce处理,把每个配置项编译成一个以选择器为键的监听器:
create(context) { return context.options.reduce((result, selectorOrObject) => { const isStringFormat = typeof selectorOrObject === "string"; const hasCustomMessage = !isStringFormat && Boolean(selectorOrObject.message); const selector = isStringFormat ? selectorOrObject : selectorOrObject.selector; const message = hasCustomMessage ? selectorOrObject.message : `Using '${selector}' is not allowed.`; return Object.assign(result, { selector { context.report({ node, messageId: "restrictedSyntax", data: { message }, }); }, }); }, {}); }这段实现揭示了几个值得注意的细节:
- 选择器即监听键:返回的对象中,
[selector]作为键名,意味着 ESLint 会在 AST 遍历过程中对每一个匹配该选择器的节点调用此回调并上报问题——这正是该规则底层的工作机制。 - 默认消息模板:字符串格式(或未提供
message的对象格式)使用Using '<selector>' is not allowed.作为默认报错消息,消息中直接回显完整的选择器文本。 - 消息统一通过
messageId上报:messages中定义了restrictedSyntax: "{{message}}",自定义消息和默认消息都以数据形式注入,测试文件 tests/lib/rules/no-restricted-syntax.js 中的断言也验证了这一行为,例如:
{ code: "var foo = 41;", options: ["VariableDeclaration"], errors: [ { messageId: "restrictedSyntax", data: { message: "Using 'VariableDeclaration' is not allowed." }, }, ], }AST 选择器语法全览
要充分发挥no-restricted-syntax的威力,需要系统掌握 AST 选择器的语法。根据 AST 选择器文档,ESLint 支持的选择器语法如下:
| 语法类别 | 示例 | 说明 |
|---|---|---|
| 节点类型 | ForStatement | 匹配指定类型的节点 |
| 通配符 | * | 匹配所有节点 |
| 属性存在 | [attr] | 匹配拥有该属性的节点 |
| 属性值 | [attr="foo"]、[attr=123] | 匹配属性等于指定值的节点 |
| 属性正则 | [attr=/foo.*/] | 属性值匹配正则的节点 |
| 属性条件 | [attr!="foo"]、[attr>2]、[attr<3]、[attr>=2]、[attr<=3] | 属性值满足比较条件的节点 |
| 嵌套属性 | [attr.level2="foo"] | 匹配嵌套属性的节点 |
| 字段 | FunctionDeclaration > Identifier.id | 匹配特定字段上的节点 |
| 首/末子节点 | :first-child、:last-child | 匹配父节点的第一个/最后一个子节点 |
| 第 N 个子节点 | :nth-child(2) | 匹配第 2 个子节点(不支持ax+b形式) |
| 倒数第 N 个子节点 | :nth-last-child(1) | 匹配倒数第 1 个子节点 |
| 后代 | FunctionExpression ReturnStatement | 匹配某节点的后代节点 |
| 直接子节点 | UnaryExpression > Literal | 匹配直接子节点 |
| 后续兄弟 | VariableDeclaration ~ VariableDeclaration | 匹配后续兄弟节点 |
| 相邻兄弟 | ArrayExpression > Literal + SpreadElement | 匹配紧邻的兄弟节点 |
| 否定 | :not(ForStatement) | 匹配不满足括号内选择器的节点 |
| 匹配任意 | :matches([attr] > :first-child, :last-child)或:is(...) | 匹配括号内任一选择器命中的节点 |
| 节点类别 | :statement、:expression、:declaration、:function、:pattern | 按节点类别匹配 |
其中:function在源码实现(lib/linter/esquery.js)中会被展开为FunctionDeclaration、FunctionExpression、ArrowFunctionExpression三种节点类型的集合。
属性值中使用正则表达式
选择器的属性值支持正则表达式,例如Identifier[name=/^foo/]会匹配所有名称以foo开头的标识符。正则中如果需要包含/字符,必须转义为\/,以免被解析为正则结束符;又因为选择器本身处于 JSON 字符串中,反斜杠还需要再转义一次(\\/)。例如禁用从some/path导入:
{ "rules": { "no-restricted-syntax": [ "error", "ImportDeclaration[source.value=/^some\\/path$/]" ] } }对应的测试用例(tests/lib/rules/no-restricted-syntax.js)验证了该选择器能命中import values from 'some/path';。
实战场景:用选择器拦截具体模式
结合文档与测试用例,以下实战场景可以直接复制到你的 ESLint 配置中。
禁用不带代码块的 if 语句
不写花括号的单行if容易引发后续维护隐患,可用两种等价写法禁用:
{ "rules": { "no-restricted-syntax": [ "error", "IfStatement > :not(BlockStatement).consequent" ] } }等价写法:
{ "rules": { "no-restricted-syntax": [ "error", "IfStatement[consequent.type!='BlockStatement']" ] } }禁用 require() 调用
在推行 ESM 的代码库中,可以禁止 CommonJS 的require:
{ "rules": { "no-restricted-syntax": [ "error", "CallExpression[callee.name='require']" ] } }强制 setTimeout 必须传两个参数
{ "rules": { "no-restricted-syntax": [ "error", "CallExpression[callee.name='setTimeout'][arguments.length!=2]" ] } }禁用一个参数超过 2 个的函数声明
{ "rules": { "no-restricted-syntax": [ "error", "FunctionDeclaration[params.length>2]" ] } }禁用带标签的 break 语句
{ "rules": { "no-restricted-syntax": [ "error", "BreakStatement[label]" ] } }禁用可选链(Optional Chaining)与正则字面量
测试用例中还覆盖了如下选择器(均可在 tests/lib/rules/no-restricted-syntax.js 中查到):
ChainExpression:命中foo?.bar?.()这类可选链整体;[optional=true]:分别命中可选链中的每个可选访问/调用;Literal[regex.flags=/./]:命中带标志位(flags)的正则字面量;VariableDeclaration[kind='using']:命中using声明(ECMAScript 显式资源管理语法)。
组合选择器::is() 一次匹配多个目标
foo + bar + baz中的三个标识符可以这样一次命中:
{ "rules": { "no-restricted-syntax": [ "error", ":is(Identifier[name='foo'], Identifier[name='bar'], Identifier[name='baz'])" ] } }底层原理:esquery 与选择器解析
no-restricted-syntax之所以能"用字符串当监听器键名",依赖的是 ESLint 内置的 esquery 封装层 lib/linter/esquery.js。该模块负责:
- 解析选择器:调用 esquery 的
parse将选择器字符串解析为可执行的结构;对简单的纯字母选择器走快速路径(trySimpleParseSelector),避免不必要的解析开销;解析失败时抛出带位置的SyntaxError,方便定位配置错误。 - 缓存解析结果:通过
selectorCache(Map)缓存已解析的选择器,同一选择器在多次运行时无需重复解析。 - 计算优先级(specificity):
ESQueryParsedSelector.compare按"属性/伪类数量 → 节点类型标识符数量 → 字典序"排序监听器;当多个选择器同时命中同一节点时,监听器按优先级由低到高调用,优先级相同时按字母序调用。
这些机制保证了即使在配置大量选择器的情况下,no-restricted-syntax依然能高效、稳定地工作,同时让多个选择器命中同一节点时每个节点都会被完整报告。
多语言场景:不仅适用于 JavaScript
文档特别指出:该规则可以用于你使用 ESLint 检查的任何语言。由于选择器基于通用 AST 概念,只要相应语言的解析器产出符合 ESTree 兼容结构的节点,就能用同样的方式禁用语法:
- 使用
typescript-eslint检查 TypeScript 时,可通过其 Playground 查看 TS 代码对应的节点类型; - 使用 ESLint 检查 JavaScript、JSON、Markdown 或 CSS 时,可通过 ESLint Code Explorer 查看对应语言的 AST 节点。
这意味着团队可以在同一套no-restricted-syntax规则框架下,为不同语言的文件制定各自的语法禁令。
与相关规则的协同与区别
no-restricted-syntax的规则头(frontmatter)声明了四条相关规则(详见 no-restricted-syntax.md):
| 规则 | 职责 |
|---|---|
| no-alert | 禁用alert、confirm、prompt |
| no-console | 禁用console |
| no-debugger | 禁用debugger语句 |
| no-restricted-properties | 禁用对象上的特定属性访问 |
其中,no-console 文档 明确提到:如果不希望手动在每个console调用处添加eslint-disable-next-line注释,就可以改用no-restricted-syntax达到同样效果——例如:
{ "rules": { "no-restricted-syntax": ["error", "CallExpression[callee.object.name='console']"] } }由此可见,no-restricted-syntax是"精确到属性/调用形态"的更细粒度拦截手段,而上述专用规则更适合语义明确、使用频繁的场景。当你想禁用的是语法形态(而非某个 API 名字)时,no-restricted-syntax是唯一的选择。
何时不使用该规则
如果你不希望限制代码使用任何 JavaScript 特性或语法,就不应开启此规则。此外,若需求只是禁用console、debugger、alert等有专属规则的常见对象,优先考虑专用规则,语义更清晰、报错消息也更友好。
小结
no-restricted-syntax以极小的配置成本换来了对任意语法形态的拦截能力:字符串形式适合简单禁用节点类型,对象形式支持自定义报错消息,两种格式可混合使用;结合属性选择器、正则、:is()、:not()等丰富语法,还能精确锁定"带标签的 break""超过 N 个参数的函数声明""可选链调用"等复杂模式。其实现上基于 esquery 选择器解析与优先级机制(lib/linter/esquery.js),整套行为都有对应的单元测试佐证(tests/lib/rules/no-restricted-syntax.js),你可以在当前仓库中进一步阅读这些源码来加深理解。
【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考