From 6ff2cef971bd279c70fa0e0c00e387e788529d16 Mon Sep 17 00:00:00 2001 From: Rick Viscomi Date: Fri, 21 Feb 2025 23:24:50 -0500 Subject: [PATCH 1/4] feat: add selector support Fixes #60 --- docs/rules/require-baseline.md | 6 ++++ src/rules/require-baseline.js | 45 ++++++++++++++++++++++++++++ tests/rules/require-baseline.test.js | 32 ++++++++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/docs/rules/require-baseline.md b/docs/rules/require-baseline.md index 1f5adcb..2955c0c 100644 --- a/docs/rules/require-baseline.md +++ b/docs/rules/require-baseline.md @@ -23,6 +23,7 @@ This rule warns when it finds any of the following: - A media condition inside `@media` that isn't widely available. - A CSS property value that isn't widely available or otherwise isn't enclosed in a `@supports` block (currently limited to identifiers only). - A CSS property function that isn't widely available. +- A CSS pseudo-element or pseudo-class selector that isn't widely available. The data is provided via the [web-features](https://npmjs.com/package/web-features) package. @@ -39,6 +40,11 @@ a { width: abs(20% - 100px); } +/* invalid - :has() is not widely available */ +h1:has(+ h2) { + margin: 0 0 0.25rem 0; +} + /* invalid - device-posture is not widely available */ @media (device-posture: folded) { a { diff --git a/src/rules/require-baseline.js b/src/rules/require-baseline.js index 6b9d90b..5c53c7d 100644 --- a/src/rules/require-baseline.js +++ b/src/rules/require-baseline.js @@ -15,6 +15,7 @@ import { atRules, mediaConditions, types, + selectors, } from "../data/baseline-data.js"; import { namedColors } from "../data/colors.js"; @@ -347,6 +348,8 @@ export default { "Type '{{type}}' is not a {{availability}} available baseline feature.", notBaselineMediaCondition: "Media condition '{{condition}}' is not a {{availability}} available baseline feature.", + notBaselineSelector: + "Selector '{{selector}}' is not a {{availability}} available baseline feature.", }, }, @@ -625,6 +628,48 @@ export default { }); } }, + + Selector(node) { + for (const child of node.children) { + const selector = child.name; + + if (!selectors.has(selector)) { + continue; + } + + const ruleLevel = selectors.get(selector); + + if (ruleLevel < baselineLevel) { + const loc = child.loc; + + // some selectors are prefixed with the : or :: symbols + let prefixSymbolLength = 0; + if (child.type === "PseudoClassSelector") { + prefixSymbolLength = 1; + } else if (child.type === "PseudoElementSelector") { + prefixSymbolLength = 2; + } + + context.report({ + loc: { + start: loc.start, + end: { + line: loc.start.line, + column: + loc.start.column + + selector.length + + prefixSymbolLength, + }, + }, + messageId: "notBaselineSelector", + data: { + selector, + availability, + }, + }); + } + } + }, }; }, }; diff --git a/tests/rules/require-baseline.test.js b/tests/rules/require-baseline.test.js index 2147367..26d6db8 100644 --- a/tests/rules/require-baseline.test.js +++ b/tests/rules/require-baseline.test.js @@ -340,5 +340,37 @@ ruleTester.run("require-baseline", rule, { }, ], }, + { + code: "h1:has(+ h2) { margin: 0 0 0.25rem 0; }", + errors: [ + { + messageId: "notBaselineSelector", + data: { + selector: "has", + availability: "widely", + }, + line: 1, + column: 3, + endLine: 1, + endColumn: 7, + }, + ], + }, + { + code: "details::details-content { background-color: #a29bfe; }", + errors: [ + { + messageId: "notBaselineSelector", + data: { + selector: "details-content", + availability: "widely", + }, + line: 1, + column: 8, + endLine: 1, + endColumn: 25, + }, + ], + }, ], }); From 66524b673d6196f9aaf1c71f6e2bbe7875476c42 Mon Sep 17 00:00:00 2001 From: Rick Viscomi Date: Mon, 24 Feb 2025 12:52:07 -0500 Subject: [PATCH 2/4] review: apply suggested changes --- src/rules/require-baseline.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rules/require-baseline.js b/src/rules/require-baseline.js index 5c53c7d..530632f 100644 --- a/src/rules/require-baseline.js +++ b/src/rules/require-baseline.js @@ -637,9 +637,9 @@ export default { continue; } - const ruleLevel = selectors.get(selector); + const selectorLevel = selectors.get(selector); - if (ruleLevel < baselineLevel) { + if (selectorLevel < baselineLevel) { const loc = child.loc; // some selectors are prefixed with the : or :: symbols From 00ce9173255291b1e41494839214416d5139cd2f Mon Sep 17 00:00:00 2001 From: Rick Viscomi Date: Mon, 24 Feb 2025 14:00:13 -0500 Subject: [PATCH 3/4] feat: ignore selectors tested in @supports rules --- src/rules/require-baseline.js | 48 ++++++++++++++++++++++++++++ tests/rules/require-baseline.test.js | 25 +++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/rules/require-baseline.js b/src/rules/require-baseline.js index 530632f..8b66e27 100644 --- a/src/rules/require-baseline.js +++ b/src/rules/require-baseline.js @@ -124,6 +124,12 @@ class SupportsRule { */ #properties = new Map(); + /** + * The selectors supported by this rule. + * @type {Set} + */ + #selectors = new Set(); + /** * Adds a property to the rule. * @param {string} property The name of the property. @@ -219,6 +225,24 @@ class SupportsRule { return supportedProperty.hasFunctions(); } + + /** + * Adds a selector to the rule. + * @param {string} selector The name of the selector. + * @returns {void} + */ + addSelector(selector) { + this.#selectors.add(selector); + } + + /** + * Determines if the rule supports a selector. + * @param {string} selector The name of the selector. + * @returns {boolean} `true` if the selector is supported, `false` if not. + */ + hasSelector(selector) { + return this.#selectors.has(selector); + } } /** @@ -304,6 +328,15 @@ class SupportsRules { hasPropertyFunctions(property) { return this.#rules.some(rule => rule.hasFunctions(property)); } + + /** + * Determines if any rule supports a selector. + * @param {string} selector The name of the selector. + * @returns {boolean} `true` if any rule supports the selector, `false` if not. + */ + hasSelector(selector) { + return this.#rules.some(rule => rule.hasSelector(selector)); + } } //----------------------------------------------------------------------------- @@ -462,6 +495,16 @@ export default { continue; } + + if ( + conditionChild.type === "FeatureFunction" && + conditionChild.feature === "selector" + ) { + for (const selectorChild of conditionChild.value + .children) { + supportsRule.addSelector(selectorChild.name); + } + } } }, @@ -637,6 +680,11 @@ export default { continue; } + // if the selector has been tested in a @supports rule, don't check it + if (supportsRules.hasSelector(selector)) { + continue; + } + const selectorLevel = selectors.get(selector); if (selectorLevel < baselineLevel) { diff --git a/tests/rules/require-baseline.test.js b/tests/rules/require-baseline.test.js index 26d6db8..b08358f 100644 --- a/tests/rules/require-baseline.test.js +++ b/tests/rules/require-baseline.test.js @@ -57,6 +57,9 @@ ruleTester.run("require-baseline", rule, { `@supports (width: abs(20% - 100px)) { a { width: abs(20% - 100px); } }`, + `@supports selector(:has()) { + h1:has(+ h2) { color: red; } + }`, "div { cursor: pointer; }", { code: `@property --foo { @@ -356,6 +359,28 @@ ruleTester.run("require-baseline", rule, { }, ], }, + { + code: `@supports selector(:has()) {} + + @supports (color: red) { + h1:has(+ h2) { + color: red; + } + }`, + errors: [ + { + messageId: "notBaselineSelector", + data: { + selector: "has", + availability: "widely", + }, + line: 4, + column: 7, + endLine: 4, + endColumn: 11, + }, + ], + }, { code: "details::details-content { background-color: #a29bfe; }", errors: [ From f1187c8097f6775734e3cc5417e315372ba75ece Mon Sep 17 00:00:00 2001 From: Rick Viscomi Date: Tue, 25 Feb 2025 14:04:47 -0500 Subject: [PATCH 4/4] docs: valid selector with supports --- docs/rules/require-baseline.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/rules/require-baseline.md b/docs/rules/require-baseline.md index 2955c0c..fdc53d6 100644 --- a/docs/rules/require-baseline.md +++ b/docs/rules/require-baseline.md @@ -45,6 +45,13 @@ h1:has(+ h2) { margin: 0 0 0.25rem 0; } +/* valid - @supports indicates you're choosing a limited availability selector */ +@supports selector(:has()) { + h1:has(+ h2) { + margin: 0 0 0.25rem 0; + } +} + /* invalid - device-posture is not widely available */ @media (device-posture: folded) { a {