Skip to content

Commit

Permalink
Add at-root-no-redundant-selector rule
Browse files Browse the repository at this point in the history
  • Loading branch information
pamelalozano16 committed Aug 22, 2023
1 parent 9aa4449 commit a7c6ff6
Show file tree
Hide file tree
Showing 5 changed files with 277 additions and 0 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,11 @@
# 5.1.0

- Added: `at-root-no-redundant-selector` rule to remove 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
Expand Down
67 changes: 67 additions & 0 deletions src/rules/at-root-no-redundant-selector/README.md
@@ -0,0 +1,67 @@
# at-root-no-redundant-selector

Suggest deletion of unnecessary @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 @at-root is followed by any selector and a & outside interpolation.
- If @at-root is nested within a @keyframes block.

## Options

### `true`

The following patterns are considered warnings:

<!-- prettier-ignore -->
```scss
@at-root .a { margin: 3px; }
```

<!-- prettier-ignore -->
```scss
.a { @at-root .b & : { margin: 3px; } }
```

<!-- prettier-ignore -->
```scss
@keyframes slidein {
@at-root from {
transform: translateX(0%);
}

to {
transform: translateX(100%);
}
}
```

The following patterns are _not_ considered warnings:

<!-- prettier-ignore -->
```scss
.a { @at-root .b { margin: 3px; } }
```

<!-- prettier-ignore -->
```scss
.a { @at-root .b#{&}: { margin: 3px; } }
```

<!-- prettier-ignore -->
```scss
.a {
@at-root .b {
@keyframes slidein {
from {
transform: translateX(0%);
}

to {
transform: translateX(100%);
}
}
}
}
```
129 changes: 129 additions & 0 deletions src/rules/at-root-no-redundant-selector/__tests__/index.js
@@ -0,0 +1,129 @@
"use strict";

const { messages, ruleName } = require("..");

testRule({
ruleName,
config: [true],
customSyntax: "postcss-scss",

accept: [
{
code: `
.a { @at-root .b: c; }
`,
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; } }
`,
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: `
.a {
margin: 3px;
}
@keyframes slidein {
from {
@extend .a;
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: `
@keyframes slidein {
@at-root from {
transform: translateX(0%);
}
to {
transform: translateX(100%);
}
}`,
line: 2,
column: 7,
message: messages.rejected,
description: "@at-root inside a keyframes block, top level."
},
{
code: `
@keyframes slidein {
from {
transform: translateX(0%);
.a {
.b {
.c {
@at-root d: e;
}
}
}
}
to {
transform: translateX(100%);
}
}
`,
line: 2,
column: 7,
message: messages.rejected,
description: "@at-root inside a @keyframes block, nested."
}
]
});
74 changes: 74 additions & 0 deletions src/rules/at-root-no-redundant-selector/index.js
@@ -0,0 +1,74 @@
"use strict";

const { utils } = require("stylelint");
const namespace = require("../../utils/namespace");
const ruleUrl = require("../../utils/ruleUrl");

const ruleName = namespace("at-root-no-redundant-selector");

function isInterpolated(string) {
const interpolationRegex = /#{(.*?)}/g;
return interpolationRegex.test(string);
}

function isAtRootRuleNested(node) {
// Recursive method to check if any descendant is an @at-root rule.
let isAtRootNested = false;
if (node.nodes !== undefined) {
for (const i in node.nodes) {
isAtRootNested = isAtRootNested || isAtRootRuleNested(node.nodes[i]);
if (isAtRootNested) break;
}
}
return isAtRootNested || (node.type === "atrule" && node.name === "at-root");
}

const messages = utils.ruleMessages(ruleName, {
rejected: "Unnecessary @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(decl => {
if (decl.type !== "atrule") return;
if (
decl.name === "at-root" &&
(decl.parent.type === "root" ||
(decl.params.includes("&") && !isInterpolated(decl.params)))
) {
utils.report({
message: messages.rejected,
node: decl,
result,
ruleName
});
}
if (decl.name === "keyframes" && isAtRootRuleNested(decl)) {
utils.report({
message: messages.rejected,
node: decl,
result,
ruleName
});
}
});
};
}

rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;

module.exports = rule;
1 change: 1 addition & 0 deletions src/rules/index.js
Expand Up @@ -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-selector": require("./at-root-no-redundant-selector"),
"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"),
Expand Down

0 comments on commit a7c6ff6

Please sign in to comment.