ESLint no-restricted-syntax 规则详解:用 AST 选择器精准禁用任意语法
2026/9/19 7:31:03 网站建设 项目流程

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-catchclasswithin运算符等),从而避免为每一个想禁用的特性单独编写规则。读完本文,你将掌握该规则的字符串/对象两种配置格式、AST 选择器的完整语法、常见实战拦截场景,以及其底层基于 esquery 的实现原理,可直接在自己的 ESLint 配置中投入使用。

规则定位:为什么需要一条"通用禁用"规则

JavaScript 语言特性众多,不同团队对语言特性的偏好差异巨大。有些项目会明确禁止某些语法结构的出现——例如禁用try-catch、禁用class、禁用in运算符,甚至禁用未加花括号的if语句体。

如果为每个想禁用的特性单独创建一条 ESLint 规则,规则数量将难以维护。no-restricted-syntax的设计目标正是用一条规则覆盖所有"禁用语法"诉求:你只需把要禁用的语法元素以AST 选择器的形式配置进去即可。

在 JavaScript 语境下,每个语法元素都对应一个 ESTree 节点类型,例如函数声明对应FunctionDeclarationwith语句对应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:匹配直接父节点为VariableDeclaratorIdentifier
  • 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为必填,且不允许出现selectormessage之外的额外属性;
  • 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 }, }); }, }); }, {}); }

这段实现揭示了几个值得注意的细节:

  1. 选择器即监听键:返回的对象中,[selector]作为键名,意味着 ESLint 会在 AST 遍历过程中对每一个匹配该选择器的节点调用此回调并上报问题——这正是该规则底层的工作机制。
  2. 默认消息模板:字符串格式(或未提供message的对象格式)使用Using '<selector>' is not allowed.作为默认报错消息,消息中直接回显完整的选择器文本。
  3. 消息统一通过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)中会被展开为FunctionDeclarationFunctionExpressionArrowFunctionExpression三种节点类型的集合。

属性值中使用正则表达式

选择器的属性值支持正则表达式,例如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,方便定位配置错误。
  • 缓存解析结果:通过selectorCacheMap)缓存已解析的选择器,同一选择器在多次运行时无需重复解析。
  • 计算优先级(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禁用alertconfirmprompt
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 特性或语法,就不应开启此规则。此外,若需求只是禁用consoledebuggeralert等有专属规则的常见对象,优先考虑专用规则,语义更清晰、报错消息也更友好。

小结

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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询