Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add at-root-no-redundant rule #846

Merged
merged 1 commit into from Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions 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
Expand Down
75 changes: 75 additions & 0 deletions src/rules/at-root-no-redundant/README.md
@@ -0,0 +1,75 @@
# at-root-no-redundant

Disallow redundant `@at-root` rule.

<!-- prettier-ignore -->
```scss
@at-root .a { margin: 3px; }
/** ↑
* This rule is unnecessary
*/
```

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:

<!-- 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%);
}
}
}
}
```
136 changes: 136 additions & 0 deletions 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."
}
]
});
59 changes: 59 additions & 0 deletions 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;
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": 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"),
Expand Down