Repository: eslint/css (js)

Issue:
Bug: `require-baseline` does not properly validate media conditions in `@media` rules
### Environment

ESLint version: v9.17.0
@eslint/css version: v0.3.0
Node version: v22.9.0
npm version: v10.9.1
Operating System: macOS


### Which language are you using?

stylesheet

### What did you do?

Tested `require-baseline` with media conditions.

<details>
<summary>Configuration</summary>

```js
import css from '@eslint/css';

export default [
  {
    files: ['**/*.css'],
    language: 'css/css',
    ...css.configs.recommended,
    rules: {
      'css/require-baseline': ['error', { available: 'widely' }],
    },
  },
];
```
</details>

```css
@media (color-gamut: srgb) {
  p {
    background: #f4ae8a;
  }
}
```


### What did you expect to happen?

An error:

```console
Media condition 'color-gamut' is not a widely available baseline feature.
```

### What actually happened?

No error was reported.

### Link to Minimal Reproducible Example

https://stackblitz.com/edit/vitejs-vite-w3knjrzj?file=src%2Findex.css

### Participation

- [x] I am willing to submit a pull request for this issue.

### Additional comments

_No response_

Pull request:
feat: Add support for media conditions in require-baseline rule
<!--
    Thank you for contributing!

    ESLint adheres to the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
-->

#### Prerequisites checklist

- [x] I have read the [contributing guidelines](https://github.com/eslint/eslint/blob/HEAD/CONTRIBUTING.md).

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (https://eslint.org/docs/latest/contribute/pull-requests)
    - Update or create tests
    - If performance-related, include a benchmark
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

#### What is the purpose of this pull request?

This pull request adds support for media conditions in the `require-baseline` rule.

#### What changes did you make? (Give an overview)

- Added checks for media conditions in `@media` queries.
- Updated `baseline-data.js` to include media conditions.
- Adjusted test cases to cover media conditions.
- Updated documentation to reflect the changes.

#### Related Issues

<!-- include tags like "fixes #123" or "refs #123" -->
fixes #48 

#### Is there anything you'd like reviewers to focus on?

<!-- markdownlint-disable-file MD004 -->


Patch:
diff --git a/README.md b/README.md
index 2b8a8fd..0521c02 100644
--- a/README.md
+++ b/README.md
@@ -242,7 +242,7 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).
 
 <h3>Platinum Sponsors</h3>
 <p><a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="128"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="128"></a></p><h3>Gold Sponsors</h3>
-<p><a href="https://qlty.sh/"><img src="https://images.opencollective.com/qltysh/33d157d/logo.png" alt="Qlty Software" height="96"></a> <a href="https://trunk.io/"><img src="https://images.opencollective.com/trunkio/fb92d60/avatar.png" alt="trunk.io" height="96"></a> <a href="https://shopify.engineering/"><img src="https://avatars.githubusercontent.com/u/8085" alt="Shopify" height="96"></a></p><h3>Silver Sponsors</h3>
+<p><a href="https://qlty.sh/"><img src="https://images.opencollective.com/qltysh/33d157d/logo.png" alt="Qlty Software" height="96"></a> <a href="https://trunk.io/"><img src="https://images.opencollective.com/trunkio/fb92d60/avatar.png" alt="trunk.io" height="96"></a></p><h3>Silver Sponsors</h3>
 <p><a href="https://vite.dev/"><img src="https://images.opencollective.com/vite/e6d15e1/logo.png" alt="Vite" height="64"></a> <a href="https://www.jetbrains.com/"><img src="https://images.opencollective.com/jetbrains/fe76f99/logo.png" alt="JetBrains" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301" alt="American Express" height="64"></a> <a href="https://stackblitz.com"><img src="https://avatars.githubusercontent.com/u/28635252" alt="StackBlitz" height="64"></a></p><h3>Bronze Sponsors</h3>
 <p><a href="https://cybozu.co.jp/"><img src="https://images.opencollective.com/cybozu/933e46d/logo.png" alt="Cybozu" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340" alt="GitBook" height="32"></a> <a href="https://nolebase.ayaka.io"><img src="https://avatars.githubusercontent.com/u/11081491" alt="Neko" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104" alt="Nx" height="32"></a> <a href="https://opensource.mercedes-benz.com/"><img src="https://avatars.githubusercontent.com/u/34240465" alt="Mercedes-Benz Group" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774" alt="HeroCoders" height="32"></a></p>
 <h3>Technology Sponsors</h3>
diff --git a/docs/rules/require-baseline.md b/docs/rules/require-baseline.md
index 2d498ef..1f5adcb 100644
--- a/docs/rules/require-baseline.md
+++ b/docs/rules/require-baseline.md
@@ -20,6 +20,7 @@ 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.
+- 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.
 
@@ -38,6 +39,13 @@ a {
 	width: abs(20% - 100px);
 }
 
+/* invalid - device-posture is not widely available */
+@media (device-posture: folded) {
+	a {
+		color: red;
+	}
+}
+
 /* invalid - property value doesn't match @supports indicator */
 @supports (accent-color: auto) {
 	a {
@@ -60,6 +68,8 @@ a {
 }
 ```
 
+**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.
+
 ### Options
 
 This rule accepts an option object with the following properties:
diff --git a/src/data/baseline-data.js b/src/data/baseline-data.js
index f19df6b..6a43751 100644
--- a/src/data/baseline-data.js
+++ b/src/data/baseline-data.js
@@ -104,7 +104,6 @@ export const properties = new Map([
 	["counter-set", 5],
 	["counter-increment", 10],
 	["counter-reset", 10],
-	["cursor", 0],
 	["custom-property", 10],
 	["display", 10],
 	["dominant-baseline", 10],
@@ -518,6 +517,46 @@ export const atRules = new Map([
 	["starting-style", 0],
 	["supports", 10],
 ]);
+export const mediaConditions = new Map([
+	["color-gamut", 5],
+	["device-posture", 0],
+	["device-aspect-ratio", 0],
+	["device-height", 0],
+	["device-width", 0],
+	["display-mode", 0],
+	["dynamic-range", 10],
+	["forced-colors", 5],
+	["any-hover", 10],
+	["any-pointer", 10],
+	["hover", 10],
+	["pointer", 10],
+	["inverted-colors", 0],
+	["aspect-ratio", 10],
+	["calc", 10],
+	["color", 10],
+	["color-index", 10],
+	["grid", 10],
+	["height", 10],
+	["monochrome", 10],
+	["nested-queries", 10],
+	["orientation", 10],
+	["width", 10],
+	["overflow-block", 5],
+	["overflow-inline", 5],
+	["prefers-color-scheme", 10],
+	["prefers-contrast", 10],
+	["prefers-reduced-data", 0],
+	["prefers-reduced-motion", 10],
+	["prefers-reduced-transparency", 0],
+	["resolution", 5],
+	["-webkit-device-pixel-ratio", 10],
+	["-webkit-max-device-pixel-ratio", 10],
+	["-webkit-min-device-pixel-ratio", 10],
+	["scripting", 5],
+	["-webkit-transform-3d", 10],
+	["update", 5],
+	["video-dynamic-range", 0],
+]);
 export const types = new Map([
 	["abs", 0],
 	["sign", 0],
@@ -1377,48 +1416,6 @@ export const propertyValues = new Map([
 			["smooth", 0],
 		]),
 	],
-	[
-		"cursor",
-		new Map([
-			["alias", 0],
-			["all-scroll", 0],
-			["auto", 0],
-			["cell", 0],
-			["col-resize", 0],
-			["context-menu", 0],
-			["copy", 0],
-			["crosshair", 0],
-			["default", 0],
-			["e-resize", 0],
-			["ew-resize", 0],
-			["grab", 0],
-			["grabbing", 0],
-			["help", 0],
-			["move", 0],
-			["n-resize", 0],
-			["ne-resize", 0],
-			["nesw-resize", 0],
-			["no-drop", 0],
-			["none", 0],
-			["not-allowed", 0],
-			["ns-resize", 0],
-			["nw-resize", 0],
-			["nwse-resize", 0],
-			["pointer", 0],
-			["progress", 0],
-			["row-resize", 0],
-			["s-resize", 0],
-			["se-resize", 0],
-			["sw-resize", 0],
-			["text", 0],
-			["url", 0],
-			["vertical-text", 0],
-			["w-resize", 0],
-			["wait", 0],
-			["zoom-in", 0],
-			["zoom-out", 0],
-		]),
-	],
 	[
 		"text-overflow",
 		new Map([
diff --git a/src/rules/require-baseline.js b/src/rules/require-baseline.js
index 6e5094c..6b9d90b 100644
--- a/src/rules/require-baseline.js
+++ b/src/rules/require-baseline.js
@@ -13,6 +13,7 @@ import {
 	properties,
 	propertyValues,
 	atRules,
+	mediaConditions,
 	types,
 } from "../data/baseline-data.js";
 import { namedColors } from "../data/colors.js";
@@ -344,6 +345,8 @@ export default {
 				"At-rule '@{{atRule}}' is not a {{availability}} available baseline feature.",
 			notBaselineType:
 				"Type '{{type}}' is not a {{availability}} available baseline feature.",
+			notBaselineMediaCondition:
+				"Media condition '{{condition}}' is not a {{availability}} available baseline feature.",
 		},
 	},
 
@@ -549,6 +552,50 @@ export default {
 				supportsRules.pop();
 			},
 
+			"Atrule[name=media] > AtrulePrelude > MediaQueryList > MediaQuery > Condition"(
+				node,
+			) {
+				for (const child of node.children) {
+					// ignore unknown media conditions - no-invalid-at-rules already catches this
+					if (!mediaConditions.has(child.name)) {
+						continue;
+					}
+
+					if (child.type !== "Feature") {
+						continue;
+					}
+
+					const conditionLevel = mediaConditions.get(child.name);
+
+					if (conditionLevel < baselineLevel) {
+						const loc = child.loc;
+
+						context.report({
+							loc: {
+								start: {
+									line: loc.start.line,
+									// add 1 to account for the @ symbol
+									column: loc.start.column + 1,
+								},
+								end: {
+									line: loc.start.line,
+									column:
+										// add 1 to account for the @ symbol
+										loc.start.column +
+										child.name.length +
+										1,
+								},
+							},
+							messageId: "notBaselineMediaCondition",
+							data: {
+								condition: child.name,
+								availability,
+							},
+						});
+					}
+				}
+			},
+
 			Atrule(node) {
 				// ignore unknown at-rules - no-invalid-at-rules already catches this
 				if (!atRules.has(node.name)) {
diff --git a/tools/generate-baseline.js b/tools/generate-baseline.js
index 8dcaf8a..e5b42bc 100644
--- a/tools/generate-baseline.js
+++ b/tools/generate-baseline.js
@@ -17,6 +17,13 @@ import fs from "node:fs";
 // Helpers
 //------------------------------------------------------------------------------
 
+/*
+ * Properties that aren't considered baseline but have wide support.
+ * https://github.com/web-platform-dx/web-features/issues/1038#issuecomment-2627370691
+ */
+
+const WIDE_SUPPORT_PROPERTIES = new Set(["cursor"]);
+
 const BASELINE_HIGH = 10;
 const BASELINE_LOW = 5;
 const BASELINE_FALSE = 0;
@@ -65,12 +72,15 @@ function extractCSSFeatures(features) {
 	const cssPropertyValuePattern =
 		/^css\.properties\.(?<property>[a-zA-Z$\d-]+)\.(?<value>[a-zA-Z$\d-]+)$/u;
 	const cssAtRulePattern = /^css\.at-rules\.(?<atRule>[a-zA-Z$\d-]+)$/u;
+	const cssMediaConditionPattern =
+		/^css\.at-rules\.media\.(?<condition>[a-zA-Z$\d-]+)$/u;
 	const cssTypePattern = /^css\.types\.(?<type>[a-zA-Z$\d-]+)$/u;
 	const cssSelectorPattern = /^css\.selectors\.(?<selector>[a-zA-Z$\d-]+)$/u;
 
 	const properties = {};
 	const propertyValues = {};
 	const atRules = {};
+	const mediaConditions = {};
 	const types = {};
 	const selectors = {};
 
@@ -78,13 +88,21 @@ function extractCSSFeatures(features) {
 		let match;
 
 		// property names
-		if ((match = cssPropertyPattern.exec(key)) !== null) {
+		if (
+			(match = cssPropertyPattern.exec(key)) !== null &&
+			!WIDE_SUPPORT_PROPERTIES.has(match.groups.property)
+		) {
 			properties[match.groups.property] = baselineIds.get(baseline);
 			continue;
 		}
 
 		// property values
 		if ((match = cssPropertyValuePattern.exec(key)) !== null) {
+			// don't include values for these properties
+			if (WIDE_SUPPORT_PROPERTIES.has(match.groups.property)) {
+				continue;
+			}
+
 			if (!propertyValues[match.groups.property]) {
 				propertyValues[match.groups.property] = {};
 			}
@@ -99,6 +117,12 @@ function extractCSSFeatures(features) {
 			continue;
 		}
 
+		// Media conditions (@media features)
+		if ((match = cssMediaConditionPattern.exec(key)) !== null) {
+			mediaConditions[match.groups.condition] = baselineIds.get(baseline);
+			continue;
+		}
+
 		// types
 		if ((match = cssTypePattern.exec(key)) !== null) {
 			types[match.groups.type] = baselineIds.get(baseline);
@@ -116,6 +140,7 @@ function extractCSSFeatures(features) {
 		properties,
 		propertyValues,
 		atRules,
+		mediaConditions,
 		types,
 		selectors,
 	};
@@ -151,6 +176,7 @@ export const BASELINE_FALSE = ${BASELINE_FALSE};
 
 export const properties = new Map(${JSON.stringify(Object.entries(cssFeatures.properties), null, "\t")});
 export const atRules = new Map(${JSON.stringify(Object.entries(cssFeatures.atRules), null, "\t")});
+export const mediaConditions = new Map(${JSON.stringify(Object.entries(cssFeatures.mediaConditions), null, "\t")});
 export const types = new Map(${JSON.stringify(Object.entries(cssFeatures.types), null, "\t")});
 export const selectors = new Map(${JSON.stringify(Object.entries(cssFeatures.selectors), null, "\t")});
 export const propertyValues = new Map([${Object.entries(


Test patch:
diff --git a/tests/rules/require-baseline.test.js b/tests/rules/require-baseline.test.js
index b584c9f..2147367 100644
--- a/tests/rules/require-baseline.test.js
+++ b/tests/rules/require-baseline.test.js
@@ -34,6 +34,8 @@ ruleTester.run("require-baseline", rule, {
 		"a { color: red; -moz-transition: bar }",
 		"@font-face { font-weight: 100 400 }",
 		"@media (min-width: 800px) { a { color: red; } }",
+		"@media (foo) { a { color: red; } }",
+		"@media (prefers-color-scheme: dark) { a { color: red; } }",
 		"@supports (accent-color: auto) { a { accent-color: auto; } }",
 		"@supports (accent-color: red) { a { accent-color: red; } }",
 		"@supports (accent-color: auto) { a { accent-color: red; } }",
@@ -55,6 +57,7 @@ ruleTester.run("require-baseline", rule, {
 		`@supports (width: abs(20% - 100px)) {
 			a { width: abs(20% - 100px); }
 		}`,
+		"div { cursor: pointer; }",
 		{
 			code: `@property --foo {
 				syntax: "*";
@@ -179,7 +182,7 @@ ruleTester.run("require-baseline", rule, {
 				@supports (backdrop-filter: auto) {
 					a { accent-color: red; }
 				}
-				
+
 				a { backdrop-filter: auto; }
 			}`,
 			errors: [
@@ -261,5 +264,81 @@ ruleTester.run("require-baseline", rule, {
 				},
 			],
 		},
+		{
+			code: "@media (color-gamut: srgb) { a { color: red; } }",
+			errors: [
+				{
+					messageId: "notBaselineMediaCondition",
+					data: {
+						condition: "color-gamut",
+						availability: "widely",
+					},
+					line: 1,
+					column: 9,
+					endLine: 1,
+					endColumn: 20,
+				},
+			],
+		},
+		{
+			code: "@media (device-posture: folded) { a { color: red; } }",
+			options: [{ available: "newly" }],
+			errors: [
+				{
+					messageId: "notBaselineMediaCondition",
+					data: {
+						condition: "device-posture",
+						availability: "newly",
+					},
+					line: 1,
+					column: 9,
+					endLine: 1,
+					endColumn: 23,
+				},
+			],
+		},
+		{
+			code: "@media (height: 600px) and (color-gamut: srgb) and (device-posture: folded) { a { color: red; } }",
+			errors: [
+				{
+					messageId: "notBaselineMediaCondition",
+					data: {
+						condition: "color-gamut",
+						availability: "widely",
+					},
+					line: 1,
+					column: 29,
+					endLine: 1,
+					endColumn: 40,
+				},
+				{
+					messageId: "notBaselineMediaCondition",
+					data: {
+						condition: "device-posture",
+						availability: "widely",
+					},
+					line: 1,
+					column: 53,
+					endLine: 1,
+					endColumn: 67,
+				},
+			],
+		},
+		{
+			code: "@media (foo) and (color-gamut: srgb) { a { color: red; } }",
+			errors: [
+				{
+					messageId: "notBaselineMediaCondition",
+					data: {
+						condition: "color-gamut",
+						availability: "widely",
+					},
+					line: 1,
+					column: 19,
+					endLine: 1,
+					endColumn: 30,
+				},
+			],
+		},
 	],
 });