diff --git a/docs/rules/use-baseline.md b/docs/rules/use-baseline.md index 6f3edc7b..50a1aaec 100644 --- a/docs/rules/use-baseline.md +++ b/docs/rules/use-baseline.md @@ -25,7 +25,7 @@ While using only Baseline widely available features can help ensure the greatest This rule warns when it finds any of the following: - A CSS property that isn't widely available or otherwise isn't enclosed in a `@supports` block. -- An at-rule that isn't widely available. +- An at-rule that isn't widely available or isn't guarded by a matching `@supports at-rule()` query. - 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. @@ -100,6 +100,15 @@ Examples of **correct** code: accent-color: auto; } } + +/* valid - @supports indicates you're choosing a limited availability at-rule */ +@supports at-rule(@scope) { + @scope (.card) { + a { + color: red; + } + } +} ``` **Important:** While the `cursor` property is not considered baseline, it has wide support and will likely be considered baseline once the WebDX Community Group adds [an editorial step](https://github.com/web-platform-dx/web-features/issues/1038). In the meantime, this rule does not warn when `cursor` is used. diff --git a/src/rules/use-baseline.js b/src/rules/use-baseline.js index 0aa37022..aa6109f6 100644 --- a/src/rules/use-baseline.js +++ b/src/rules/use-baseline.js @@ -176,6 +176,12 @@ class SupportsRule { */ #selectors = new Set(); + /** + * The at-rules supported by this rule. + * @type {Set} + */ + #atRules = new Set(); + /** * Adds a property to the rule. * @param {string} property The name of the property. @@ -320,6 +326,24 @@ class SupportsRule { hasSelector(selector) { return this.#selectors.has(selector); } + + /** + * Adds an at-rule to the rule. + * @param {string} atRule The name of the at-rule. + * @returns {void} + */ + addAtRule(atRule) { + this.#atRules.add(atRule); + } + + /** + * Determines if the rule supports an at-rule. + * @param {string} atRule The name of the at-rule. + * @returns {boolean} `true` if the at-rule is supported, `false` if not. + */ + hasAtRule(atRule) { + return this.#atRules.has(atRule); + } } /** @@ -424,6 +448,15 @@ class SupportsRules { hasSelector(selector) { return this.#rules.some(rule => rule.hasSelector(selector)); } + + /** + * Determines if any rule supports an at-rule. + * @param {string} atRule The name of the at-rule. + * @returns {boolean} `true` if any rule supports the at-rule, `false` if not. + */ + hasAtRule(atRule) { + return this.#rules.some(rule => rule.hasAtRule(atRule)); + } } /** @@ -775,14 +808,26 @@ export default /** @satisfies {UseBaselineRuleDefinition} */ ({ continue; } - if ( - conditionChild.type === "FeatureFunction" && - conditionChild.feature === "selector" - ) { + if (conditionChild.type !== "FeatureFunction") { + continue; + } + + const feature = conditionChild.feature.toLowerCase(); + + if (feature === "selector") { for (const selectorChild of conditionChild.value .children) { supportsRule.addSelector(selectorChild.name); } + + continue; + } + + if (feature === "at-rule") { + const atRule = conditionChild.value.value + .slice(1) + .toLowerCase(); + supportsRule.addAtRule(atRule); } } }, @@ -950,6 +995,10 @@ export default /** @satisfies {UseBaselineRuleDefinition} */ ({ return; } + if (supportsRules.hasAtRule(atRuleName)) { + return; + } + const featureStatus = atRules.get(atRuleName); if (!baselineAvailability.isSupported(featureStatus)) { diff --git a/tests/rules/use-baseline.test.js b/tests/rules/use-baseline.test.js index 736bc529..d5071fac 100644 --- a/tests/rules/use-baseline.test.js +++ b/tests/rules/use-baseline.test.js @@ -73,6 +73,16 @@ ruleTester.run("use-baseline", rule, { `@supports selector(:fullscreen) { h1:fullscreen { color: red; } }`, + `@supports at-rule(@scope) { + @scope (.card) { + a { color: red; } + } + }`, + `@SUPPORTS AT-RULE(@SCOPE) { + @scope (.card) { + a { color: red; } + } + }`, "div { cursor: pointer; }", "pre { overflow: auto; }", ".highlight, #highlight, highlight { color: red }", @@ -549,6 +559,38 @@ ruleTester.run("use-baseline", rule, { }, ], }, + { + code: "@supports at-rule(@scope) {}\n@supports (color: red) {\n@scope (.card) { a { color: red; } }\n}", + errors: [ + { + messageId: "notBaselineAtRule", + data: { + atRule: "scope", + availability: "widely", + }, + line: 3, + column: 1, + endLine: 3, + endColumn: 7, + }, + ], + }, + { + code: "@supports at-rule(@scope) {\n@view-transition { navigation: auto; }\n}", + errors: [ + { + messageId: "notBaselineAtRule", + data: { + atRule: "view-transition", + availability: "widely", + }, + line: 2, + column: 1, + endLine: 2, + endColumn: 17, + }, + ], + }, { code: "details::details-content { background-color: #a29bfe; }", errors: [