From 2f1c7bf641b1ed9bc3496bce5c7d1fdbb2744474 Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Fri, 19 Jan 2024 10:53:29 +0100 Subject: [PATCH] nesting : add check for specificity --- package-lock.json | 28 ++++++++++++------------- packages/nesting/CHANGELOG.md | 4 ++++ packages/nesting/index.mjs | 37 +++++++++++++++++++++++++++++++++ packages/nesting/index.test.mjs | 22 ++++++++++++++++++++ packages/nesting/package.json | 3 ++- 5 files changed, 79 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index a2b7b57..7283dd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -131,7 +131,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.1.tgz", "integrity": "sha512-NPljRHkq4a14YzZ3YD406uaxh7s0g6eAq3L9aLOWywoqe8PkYamAvtsh7KNX6c++ihDrJ0RiU+/z7rGnhlZ5ww==", - "dev": true, "funding": [ { "type": "github", @@ -214,9 +213,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.21.tgz", + "integrity": "sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1967,14 +1966,14 @@ } }, "node_modules/stylelint": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.1.0.tgz", - "integrity": "sha512-Sh1rRV0lN1qxz/QsuuooLWsIZ/ona7NKw/fRZd6y6PyXYdD2W0EAzJ8yJcwSx4Iw/muz0CF09VZ+z4EiTAcKmg==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.2.0.tgz", + "integrity": "sha512-gwqU5AkIb52wrAzzn+359S3NIJDMl02TXLUaV2tzA/L6jUdpTwNt+MCxHlc8+Hb2bUHlYVo92YeSIryF2gJthA==", "dev": true, "dependencies": { - "@csstools/css-parser-algorithms": "^2.4.0", - "@csstools/css-tokenizer": "^2.2.2", - "@csstools/media-query-list-parser": "^2.1.6", + "@csstools/css-parser-algorithms": "^2.5.0", + "@csstools/css-tokenizer": "^2.2.3", + "@csstools/media-query-list-parser": "^2.1.7", "@csstools/selector-specificity": "^3.0.1", "balanced-match": "^2.0.0", "colord": "^2.9.3", @@ -1994,14 +1993,14 @@ "is-plain-object": "^5.0.0", "known-css-properties": "^0.29.0", "mathml-tag-names": "^2.1.3", - "meow": "^13.0.0", + "meow": "^13.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "picocolors": "^1.0.0", - "postcss": "^8.4.32", + "postcss": "^8.4.33", "postcss-resolve-nested-selector": "^0.1.1", "postcss-safe-parser": "^7.0.0", - "postcss-selector-parser": "^6.0.13", + "postcss-selector-parser": "^6.0.15", "postcss-value-parser": "^4.2.0", "resolve-from": "^5.0.0", "string-width": "^4.2.3", @@ -2460,9 +2459,10 @@ }, "packages/nesting": { "name": "@mrhenry/stylelint-mrhenry-nesting", - "version": "3.0.1", + "version": "3.1.0", "license": "MIT", "dependencies": { + "@csstools/selector-specificity": "^3.0.1", "postcss-selector-parser": "^6.0.15" }, "devDependencies": { diff --git a/packages/nesting/CHANGELOG.md b/packages/nesting/CHANGELOG.md index 83ed429..879c893 100644 --- a/packages/nesting/CHANGELOG.md +++ b/packages/nesting/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 3.1.0 + +- Add warnings for mixed specificity in nesting + ## 3.0.1 - Add type annotations diff --git a/packages/nesting/index.mjs b/packages/nesting/index.mjs index 84f9b1f..97e33e3 100644 --- a/packages/nesting/index.mjs +++ b/packages/nesting/index.mjs @@ -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, { @@ -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 = { @@ -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; diff --git a/packages/nesting/index.test.mjs b/packages/nesting/index.test.mjs index d618051..b454fc0 100644 --- a/packages/nesting/index.test.mjs +++ b/packages/nesting/index.test.mjs @@ -63,6 +63,10 @@ testRule({ code: "div { { color: red; } }", description: "empty selector", }, + { + code: ":where(div), :where(.foo) { &:hover { color: green; } }", + description: "Mixed specificity", + }, ], reject: [ @@ -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 + }, ] }); diff --git a/packages/nesting/package.json b/packages/nesting/package.json index 8276e68..e175be3 100644 --- a/packages/nesting/package.json +++ b/packages/nesting/package.json @@ -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" @@ -28,6 +28,7 @@ "stylelint-plugin" ], "dependencies": { + "@csstools/selector-specificity": "^3.0.1", "postcss-selector-parser": "^6.0.15" }, "peerDependencies": {