Skip to content

Commit 9c7fd6a

Browse files
authored
feat: add selector support to require-baseline (#61)
* feat: add selector support Fixes #60 * review: apply suggested changes * feat: ignore selectors tested in @supports rules * docs: valid selector with supports
1 parent a9692b0 commit 9c7fd6a

File tree

3 files changed

+163
-0
lines changed

3 files changed

+163
-0
lines changed

docs/rules/require-baseline.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ This rule warns when it finds any of the following:
2323
- A media condition inside `@media` that isn't widely available.
2424
- A CSS property value that isn't widely available or otherwise isn't enclosed in a `@supports` block (currently limited to identifiers only).
2525
- A CSS property function that isn't widely available.
26+
- A CSS pseudo-element or pseudo-class selector that isn't widely available.
2627

2728
The data is provided via the [web-features](https://npmjs.com/package/web-features) package.
2829

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

43+
/* invalid - :has() is not widely available */
44+
h1:has(+ h2) {
45+
margin: 0 0 0.25rem 0;
46+
}
47+
48+
/* valid - @supports indicates you're choosing a limited availability selector */
49+
@supports selector(:has()) {
50+
h1:has(+ h2) {
51+
margin: 0 0 0.25rem 0;
52+
}
53+
}
54+
4255
/* invalid - device-posture is not widely available */
4356
@media (device-posture: folded) {
4457
a {

src/rules/require-baseline.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
atRules,
1616
mediaConditions,
1717
types,
18+
selectors,
1819
} from "../data/baseline-data.js";
1920
import { namedColors } from "../data/colors.js";
2021

@@ -123,6 +124,12 @@ class SupportsRule {
123124
*/
124125
#properties = new Map();
125126

127+
/**
128+
* The selectors supported by this rule.
129+
* @type {Set<string>}
130+
*/
131+
#selectors = new Set();
132+
126133
/**
127134
* Adds a property to the rule.
128135
* @param {string} property The name of the property.
@@ -218,6 +225,24 @@ class SupportsRule {
218225

219226
return supportedProperty.hasFunctions();
220227
}
228+
229+
/**
230+
* Adds a selector to the rule.
231+
* @param {string} selector The name of the selector.
232+
* @returns {void}
233+
*/
234+
addSelector(selector) {
235+
this.#selectors.add(selector);
236+
}
237+
238+
/**
239+
* Determines if the rule supports a selector.
240+
* @param {string} selector The name of the selector.
241+
* @returns {boolean} `true` if the selector is supported, `false` if not.
242+
*/
243+
hasSelector(selector) {
244+
return this.#selectors.has(selector);
245+
}
221246
}
222247

223248
/**
@@ -303,6 +328,15 @@ class SupportsRules {
303328
hasPropertyFunctions(property) {
304329
return this.#rules.some(rule => rule.hasFunctions(property));
305330
}
331+
332+
/**
333+
* Determines if any rule supports a selector.
334+
* @param {string} selector The name of the selector.
335+
* @returns {boolean} `true` if any rule supports the selector, `false` if not.
336+
*/
337+
hasSelector(selector) {
338+
return this.#rules.some(rule => rule.hasSelector(selector));
339+
}
306340
}
307341

308342
//-----------------------------------------------------------------------------
@@ -347,6 +381,8 @@ export default {
347381
"Type '{{type}}' is not a {{availability}} available baseline feature.",
348382
notBaselineMediaCondition:
349383
"Media condition '{{condition}}' is not a {{availability}} available baseline feature.",
384+
notBaselineSelector:
385+
"Selector '{{selector}}' is not a {{availability}} available baseline feature.",
350386
},
351387
},
352388

@@ -459,6 +495,16 @@ export default {
459495

460496
continue;
461497
}
498+
499+
if (
500+
conditionChild.type === "FeatureFunction" &&
501+
conditionChild.feature === "selector"
502+
) {
503+
for (const selectorChild of conditionChild.value
504+
.children) {
505+
supportsRule.addSelector(selectorChild.name);
506+
}
507+
}
462508
}
463509
},
464510

@@ -625,6 +671,53 @@ export default {
625671
});
626672
}
627673
},
674+
675+
Selector(node) {
676+
for (const child of node.children) {
677+
const selector = child.name;
678+
679+
if (!selectors.has(selector)) {
680+
continue;
681+
}
682+
683+
// if the selector has been tested in a @supports rule, don't check it
684+
if (supportsRules.hasSelector(selector)) {
685+
continue;
686+
}
687+
688+
const selectorLevel = selectors.get(selector);
689+
690+
if (selectorLevel < baselineLevel) {
691+
const loc = child.loc;
692+
693+
// some selectors are prefixed with the : or :: symbols
694+
let prefixSymbolLength = 0;
695+
if (child.type === "PseudoClassSelector") {
696+
prefixSymbolLength = 1;
697+
} else if (child.type === "PseudoElementSelector") {
698+
prefixSymbolLength = 2;
699+
}
700+
701+
context.report({
702+
loc: {
703+
start: loc.start,
704+
end: {
705+
line: loc.start.line,
706+
column:
707+
loc.start.column +
708+
selector.length +
709+
prefixSymbolLength,
710+
},
711+
},
712+
messageId: "notBaselineSelector",
713+
data: {
714+
selector,
715+
availability,
716+
},
717+
});
718+
}
719+
}
720+
},
628721
};
629722
},
630723
};

tests/rules/require-baseline.test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ ruleTester.run("require-baseline", rule, {
5757
`@supports (width: abs(20% - 100px)) {
5858
a { width: abs(20% - 100px); }
5959
}`,
60+
`@supports selector(:has()) {
61+
h1:has(+ h2) { color: red; }
62+
}`,
6063
"div { cursor: pointer; }",
6164
{
6265
code: `@property --foo {
@@ -340,5 +343,59 @@ ruleTester.run("require-baseline", rule, {
340343
},
341344
],
342345
},
346+
{
347+
code: "h1:has(+ h2) { margin: 0 0 0.25rem 0; }",
348+
errors: [
349+
{
350+
messageId: "notBaselineSelector",
351+
data: {
352+
selector: "has",
353+
availability: "widely",
354+
},
355+
line: 1,
356+
column: 3,
357+
endLine: 1,
358+
endColumn: 7,
359+
},
360+
],
361+
},
362+
{
363+
code: `@supports selector(:has()) {}
364+
365+
@supports (color: red) {
366+
h1:has(+ h2) {
367+
color: red;
368+
}
369+
}`,
370+
errors: [
371+
{
372+
messageId: "notBaselineSelector",
373+
data: {
374+
selector: "has",
375+
availability: "widely",
376+
},
377+
line: 4,
378+
column: 7,
379+
endLine: 4,
380+
endColumn: 11,
381+
},
382+
],
383+
},
384+
{
385+
code: "details::details-content { background-color: #a29bfe; }",
386+
errors: [
387+
{
388+
messageId: "notBaselineSelector",
389+
data: {
390+
selector: "details-content",
391+
availability: "widely",
392+
},
393+
line: 1,
394+
column: 8,
395+
endLine: 1,
396+
endColumn: 25,
397+
},
398+
],
399+
},
343400
],
344401
});

0 commit comments

Comments
 (0)