Skip to content

Commit

Permalink
feat: add selector support to require-baseline (#61)
Browse files Browse the repository at this point in the history
* feat: add selector support

Fixes #60

* review: apply suggested changes

* feat: ignore selectors tested in @supports rules

* docs: valid selector with supports
  • Loading branch information
rviscomi authored Feb 25, 2025
1 parent a9692b0 commit 9c7fd6a
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 0 deletions.
13 changes: 13 additions & 0 deletions docs/rules/require-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -39,6 +40,18 @@ a {
width: abs(20% - 100px);
}

/* invalid - :has() is not widely available */
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 {
Expand Down
93 changes: 93 additions & 0 deletions src/rules/require-baseline.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
atRules,
mediaConditions,
types,
selectors,
} from "../data/baseline-data.js";
import { namedColors } from "../data/colors.js";

Expand Down Expand Up @@ -123,6 +124,12 @@ class SupportsRule {
*/
#properties = new Map();

/**
* The selectors supported by this rule.
* @type {Set<string>}
*/
#selectors = new Set();

/**
* Adds a property to the rule.
* @param {string} property The name of the property.
Expand Down Expand Up @@ -218,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);
}
}

/**
Expand Down Expand Up @@ -303,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));
}
}

//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -347,6 +381,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.",
},
},

Expand Down Expand Up @@ -459,6 +495,16 @@ export default {

continue;
}

if (
conditionChild.type === "FeatureFunction" &&
conditionChild.feature === "selector"
) {
for (const selectorChild of conditionChild.value
.children) {
supportsRule.addSelector(selectorChild.name);
}
}
}
},

Expand Down Expand Up @@ -625,6 +671,53 @@ export default {
});
}
},

Selector(node) {
for (const child of node.children) {
const selector = child.name;

if (!selectors.has(selector)) {
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) {
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,
},
});
}
}
},
};
},
};
57 changes: 57 additions & 0 deletions tests/rules/require-baseline.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -340,5 +343,59 @@ 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: `@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: [
{
messageId: "notBaselineSelector",
data: {
selector: "details-content",
availability: "widely",
},
line: 1,
column: 8,
endLine: 1,
endColumn: 25,
},
],
},
],
});

0 comments on commit 9c7fd6a

Please sign in to comment.