Skip to content

Commit

Permalink
nesting : add check for specificity (#67)
Browse files Browse the repository at this point in the history
  • Loading branch information
romainmenke authored Jan 19, 2024
1 parent 3208f14 commit ebdb2cb
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 15 deletions.
28 changes: 14 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/nesting/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 3.1.0

- Add warnings for mixed specificity in nesting

## 3.0.1

- Add type annotations
Expand Down
37 changes: 37 additions & 0 deletions packages/nesting/index.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import stylelint from 'stylelint';
import selectorParser from 'postcss-selector-parser';
import { compare, selectorSpecificity } from '@csstools/selector-specificity';

const ruleName = "@mrhenry/stylelint-mrhenry-nesting";
const messages = stylelint.utils.ruleMessages(ruleName, {
Expand All @@ -18,6 +19,9 @@ const messages = stylelint.utils.ruleMessages(ruleName, {
rejectedNestingSelectorIncorrectShape: () => {
return `Nested selectors must be compound selectors, starting with "&" and followed by a single pseudo selector.`;
},
rejectedMixedSpecificity: () => {
return `Each selector of a list in a nested context take the specificity of the most specific list item. This can lead to unexpected results.`;
},
});

const meta = {
Expand Down Expand Up @@ -101,6 +105,39 @@ const ruleFunction = (primaryOption, secondaryOption, context) => {
});
});

postcssRoot.walkRules((rule) => {
const containsBlocks = rule.nodes && rule.nodes.some((node) => node.type === 'rule' || node.type === 'atrule');
if (!containsBlocks) {
return;
}

const selectorAST = selectorParser().astSync(rule.selector);
if (selectorAST.nodes?.length < 2) {
return;
}

const specificities = selectorAST.nodes.map((node) => {
return selectorSpecificity(node);
});

const specificitiesAreEqual = specificities.every((specificity) => {
return compare(specificity, specificities[0]) === 0;
});

if (specificitiesAreEqual) {
return;
}

stylelint.utils.report({
message: messages.rejectedMixedSpecificity(),
node: rule,
index: 0,
endIndex: rule.selector.length,
result: postcssResult,
ruleName,
});
});

postcssRoot.walkRules((rule) => {
{
let rulesDepth = 1;
Expand Down
22 changes: 22 additions & 0 deletions packages/nesting/index.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ testRule({
code: "div { { color: red; } }",
description: "empty selector",
},
{
code: ":where(div), :where(.foo) { &:hover { color: green; } }",
description: "Mixed specificity",
},
],

reject: [
Expand Down Expand Up @@ -192,6 +196,24 @@ testRule({
endLine: 1,
endColumn: 22
},
{
code: "div, .foo { @media screen { color: green; } }",
description: "Mixed specificity",
message: rule.messages.rejectedMixedSpecificity(),
line: 1,
column: 1,
endLine: 1,
endColumn: 10
},
{
code: "div, .foo { &:hover { color: green; } }",
description: "Mixed specificity",
message: rule.messages.rejectedMixedSpecificity(),
line: 1,
column: 1,
endLine: 1,
endColumn: 10
},
]
});

Expand Down
3 changes: 2 additions & 1 deletion packages/nesting/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mrhenry/stylelint-mrhenry-nesting",
"version": "3.0.1",
"version": "3.1.0",
"description": "Mr. Henry's preferred way of writing nested CSS",
"publishConfig": {
"access": "public"
Expand Down Expand Up @@ -28,6 +28,7 @@
"stylelint-plugin"
],
"dependencies": {
"@csstools/selector-specificity": "^3.0.1",
"postcss-selector-parser": "^6.0.15"
},
"peerDependencies": {
Expand Down

0 comments on commit ebdb2cb

Please sign in to comment.