Skip to content

Commit

Permalink
Add mixin-no-risky-parent-selectors rule.
Browse files Browse the repository at this point in the history
  • Loading branch information
pamelalozano16 committed Apr 2, 2024
1 parent 26c105c commit c3ee96c
Show file tree
Hide file tree
Showing 5 changed files with 367 additions and 2 deletions.
Expand Up @@ -83,8 +83,7 @@ testRule({
line: 2,
column: 20,
message: messages.rejected("scssy"),
description:
"One file, ext not from an allowed list, space at the end."
description: "One file, ext not from an allowed list, space at the end."
},
{
code: `
Expand Down
1 change: 1 addition & 0 deletions src/rules/index.js
Expand Up @@ -56,6 +56,7 @@ const rules = {
"function-unquote-no-unquoted-strings-inside": require("./function-unquote-no-unquoted-strings-inside"),
"map-keys-quotes": require("./map-keys-quotes"),
"media-feature-value-dollar-variable": require("./media-feature-value-dollar-variable"),
"mixin-no-risky-parent-selectors": require("./mixin-no-risky-parent-selectors"),
"no-dollar-variables": require("./no-dollar-variables"),
"no-duplicate-dollar-variables": require("./no-duplicate-dollar-variables"),
"no-duplicate-mixins": require("./no-duplicate-mixins"),
Expand Down
139 changes: 139 additions & 0 deletions src/rules/mixin-no-risky-parent-selectors/README.md
@@ -0,0 +1,139 @@
# mixin-no-risky-parent-selectors

If a mixin contains a parent selector within another style rule, and is used in a nested context,
the output selector may include the outermost parent selector in an unexpected way.

This example:
```scss
@mixin foo {
.a {
color: blue;
.b & {
color: red;
}
}
}

.c {
@include foo;
}
```

Outputs:
```scss
.c .a {
color: blue;
}
.b .c .a {
color: red;
}
```

However, if we pull the parent selector into the child and make the child style rule a sibling:
```scss
@mixin foo {
.a {
color: blue;
}
.b .a {
color: red;
}
}

.c {
@include foo;
}
```

Outputs:
```scss
.c .a {
color: blue;
}
.c .b .a {
color: red;
}
```

This occurs when a parent selector meets all of the following conditions:
- Is within a `@mixin` rule.
- Is nested within another style rule.
- Is not positioned at the beginning of a complex selector.

## Options

### `true`

The following patterns are considered warnings:

```scss
@mixin foo {
.bar {
color: blue;
.baz & {
color: red;
}
}
}
```

```scss
@mixin foo {
.bar {
color: blue;
.qux, .baz & .quux{
color: red;
}
}
}
```

The following patterns are _not_ considered warnings:

```scss
.foo {
.bar {
color: blue;
.baz & {
color: red;
}
}
}
```

```scss
@mixin foo {
.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 {
& .baz {
color: red;
}
}
```
159 changes: 159 additions & 0 deletions src/rules/mixin-no-risky-parent-selectors/__tests__/index.js
@@ -0,0 +1,159 @@
"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"
},
{
code: `
@mixin foo {
&.context {
color: red;
}
}
`,
description: "Parent selector in mixin"
},
{
code: `
.bar {
.parent {
color: blue;
& .context {
color: red;
}
}
}
`,
description: "Parent selector nested in more than one style rule"
},
{
code: `
.parent {
color: blue;
.context & {
color: red;
}
}
`,
description: "Selector ending in parent selector"
},
{
code: `
.parent {
color: blue;
.context & .b {
color: red;
}
}
`,
description: "Parent selector in the middle of complex selector"
},
{
code: `
.parent {
color: blue;
& .b, .context & {
color: red;
}
}
`,
description: "Complex selector, one ending in parent selector"
},
{
code: `
@mixin foo {
.parent {
color: blue;
& .context {
color: red;
}
}
}
`,
description: "Parent selector in mixin, nested, at the beginning",
message:
"Unexpected nested parent selector in @mixin rule. (scss/mixin-no-risky-parent-selectors)"
}
],

reject: [
{
code: `
@mixin foo {
.bar {
color: blue;
.baz & {
color: red;
}
}
}
`,
description: "Parent selector nested in selector within a mixin",
message:
"Unexpected nested parent selector in @mixin rule. (scss/mixin-no-risky-parent-selectors)"
},
{
code: `
@mixin foo {
.bar {
color: blue;
.qux, .baz & .quux{
color: red;
}
}
}
`,
description: "Parent selector nested in complex selector within mixin",
message:
"Unexpected nested parent selector in @mixin rule. (scss/mixin-no-risky-parent-selectors)"
}
]
});
67 changes: 67 additions & 0 deletions src/rules/mixin-no-risky-parent-selectors/index.js
@@ -0,0 +1,67 @@
"use strict";

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

const ruleName = namespace("mixin-no-risky-parent-selectors");

const messages = utils.ruleMessages(ruleName, {
rejected: `Unexpected nested parent selector in @mixin rule.`
});

const meta = {
url: ruleUrl(ruleName)
};

function isWithinMixin(node) {
let parent = node.parent;
while (parent) {
if (parent.type === "atrule" && parent.name === "mixin") {
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("&") &&
isWithinMixin(node) &&
hasNestedParentSelector(node.selector) &&
node.parent.selector
) {
utils.report({
message: messages.rejected,
node,
result,
ruleName
});
}
});
};
}

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

module.exports = rule;

0 comments on commit c3ee96c

Please sign in to comment.