From 19fba7249d65b19335021a90a33629631f0c67c1 Mon Sep 17 00:00:00 2001 From: Pamela Lozano Date: Mon, 21 Aug 2023 20:43:18 -0600 Subject: [PATCH] Add at-root-no-redundant rule --- CHANGELOG.md | 6 + src/rules/at-root-no-redundant/README.md | 67 +++++++++ .../at-root-no-redundant/__tests__/index.js | 136 ++++++++++++++++++ src/rules/at-root-no-redundant/index.js | 59 ++++++++ src/rules/index.js | 1 + 5 files changed, 269 insertions(+) create mode 100644 src/rules/at-root-no-redundant/README.md create mode 100644 src/rules/at-root-no-redundant/__tests__/index.js create mode 100644 src/rules/at-root-no-redundant/index.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e07e624b..85e57e0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 5.1.0 +- Added: `at-root-no-redundant` rule to ban unnecessary `@at-root` rule (#601). + +**Full Changelog**: https://github.com/stylelint-scss/stylelint-scss/compare/v5.1.0...v5.2.0 + +# 5.1.0 + - Added: `function-disallowed-list` rule support to ban specific built-in functions (#422, #844). **Full Changelog**: https://github.com/stylelint-scss/stylelint-scss/compare/v5.0.1...v5.1.0 diff --git a/src/rules/at-root-no-redundant/README.md b/src/rules/at-root-no-redundant/README.md new file mode 100644 index 00000000..6bda9b06 --- /dev/null +++ b/src/rules/at-root-no-redundant/README.md @@ -0,0 +1,67 @@ +# at-root-no-redundant + +Disallow redundant `@at-root` rule. + +The `@at-root` rule is redundant in the following cases: + +- If `@at-root` is already at the root of the document. +- If any `@at-root` selector contains the parent selector, [`&`](https://sass-lang.com/documentation/style-rules/parent-selector/), outside interpolation. +- If `@at-root` is nested within a `@keyframes` block. + +## Options + +### `true` + +The following patterns are considered warnings: + + +```scss +@at-root .a { margin: 3px; } +``` + + +```scss +.a { @at-root .b & { margin: 3px; } } +``` + + +```scss +@keyframes slidein { + @at-root from { + transform: translateX(0%); + } + + to { + transform: translateX(100%); + } +} +``` + +The following patterns are _not_ considered warnings: + + +```scss +.a { @at-root .b { margin: 3px; } } +``` + + +```scss +.a { @at-root .b#{&} { margin: 3px; } } +``` + + +```scss +.a { + @at-root .b { + @keyframes slidein { + from { + transform: translateX(0%); + } + + to { + transform: translateX(100%); + } + } + } +} +``` diff --git a/src/rules/at-root-no-redundant/__tests__/index.js b/src/rules/at-root-no-redundant/__tests__/index.js new file mode 100644 index 00000000..0985db3e --- /dev/null +++ b/src/rules/at-root-no-redundant/__tests__/index.js @@ -0,0 +1,136 @@ +"use strict"; + +const { messages, ruleName } = require(".."); + +testRule({ + ruleName, + config: [true], + customSyntax: "postcss-scss", + + accept: [ + { + code: ` + .a { @at-root .b { c: d } } + `, + description: "@at-root rule is nested." + }, + { + code: ` + .a { @at-root .b#{&} { c: d; } } + `, + description: + "@at-root is followed by a selector with an interpolated `&`." + }, + { + code: ` + .a { @at-root .b { .c & { d: e; } } } + `, + line: 2, + column: 12, + message: messages.rejected, + description: "A parent selector (&) is used under the @at-root rule." + }, + { + code: ` + .a { + @at-root .b { + @keyframes slidein { + from { + transform: translateX(0%); + } + + to { + transform: translateX(100%); + } + } + } + }`, + description: "@at-root outside a keyframes block, nested." + }, + { + code: ` + @mixin foo {} + + @keyframes slidein { + from { + @include foo; + transform: translateX(0%); + } + + to { + transform: translateX(100%); + } + }`, + description: "Other at-rules nested inside @keyframes block." + } + ], + reject: [ + { + code: ` + @at-root .a { b: c; } + `, + line: 2, + column: 7, + message: messages.rejected, + description: "@at-root rule is already in the root." + }, + { + code: ` + .a { @at-root .b & { c: d; } } + `, + line: 2, + column: 12, + message: messages.rejected, + description: + "@at-root is followed by a selector with a `&` outside an interpolation." + }, + { + code: ` + .a { @at-root .b & #{.c} { d: e; } } + `, + line: 2, + column: 12, + message: messages.rejected, + description: + "@at-root is followed by a selector with a `&` outside an interpolation." + }, + { + code: ` + @keyframes slidein { + @at-root from { + transform: translateX(0%); + } + + to { + transform: translateX(100%); + } + }`, + line: 3, + column: 9, + message: messages.rejected, + description: "@at-root inside a keyframes block, top level." + }, + { + code: ` + @keyframes slidein { + from { + transform: translateX(0%); + @at-root from { + transform: translateX(100%); + } + to { + transform: translateX(0%); + } + } + + to { + transform: translateX(100%); + } + }`, + line: 5, + column: 14, + message: messages.rejected, + description: "@at-root inside a @keyframes block, nested." + } + ] +}); diff --git a/src/rules/at-root-no-redundant/index.js b/src/rules/at-root-no-redundant/index.js new file mode 100644 index 00000000..86a92327 --- /dev/null +++ b/src/rules/at-root-no-redundant/index.js @@ -0,0 +1,59 @@ +"use strict"; + +const { utils } = require("stylelint"); +const namespace = require("../../utils/namespace"); +const ruleUrl = require("../../utils/ruleUrl"); + +const ruleName = namespace("at-root-no-redundant"); + +function isWithinKeyframes(node) { + let parent = node.parent; + while (parent) { + if (parent.type === "atrule" && parent.name === "keyframes") { + return true; + } + parent = parent.parent; + } + return false; +} + +const messages = utils.ruleMessages(ruleName, { + rejected: "Unexpected @at-root rule." +}); + +const meta = { + url: ruleUrl(ruleName) +}; + +function rule(actual) { + return (root, result) => { + const validOptions = utils.validateOptions(result, ruleName, { + actual + }); + + if (!validOptions) { + return; + } + + root.walkAtRules("at-root", node => { + if ( + node.parent.type === "root" || + node.params.replace(/#{.*}/g, "").includes("&") || + isWithinKeyframes(node) + ) { + utils.report({ + message: messages.rejected, + node, + result, + ruleName + }); + } + }); + }; +} + +rule.ruleName = ruleName; +rule.messages = messages; +rule.meta = meta; + +module.exports = rule; diff --git a/src/rules/index.js b/src/rules/index.js index fcdbe995..f02c8964 100644 --- a/src/rules/index.js +++ b/src/rules/index.js @@ -22,6 +22,7 @@ const rules = { "at-mixin-parentheses-space-before": require("./at-mixin-parentheses-space-before"), "at-mixin-pattern": require("./at-mixin-pattern"), "at-rule-conditional-no-parentheses": require("./at-rule-conditional-no-parentheses"), + "at-root-no-redundant": require("./at-root-no-redundant"), "at-rule-no-unknown": require("./at-rule-no-unknown"), "at-use-no-unnamespaced": require("./at-use-no-unnamespaced"), "comment-no-empty": require("./comment-no-empty"),