diff --git a/docs/rules/use-baseline.md b/docs/rules/use-baseline.md index c31f21a2..3aad0221 100644 --- a/docs/rules/use-baseline.md +++ b/docs/rules/use-baseline.md @@ -99,11 +99,66 @@ Examples of **correct** code: ### Options -This rule accepts an option object with the following properties: +This rule accepts an options object with the following properties: - `available` (default: `"widely"`) - change to `"newly"` to allow features that are at the Baseline newly available stage: features that have been supported on all core browsers for less than 30 months - set to a numeric baseline year, such as `2023`, to allow features that became Baseline newly available that year, or earlier +- `allowAtRules` (default: `[]`) - Specify an array of at-rules that are allowed to be used. +- `allowProperties` (default: `[]`) - Specify an array of properties that are allowed to be used. +- `allowSelectors` (default: `[]`) - Specify an array of selectors that are allowed to be used. + +#### `allowAtRules` + +Examples of **correct** code with `{ allowAtRules: ["container"] }`: + +```css +/* eslint css/use-baseline: ["error", { allowAtRules: ["container"] }] */ + +@container (width > 400px) { + h2 { + font-size: 1.5em; + } +} +``` + +#### `allowProperties` + +Examples of **correct** code with `{ allowProperties: ["user-select"] }`: + +```css +/* eslint css/use-baseline: ["error", { allowProperties: ["user-select"] }] */ + +.unselectable { + user-select: none; +} +``` + +#### `allowSelectors` + +When you want to allow the [& nesting selector](https://developer.mozilla.org/en-US/docs/Web/CSS/Nesting_selector), you can use `"nesting"`. + +Examples of **correct** code with `{ allowSelectors: ["nesting"] }`: + +```css +/* eslint css/use-baseline: ["error", { allowSelectors: ["nesting"] }] */ + +.parent { + &:hover { + background-color: blue; + } +} +``` + +Examples of **correct** code with `{ allowSelectors: ["has"] }`: + +```css +/* eslint css/use-baseline: ["error", { allowSelectors: ["has"] }] */ + +h1:has(+ h2) { + margin: 0 0 0.25rem 0; +} +``` ## When Not to Use It diff --git a/src/rules/use-baseline.js b/src/rules/use-baseline.js index fbb7d00c..49399e32 100644 --- a/src/rules/use-baseline.js +++ b/src/rules/use-baseline.js @@ -28,7 +28,10 @@ import { namedColors } from "../data/colors.js"; * @import { Identifier, FunctionNodePlain } from "@eslint/css-tree" * @typedef {"notBaselineProperty" | "notBaselinePropertyValue" | "notBaselineAtRule" | "notBaselineType" | "notBaselineMediaCondition" | "notBaselineSelector"} UseBaselineMessageIds * @typedef {[{ - * available?: "widely" | "newly" | number + * available?: "widely" | "newly" | number, + * allowAtRules?: string[], + * allowProperties?: string[], + * allowSelectors?: string[] * }]} UseBaselineOptions * @typedef {CSSRuleDefinition<{ RuleOptions: UseBaselineOptions, MessageIds: UseBaselineMessageIds }>} UseBaselineRuleDefinition */ @@ -430,6 +433,27 @@ export default { }, ], }, + allowAtRules: { + type: "array", + items: { + type: "string", + }, + uniqueItems: true, + }, + allowProperties: { + type: "array", + items: { + type: "string", + }, + uniqueItems: true, + }, + allowSelectors: { + type: "array", + items: { + type: "string", + }, + uniqueItems: true, + }, }, additionalProperties: false, }, @@ -438,6 +462,9 @@ export default { defaultOptions: [ { available: "widely", + allowAtRules: [], + allowProperties: [], + allowSelectors: [], }, ], @@ -462,6 +489,9 @@ export default { context.options[0].available, ); const supportsRules = new SupportsRules(); + const allowAtRules = new Set(context.options[0].allowAtRules); + const allowProperties = new Set(context.options[0].allowProperties); + const allowSelectors = new Set(context.options[0].allowSelectors); /** * Checks a property value identifier to see if it's a baseline feature. @@ -587,6 +617,10 @@ export default { return; } + if (allowProperties.has(property)) { + return; + } + /* * Step 1: Check that the property is in the baseline. * @@ -723,6 +757,10 @@ export default { return; } + if (allowAtRules.has(node.name)) { + return; + } + const featureStatus = atRules.get(node.name); if (!baselineAvailability.isSupported(featureStatus)) { @@ -756,6 +794,10 @@ export default { return; } + if (allowSelectors.has(selector)) { + return; + } + // if the selector has been tested in a @supports rule, don't check it if (supportsRules.hasSelector(selector)) { return; @@ -799,6 +841,11 @@ export default { NestingSelector(node) { // NestingSelector implies CSS nesting const selector = "nesting"; + + if (allowSelectors.has(selector)) { + return; + } + const featureStatus = selectors.get(selector); if (baselineAvailability.isSupported(featureStatus)) { return; diff --git a/tests/rules/use-baseline.test.js b/tests/rules/use-baseline.test.js index 19fb8b4d..a7e47d9c 100644 --- a/tests/rules/use-baseline.test.js +++ b/tests/rules/use-baseline.test.js @@ -88,6 +88,26 @@ ruleTester.run("use-baseline", rule, { code: ".box { backdrop-filter: blur(10px); }", options: [{ available: 2024 }], }, + { + code: "h1:has(+ h2) { margin: 0 0 0.25rem 0; }", + options: [{ allowSelectors: ["has"] }], + }, + { + code: `label { + & input { + border: blue 2px dashed; + } + }`, + options: [{ available: 2022, allowSelectors: ["nesting"] }], + }, + { + code: "@container (min-width: 800px) { a { color: red; } }", + options: [{ available: 2022, allowAtRules: ["container"] }], + }, + { + code: "a { accent-color: bar; backdrop-filter: auto }", + options: [{ allowProperties: ["accent-color", "backdrop-filter"] }], + }, ], invalid: [ { @@ -478,5 +498,56 @@ ruleTester.run("use-baseline", rule, { }, ], }, + { + code: "@view-transition { from-view: a; to-view: b; }\n@container (min-width: 800px) { a { color: red; } }", + options: [{ allowAtRules: ["container"] }], + errors: [ + { + messageId: "notBaselineAtRule", + data: { + atRule: "view-transition", + availability: "widely", + }, + line: 1, + column: 1, + endLine: 1, + endColumn: 17, + }, + ], + }, + { + code: "a { accent-color: red; backdrop-filter: blur(10px); }", + options: [{ allowProperties: ["accent-color"] }], + errors: [ + { + messageId: "notBaselineProperty", + data: { + property: "backdrop-filter", + availability: "widely", + }, + line: 1, + column: 24, + endLine: 1, + endColumn: 39, + }, + ], + }, + { + code: "h1:has(+ h2) { margin: 0; }\nh1:fullscreen { color: red; }", + options: [{ allowSelectors: ["has"] }], + errors: [ + { + messageId: "notBaselineSelector", + data: { + selector: "fullscreen", + availability: "widely", + }, + line: 2, + column: 3, + endLine: 2, + endColumn: 14, + }, + ], + }, ], });