Stylelint 嵌套深度限制规则max-nesting-depth完全指南深度计算、全部配置项与源码实现剖析【免费下载链接】stylelintA mighty CSS linter that helps you avoid errors and enforce conventions.项目地址: https://gitcode.com/gh_mirrors/st/stylelintStylelint 的max-nesting-depth规则用于限制 CSS 规则rule与 at-rule 的嵌套层级防止预处理器Sass、Less、PostCSS 嵌套语法等中嵌套地狱导致的代码难以阅读与维护。本文基于当前仓库的 规则文档、规则实现 与 测试用例完整讲解嵌套深度的计算规则、number主选项与四类ignore系次选项的精确行为并深入源码揭示其递归计数原理帮助你准确配置这一规则而不产生误报。规则概览为什么需要限制嵌套深度嵌套是 CSS 预处理器与原生 CSS Nesting 的核心能力但无节制地嵌套会带来三个典型问题可读性下降层级越深选择器越难一眼看懂特异性失控深层嵌套通常意味着更复杂的选择器后续覆写成本剧增维护成本上升修改中间层时影响范围难以预估。max-nesting-depth的职责就是给嵌套层数设定一条硬性上限。它检查的是规则与 at-rule 的实际嵌套深度并与之对比你配置的最大值a { b { top: 0; } } /** ↑ * This nesting */一旦深度超过配置值Stylelint 就会按规则名max-nesting-depth输出错误。从 规则实现 可以看到其默认消息模板为const ruleName max-nesting-depth; const messages ruleMessages(ruleName, { expected: (depth) Too deep nesting, maximum ${depth}, });即错误消息形如Too deep nesting, maximum 2。该规则支持 1 个 消息参数message argument即配置的最大深度值本身可在自定义消息模板中通过占位符引用。嵌套深度是如何计算的该规则的核心是嵌套深度这一概念。以下面这段代码为例每一层嵌套的深度如下a { b { /* nesting depth 1 */ .foo { /* nesting depth 2 */ media print { /* nesting depth 3 */ .baz { /* nesting depth 4 */ color: pink; } } } } }可以看到规则rule与 at-rule 都参与深度计数media print作为带块的 at-rule同样会加深一级。深度计算从根节点出发每遇到一层带块的规则或 at-rule 就加 1直到到达待检查的节点本身。根级 at-rule 不计入深度一个容易踩坑的细节根级root-levelat-rule 不参与深度计算。原因很实际——用户普遍认为根级的media、supports等是必要的、免费的不应因为包了一层媒体查询就让整棵子树超出限制。因此在以下两种写法中.foo的嵌套深度都是2只要max小于等于 2 就都能通过a { b { /* 1 */ .foo {} /* 2 */ } } media print { /* ignored */ a { b { /* 1 */ .foo {} /* 2 */ } } }这一行为在 实现 中由递归终止条件保证当父节点是根节点或父节点是父节点的父节点为根的 at-rule时直接返回当前层级if (isRoot(parent) || (isAtRule(parent) parent.parent isRoot(parent.parent))) { return level; }这正是根级 at-rule 不计深度的源码证据。主选项number规则的主选项是一个数字表示允许的最大嵌套深度。配置方式以 JSON 配置为例{ max-nesting-depth: 2 }以下写法会被判定为问题存在深度 3 的节点a { .foo { /* 1 */ __foo { /* 2 */ .bar {} /* 3 */ } } }a { media print { /* 1 */ .foo { /* 2 */ .bar {} /* 3 */ } } }以下写法不会被视为问题最大深度恰好为 2且非嵌套的扁平选择器不参与深度计算a { .foo { /* 1 */ __foo {} /* 2 */ } } a .foo__foo .bar .baz {}media print { a { .foo { /* 1 */ __foo {} /* 2 */ } } }注意最后这个例子media print位于根级按前述规则不计深度所以内部最深仍只有 2 层。从 参数校验逻辑 可见主选项仅接受数字possible: [isNumber]若传入非数字规则会报告无效选项且不执行检查。而次选项是可选的optional: true仅在提供时校验。次选项ignoreignore是一个字符串数组目前支持两个取值blockless-at-rules与pseudo-classes。{ ignore: [array, of, options] }ignore: [blockless-at-rules]忽略**只有包装作用、自身没有声明块declaration block**的 at-rule。也就是说如果某个 at-rule 的块内只有其他规则、没有任何声明property: value那么它本身不占用深度。给定配置{ max-nesting-depth: [1, { ignore: [blockless-at-rules] }] }以下写法会被判定为问题因为这些 at-rule 自己带有声明块不属于blocklessa { :hover { /* 1 */ media (min-width: 500px) { color: pink; } /* 2 */ } }a { nest b { /* 1 */ .foo { color: pink; } /* 2 */ } }以下写法不会被判定为问题因为其中的.foo嵌套深度都只有 1a { .foo { color: pink; } /* 1 */ }media print { /* ignored regardless of options */ a { .foo { color: pink; } /* 1 */ } }a { media print { /* ignored because its an at-rule without a declaration block of its own */ .foo { color: pink; } /* 1 */ } }该行为的实现位于 嵌套深度递归函数当开启该选项、当前节点是 at-rule、且其所有子节点都不是声明时递归时不增加深度直接继续向上统计if ( (optionsMatches(secondaryOptions, ignore, blockless-at-rules) isAtRule(node) node.every((child) !isDeclaration(child))) || ... ) { return nestingDepth(parent, level); }对应的 测试用例 验证了a { media print { b { top: 0; }}}在max: 1下通过而a { media print { b { c { top: 0; }}}}因为真实嵌套超出而被拒绝。ignore: [pseudo-classes]忽略选择器列表中每一项的首个选择器为伪类的规则。其意图是常见的:hover、:focus这类状态嵌套通常不代表真实的层级加深可以不计深度。给定配置{ max-nesting-depth: [1, { ignore: [pseudo-classes] }] }以下写法会被判定为问题a { b { /* 1 */ .c { /* 2 */ top: 0; } } }a { :hover { /* ignored */ b { /* 1 */ .c { /* 2 */ top: 0; } } } }注意第二个例子:hover自身被忽略不计但它内部的b → .c仍然形成了深度 2因此依然违规——忽略只影响该节点自身是否占用深度不影响其子树的真实层数。a { b { /* 1 */ ::selection { /* 2 */ color: #64FFDA; } } }第三个例子说明伪元素如::selection不属于伪类因此::selection不会被忽略深度照算。a { b { /* 1 */ :hover, .c { /* 2 */ top: 0; } } }第四个例子说明只要选择器列表中任一项的首选不是伪类整条规则就不算纯伪类规则深度照算。以下写法不会被判定为问题因为这些伪类规则的深度都只算 1a { b { /* 1 */ :hover { /* ignored */ top: 0; } } }a { b { /* 1 */ :nest { :nest-lvl2 { /* ignored */ top: 0; } } } }a { :hover { /* ignored */ b { /* 1 */ top: 0; } } }a { :nest { /* ignored */ :nest-lvl2 { /* ignored */ top: 0; b { /* 1 */ bottom: 0; } } } }a { b { /* 1 */ :hover, :focus { /* ignored */ top: 0; } } }最后这个例子很关键一条规则的所有选择器项都是伪类时整条规则整体被忽略。实现的判定逻辑是containsPseudoClassesOnly源码先通过postcss-selector-parser规范化选择器并按逗号拆分剔除命中ignoreRules的项后要求剩余每一项都能被extractPseudoRule提取为伪类。而 extractPseudoRule 的实现很精妙function extractPseudoRule(selector) { return selector.startsWith(:) selector[2] ! : ? selector.slice(2) : undefined; }它要求选择器以:开头、且第三个字符不是:以此排除::selection这类伪元素然后截取:之后的部分作为伪类名。测试用例 index.mjs 明确覆盖了::selection被拒绝、:hover, c混合被拒绝、连续:hover { :focus { :otherone {...} } }等场景。次选项ignoreAtRules{ ignoreAtRules: [array, of, at-rules, /regex/] }忽略指定的 at-rule支持字符串精确匹配与/正则/形式。被忽略的 at-rule 既不作为检查对象其内部子树也不因它而加深深度——注意这与ignore: [blockless-at-rules]只免去一层不同。给定配置{ max-nesting-depth: [1, { ignoreAtRules: [/^--my-/, media] }] }以下写法不会被判定为问题media与以--my-开头的 at-rule 被整体忽略内部嵌套无论多深都不计其贡献a { media print { /* 1 */ b { /* 2 */ c { top: 0; } /* 3 */ } } }a { b { /* 1 */ media print { /* 2 */ c { top: 0; } /* 3 */ } } }a { --my-at-rule print { /* 1 */ b { /* 2 */ c { top: 0; } /* 3 */ } } }a { --my-other-at-rule print { /* 1 */ b { /* 2 */ c { top: 0; } /* 3 */ } } }以下写法会被判定为问题不在忽略名单内的 at-rule 正常计深度a { import print { /* 1 */ b { top: 0; } /* 2 */ } }a { --not-my-at-rule print { /* 1 */ b { top: 0; } /* 2 */ } }注意正则/^--my-/只匹配以--my-开头的名称--not-my-at-rule不命中。该行为的判定在 isIgnoreAtRule 与递归终止条件中共同实现const isIgnoreAtRule (node) isAtRule(node) optionsMatches(secondaryOptions, ignoreAtRules, node.name);当某节点的父节点是被忽略的 at-rule 时递归直接返回 0源码 L108-L110这正是被忽略 at-rule 的子树整体不再加深的原因。测试 index.mjs L220-L271 还覆盖了ignoreAtRules传入纯正则如/^my-/的用法。次选项ignorePseudoClasses{ ignorePseudoClasses: [array, of, pseudo-classes, /regex/] }忽略指定的伪类支持字符串与/正则/。与ignore: [pseudo-classes]的全有或全无不同这里可以精确挑选要放行的伪类例如只放行hover与所有focus-*。给定配置{ max-nesting-depth: [1, { ignorePseudoClasses: [hover, ^focus-] }] }以下写法不会被判定为问题a { :hover { /* ignored */ b { /* 1 */ top: 0; } } }a { :hover, :active { /* ignored */ b { /* 1 */ top: 0; } } }注意第二个例子:hover, :active两条选择器项的伪类都在忽略名单内hover精确命中、active不命中但被hover项……等等——实际上这里的关键是hover在名单内而active不在。为什么仍被忽略因为判定条件是containsIgnoredPseudoClassesOrRulesOnly规则的所有选择器项都必须命中忽略名单或命中 ignoreRules。这里active并不在名单里那么为什么通过回顾 测试用例 L196-L199{ code: a { :hover, :--custom-pseudo { b { top: 0; } } }, },以及 reject 用例a { :hover, :visited { b { top: 0; } } }README 中的 problems 示例。综合来看README 中:hover, :active被忽略的示例与源码测试存在出入——以源码测试为准只有整条规则的所有选择器项都命中忽略名单或 ignoreRules时才会被忽略:hover, :active中active不在名单内应当照常计深度。以下写法会被判定为问题a { :visited { /* 1 */ b { /* 2 */ top: 0; } } }a { :hover, :visited { /* 1 */ b { /* 2 */ top: 0; } } }第二个例子的违规原因正是visited不在忽略名单内导致整条规则不被忽略。实现细节在 containsIgnoredPseudoClassesOrRulesOnly依次检查每个选择器项若命中ignoreRules视为通过否则提取其伪类名若提取失败不是伪类或伪类名不在ignorePseudoClasses名单内则整条规则照常计深度。次选项ignoreRules{ ignoreRules: [array, of, selectors, /regex/] }忽略选择器匹配指定规则支持字符串精确匹配与/正则/的规则节点。匹配对象是规则的完整选择器字符串。给定配置{ max-nesting-depth: [ 1, { ignoreRules: [.my-selector, /^.ignored-sel/] } ] }以下写法不会被判定为问题a { .my-selector { /* ignored */ b { /* 1 */ top: 0; } } }a { .my-selector, .ignored-selector { /* ignored */ b { /* 1 */ top: 0; } } }第二个例子说明当一条规则的所有选择器项都命中忽略名单时整条规则被忽略对应containsIgnoredPseudoClassesOrRulesOnly中的ignoreRules分支。以下写法会被判定为问题a { .not-ignored-selector { /* 1 */ b { /* 2 */ top: 0; } } }a { .my-selector, .not-ignored-selector { /* 1 */ b { /* 2 */ top: 0; } } }第二个例子说明只要存在一个未命中名单的选择器项整条规则就照常计深度。测试 index.mjs L273-L360 对忽略选择器位于树中间混合选择器等边界情况有非常细致的覆盖例如a { b { .my-selector c { top: 0; }}}会被拒绝因为选择器字符串.my-selector c不等于也不匹配.my-selector。组合使用与消息自定义四个次选项可以任意组合。例如 测试用例 L413-L429 同时启用ignoreRules与ignorePseudoClasses{ max-nesting-depth: [ 1, { ignoreRules: [/^.some-sel/, .my-selector], ignorePseudoClasses: [hover, /^--custom-.*$/] } ] }此时a { :--custom-pseudo, .my-selector { b { top: 0; } } }可以通过伪类命中ignorePseudoClasses、选择器命中ignoreRules。此外规则支持 1 个消息参数message argument配置的最大深度值。若你想自定义错误文案例如{ rules: { max-nesting-depth: [2, { message: 嵌套深度不得超过 {{ expected }} 层实际超出请重构选择器 }] } }占位符{{ expected }}会替换为配置的最大深度。需要说明的是自定义消息的具体占位符语法以 配置文档 中的message约定为准。源码实现原理递归计数与匹配机制规则的主流程非常简洁全部集中在 index.mjs 约 190 行代码中可分为三层1. 遍历与前置过滤checkStatement。注册时通过root.walkRules(checkStatement)与root.walkAtRules(checkStatement)源码 L59-L60同时遍历规则与 at-rule。每个节点依次经过四道过滤命中ignoreAtRules的 at-rule跳过命中ignoreRules的规则跳过isIgnoreRule没有块hasBlock即statement.nodes ! undefined见 hasBlock的节点跳过——这也是a { b { include foo; } }这类无块 mixin 不报错的原因测试 L24-L26非标准语法的规则!isStandardSyntaxRule跳过用于规避 Sass 嵌套属性等预处理器特例。2. 递归深度计算nestingDepth。对每个通过过滤的节点从 0 开始向上递归父节点为根或父节点是其父为根的 at-rule返回当前层级根级 at-rule 免费父节点为被忽略的 at-rule返回 0该子树整体不加深当前节点满足blockless-at-rules/pseudo-classes/ 混合忽略条件nestingDepth(parent, level)——不加深继续向上其余情况nestingDepth(parent, level 1)——加深一级继续向上。3. 匹配机制optionsMatches。所有ignore*选项的字符串/正则匹配统一走 optionsMatches最终落到 matchesStringOrRegExp任何以/开头并以/或/i结尾的字符串会被当作正则表达式解析其余字符串做严格相等比较。这就是配置文件中/^--my-/、/^.ignored-sel/这类写法能生效的根本原因。最后若depth primary则调用report输出Too deep nesting, maximum ${depth}其中depth为配置值见 messages.expected错误定位到违规节点本身。测试验证与生态定位该规则拥有完善的测试覆盖lib/rules/max-nesting-depth/tests/index.mjs共 457 行除上述各类accept/reject组合外还包括两个值得注意的预处理器用例使用customSyntax: postcss-scss验证a { media print { b { top: 0; }}}通过blockless-at-rules 生效以及.foo { .bar { margin: { bottom: 0; } } }这类 SCSS 嵌套属性不会误报L431-L442使用customSyntax: postcss-sass验证无块的media print声明不被误报L444-L457。在 Stylelint 生态中本规则将现已废弃的第三方插件stylelint-statement-max-nesting-depth的功能整合进了核心见 规则文档并已在 规则注册表 中登记无需额外安装插件即可直接使用。小结max-nesting-depth是一把双刃剑设置过小容易频繁误伤合理的媒体查询包装设置过大则失去约束意义。掌握其三个关键事实就能配置得心应手根级 at-rule 永远免费不会计入任何节点的深度带块的 at-rule 与规则一样占用深度除非被ignore/ignoreAtRules明确豁免忽略选项只决定该节点是否占一级不会抹去其子树的真实深度——混合选择器只要有一项未命中名单整条规则照常计深度。建议在项目中先以[2, { ignore: [blockless-at-rules, pseudo-classes], ignoreAtRules: [media, supports] }]起步再根据实际告警逐步收紧或精确豁免配合上述源码级理解即可让规则既严格又精准。【免费下载链接】stylelintA mighty CSS linter that helps you avoid errors and enforce conventions.项目地址: https://gitcode.com/gh_mirrors/st/stylelint创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
