Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add mixins-no-risky-parent-selectors rule.
- Loading branch information
1 parent
26c105c
commit e30d734
Showing
4 changed files
with
343 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
# mixins-no-risky-parent-selectors | ||
|
||
|
||
Parent selectors are not reliable in the following contexts: | ||
|
||
- Within a @mixin rule. | ||
- Nested within another style rule. | ||
- When they are not positioned at the beginning of a complex selector. | ||
- Within a @at-root rule. | ||
|
||
Using parent selectors in these scenarios may lead to unexpected and uncommon behavior. | ||
|
||
```scss | ||
@mixin foo { | ||
.bar { | ||
color: blue; | ||
.baz & { | ||
/* ↑ | ||
* Disallow this */ | ||
color: red; | ||
} | ||
} | ||
} | ||
``` | ||
|
||
A more deterministic approach would be: | ||
```scss | ||
@mixin foo { | ||
.bar { | ||
color: blue; | ||
} | ||
.baz .bar { | ||
color: red; | ||
} | ||
} | ||
``` | ||
|
||
## Options | ||
|
||
### `true` | ||
|
||
The following patterns are considered warnings: | ||
|
||
```scss | ||
@mixin foo { | ||
& .baz { | ||
color: red; | ||
} | ||
} | ||
``` | ||
|
||
```scss | ||
.bar { | ||
.bar { | ||
color: blue; | ||
& .baz { | ||
color: red; | ||
} | ||
} | ||
} | ||
``` | ||
|
||
```scss | ||
.bar { | ||
color: blue; | ||
.baz & { | ||
color: red; | ||
} | ||
} | ||
``` | ||
|
||
```scss | ||
.bar { | ||
color: blue; | ||
.baz & { | ||
color: red; | ||
} | ||
} | ||
``` | ||
|
||
```scss | ||
.foo { | ||
color: blue; | ||
|
||
& .bar, .baz & .qux { | ||
color: red; | ||
} | ||
} | ||
``` | ||
|
||
```scss | ||
@mixin foo { | ||
color: blue; | ||
@at-root .b { | ||
& { | ||
color: red; | ||
} | ||
} | ||
} | ||
``` | ||
|
||
The following patterns are _not_ considered warnings: | ||
```scss | ||
.foo { | ||
color: blue; | ||
|
||
& .bar { | ||
color: red; | ||
} | ||
} | ||
``` | ||
|
||
```scss | ||
.foo { | ||
color: blue; | ||
|
||
& .bar, & .baz .qux { | ||
color: red; | ||
} | ||
} | ||
``` | ||
|
||
```scss | ||
.foo { | ||
color: blue; | ||
|
||
& .bar { | ||
.baz { | ||
color: red; | ||
} | ||
} | ||
} | ||
``` |
139 changes: 139 additions & 0 deletions
139
src/rules/mixins-no-risky-parent-selectors/__tests__/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
"use strict"; | ||
|
||
const { ruleName } = require(".."); | ||
|
||
testRule({ | ||
ruleName, | ||
config: [true], | ||
customSyntax: "postcss-scss", | ||
|
||
accept: [ | ||
{ | ||
code: ` | ||
.parent { | ||
color: blue; | ||
& .b { | ||
color: red; | ||
} | ||
} | ||
`, | ||
description: "Nested parent selector" | ||
}, | ||
{ | ||
code: ` | ||
.parent { | ||
color: blue; | ||
& .b, &.context { | ||
color: red; | ||
} | ||
} | ||
`, | ||
description: "Nested parent selector in complex selector" | ||
}, | ||
{ | ||
code: ` | ||
.bar { | ||
& .parent { | ||
color: blue; | ||
.context { | ||
color: red; | ||
} | ||
} | ||
} | ||
`, | ||
description: "Parent selector nested in another style rule" | ||
} | ||
], | ||
|
||
reject: [ | ||
{ | ||
code: ` | ||
@mixin foo { | ||
&.context { | ||
color: red; | ||
} | ||
} | ||
`, | ||
description: "Parent selector in mixin", | ||
message: | ||
"Unexpected parent selector in irregular context. (scss/mixins-no-risky-parent-selectors)" | ||
}, | ||
{ | ||
code: ` | ||
.bar { | ||
.parent { | ||
color: blue; | ||
& .context { | ||
color: red; | ||
} | ||
} | ||
} | ||
`, | ||
description: "Parent selector nested in more than one style rule", | ||
message: | ||
"Unexpected parent selector in irregular context. (scss/mixins-no-risky-parent-selectors)" | ||
}, | ||
{ | ||
code: ` | ||
.parent { | ||
color: blue; | ||
.context & { | ||
color: red; | ||
} | ||
} | ||
`, | ||
description: "Selector ending in parent selector", | ||
message: | ||
"Unexpected parent selector in irregular context. (scss/mixins-no-risky-parent-selectors)" | ||
}, | ||
{ | ||
code: ` | ||
.parent { | ||
color: blue; | ||
.context & .b { | ||
color: red; | ||
} | ||
} | ||
`, | ||
description: "Parent selector in the middle of complex selector", | ||
message: | ||
"Unexpected parent selector in irregular context. (scss/mixins-no-risky-parent-selectors)" | ||
}, | ||
{ | ||
code: ` | ||
.parent { | ||
color: blue; | ||
& .b, .context & { | ||
color: red; | ||
} | ||
} | ||
`, | ||
description: "Complex selector, one ending in parent selector", | ||
message: | ||
"Unexpected parent selector in irregular context. (scss/mixins-no-risky-parent-selectors)" | ||
}, | ||
{ | ||
code: ` | ||
@mixin foo { | ||
color: blue; | ||
@at-root .b { | ||
& { | ||
color: red; | ||
} | ||
} | ||
} | ||
`, | ||
description: "Selector within @at-root", | ||
message: | ||
"Unexpected parent selector in irregular context. (scss/mixins-no-risky-parent-selectors)" | ||
} | ||
] | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
"use strict"; | ||
|
||
const { utils } = require("stylelint"); | ||
const namespace = require("../../utils/namespace"); | ||
const ruleUrl = require("../../utils/ruleUrl"); | ||
|
||
const ruleName = namespace("mixins-no-risky-parent-selectors"); | ||
|
||
const messages = utils.ruleMessages(ruleName, { | ||
rejected: `Unexpected parent selector in irregular context.` | ||
}); | ||
|
||
const meta = { | ||
url: ruleUrl(ruleName) | ||
}; | ||
|
||
function isWithinMixinOrAtRoot(node) { | ||
let parent = node.parent; | ||
while (parent) { | ||
if ( | ||
parent.type === "atrule" && | ||
(parent.name === "mixin" || parent.name === "at-root") | ||
) { | ||
return true; | ||
} | ||
parent = parent.parent; | ||
} | ||
return false; | ||
} | ||
|
||
function hasNestedParentSelector(selectors) { | ||
return selectors | ||
.split(",") | ||
.some( | ||
selector => | ||
selector.includes("&") && !selector.replace(" ", "").startsWith("&") | ||
); | ||
} | ||
|
||
function rule(actual) { | ||
return (root, result) => { | ||
const validOptions = utils.validateOptions(result, ruleName, { actual }); | ||
|
||
if (!validOptions) { | ||
return; | ||
} | ||
|
||
root.walkRules(node => { | ||
if ( | ||
node.selector?.includes("&") && | ||
(isWithinMixinOrAtRoot(node) || | ||
hasNestedParentSelector(node.selector) || | ||
node.parent.parent.selector) | ||
) { | ||
utils.report({ | ||
message: messages.rejected, | ||
node, | ||
result, | ||
ruleName | ||
}); | ||
} | ||
}); | ||
}; | ||
} | ||
|
||
rule.ruleName = ruleName; | ||
rule.messages = messages; | ||
rule.meta = meta; | ||
|
||
module.exports = rule; |