Repository: eslint/css (js)

Issue:
Bug: leading dots in selectors being ignored in `@supports` blocks since last update
### Environment

ESLint version: v9.22.0
@eslint/css version: v0.5.0
Node version: v23.9.0
npm version: v10.9.1
Operating System: win32 10.0.19045


### Which language are you using?

stylesheet

### What did you do?

<details>
<summary>Configuration</summary>

```js
import js from '@eslint/js'
import globals from 'globals'
import css from '@eslint/css'
import html from '@html-eslint/eslint-plugin'
import htmlParser from '@html-eslint/parser'
import importPlugin from 'eslint-plugin-import'
import json from '@eslint/json'
import regexp from 'eslint-plugin-regexp'
import stylisticJS from '@stylistic/eslint-plugin-js'
import yml from 'eslint-plugin-yml'

export default [
    { ignores: ['**/*.min.js', '**/package-lock.json'] },
    {
        files: ['**/*.{js,mjs}'],
        languageOptions: { ecmaVersion: 'latest', sourceType: 'script', globals: { ...globals.browser }},
        plugins: { 'import': importPlugin, 'js-styles': stylisticJS, regexp },
        rules: {
            ...js.configs.recommended.rules,
            ...importPlugin.flatConfigs.recommended.rules,
            ...regexp.configs['flat/recommended'].rules,
            'indent': 'off', 'no-unexpected-multiline': 'off', // allow whitespace anywhere
            'js-styles/no-trailing-spaces': 'error', // ...except at ends of lines
            'js-styles/max-len': ['error', { 'code': 120, // limit lines to 120 chars except if containing...
                'ignoreComments': true, 'ignoreStrings': true, // ...trailing/own-line comments, quoted strings...
                'ignoreTemplateLiterals': true, 'ignoreRegExpLiterals': true }], // ...or template/regex literals
            'js-styles/no-extra-semi': 'error', // disallow unnecessary semicolons
            'quotes': ['error', 'single', { 'allowTemplateLiterals': true }], // enforce single quotes except backticks to avoid escaping quotes
            'key-spacing': ['error', { 'beforeColon': false, 'afterColon': true }], // enforce spacing in object properties
            'comma-dangle': ['error', 'never'], // enforce no trailing commas in arrays or objects
            'no-async-promise-executor': 'off', // allow promise executor functions to be async (to accomodate await lines)
            'no-constant-condition': 'off', // allow constant conditions
            'no-empty': 'off', // allow empty blocks
            'no-inner-declarations': 'off', // allow function declarations anywhere
            'no-useless-escape': 'off', // allow all escape chars cause ESLint sucks at detecting truly useless ones
            'no-unused-vars': ['error', { 'caughtErrors': 'none' }] // allow unused named args in catch blocks
        }
    },
    { files: ['**/*.css'], language: 'css/css', ...css.configs.recommended },
    {
        files: ['**/*.html'], languageOptions: { parser: htmlParser }, plugins: { '@html-eslint': html },
        rules: {
            ...html.configs['flat/recommended'].rules,
            '@html-eslint/require-title': 'off', // allow missing title tags
            '@html-eslint/quotes': 'off', // allow unquoted attrs for compactness
            '@html-eslint/attrs-newline': 'off', // allow multi-attrs in single line
            '@html-eslint/require-img-alt': 'off', // allow img tags w/o alt attributes
            '@html-eslint/element-newline': ['error', { 'skip': ['html'] }] // allow multi-tags in single line
        }
    },
    { files: ['**/*.json'], language: 'json/json', ...json.configs.recommended },
    { files: ['**/*.mjs'], languageOptions: { sourceType: 'module' }},
    { files: ['**/*.{yaml,yml}'], ...yml.configs['flat/standard'][1] }
]
```
</details>

```css
...

/* Non-baseline features */
@supports (overflow: clip) { body { overflow: clip }}
@supports (user-select: none) { body, button, input, select, textarea { user-select: none }}
@supports (cursor: pointer) {
    .highlight:focus, .highlight:hover, .toggle-switch .track, .cjs-span { cursor: pointer }}
```


### What did you expect to happen?

No error on last line just like pre-update

### What actually happened?

![Image](https://github.com/user-attachments/assets/49dbf07d-bfe9-4ee6-821f-ffaf77504b3e)

### Link to Minimal Reproducible Example

https://github.com/adamlui/chatgptevo.com/actions/runs/13912623235/job/38929820287

### Participation

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

### Additional comments

The `highlight` it refers to is a class name so is supported in every browser (but even so it's part of `@supports` block so should still not error)

Pull request description:
fix: Don't check class names in require-baseline
<!--
    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?

Fixes incorrect flagging of `highlight` class name in `require-basline`

#### What changes did you make? (Give an overview)

- Updated `require-baseline` to skip class, type, ID, and attribute selectors
- Added test to verify the change

#### Related Issues

fixes #91

<!-- include tags like "fixes #123" or "refs #123" -->

#### Is there anything you'd like reviewers to focus on?

Ping @rviscomi can you take a look at this to make sure I fixed it correctly?

<!-- markdownlint-disable-file MD004 -->


Patch:
diff --git a/docs/rules/use-layers.md b/docs/rules/use-layers.md
index f396d04..7285c9d 100644
--- a/docs/rules/use-layers.md
+++ b/docs/rules/use-layers.md
@@ -91,7 +91,7 @@ When `allowUnnamedLayers` is set to `true`, the following code is **correct**:
 
 #### `layerNamePattern`
 
-The `layerNamePattern` is a regular expression string that allows you to validate the name of layers and prevent misspellings.
+The `layerNamePattern` is a regular expression string that allows you to validate the name of layers and prevent misspellings. This option supports period-separated layer names (e.g., `foo.bar`) as defined in [CSS Cascade and Inheritance Level 5](https://drafts.csswg.org/css-cascade-5/#layer-names).
 
 Here's an example of **incorrect** code:
 
@@ -106,8 +106,21 @@ Here's an example of **incorrect** code:
 		color: red;
 	}
 }
+
+/* unknown period-separated layer name */
+@layer theme.custom {
+	a {
+		color: red;
+	}
+}
 ```
 
+Each part of a period-separated layer name is validated individually against the pattern. For example, with `layerNamePattern: "^(reset|theme|base)$"`:
+
+- `theme.base` is valid (both parts match the pattern)
+- `theme.custom` is invalid (`custom` doesn't match the pattern)
+- `other.base` is invalid (`other` doesn't match the pattern)
+
 #### `requireImportLayers: false`
 
 When `requireImportLayers` is set to `false`, the following code is **correct**:
diff --git a/src/rules/require-baseline.js b/src/rules/require-baseline.js
index 7b74309..6d2f7fe 100644
--- a/src/rules/require-baseline.js
+++ b/src/rules/require-baseline.js
@@ -735,50 +735,48 @@ export default {
 				}
 			},
 
-			Selector(node) {
-				for (const child of node.children) {
-					const selector = child.name;
+			"PseudoClassSelector,PseudoElementSelector"(node) {
+				const selector = node.name;
 
-					if (!selectors.has(selector)) {
-						continue;
-					}
+				if (!selectors.has(selector)) {
+					return;
+				}
 
-					// if the selector has been tested in a @supports rule, don't check it
-					if (supportsRules.hasSelector(selector)) {
-						continue;
-					}
+				// if the selector has been tested in a @supports rule, don't check it
+				if (supportsRules.hasSelector(selector)) {
+					return;
+				}
 
-					const featureStatus = selectors.get(selector);
+				const featureStatus = selectors.get(selector);
 
-					if (!baselineAvailability.isSupported(featureStatus)) {
-						const loc = child.loc;
+				if (!baselineAvailability.isSupported(featureStatus)) {
+					const loc = node.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;
-						}
+					// some selectors are prefixed with the : or :: symbols
+					let prefixSymbolLength = 0;
+					if (node.type.startsWith("PseudoClass")) {
+						prefixSymbolLength = 1;
+					} else if (node.type.startsWith("PseudoElement")) {
+						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: baselineAvailability.availability,
+					context.report({
+						loc: {
+							start: loc.start,
+							end: {
+								line: loc.start.line,
+								column:
+									loc.start.column +
+									selector.length +
+									prefixSymbolLength,
 							},
-						});
-					}
+						},
+						messageId: "notBaselineSelector",
+						data: {
+							selector,
+							availability: baselineAvailability.availability,
+						},
+					});
 				}
 			},
 
diff --git a/src/rules/use-layers.js b/src/rules/use-layers.js
index 13aadb3..19b94f1 100644
--- a/src/rules/use-layers.js
+++ b/src/rules/use-layers.js
@@ -89,16 +89,39 @@ export default {
 					return;
 				}
 
-				if (!layerNameRegex.test(node.name)) {
-					context.report({
-						loc: node.loc,
-						messageId: "layerNameMismatch",
-						data: {
-							name: node.name,
-							pattern: options.layerNamePattern,
-						},
-					});
-				}
+				const parts = node.name.split(".");
+				let currentPos = 0;
+
+				parts.forEach((part, index) => {
+					if (!layerNameRegex.test(part)) {
+						const startColumn = node.loc.start.column + currentPos;
+						const endColumn = startColumn + part.length;
+
+						context.report({
+							loc: {
+								start: {
+									line: node.loc.start.line,
+									column: startColumn,
+								},
+								end: {
+									line: node.loc.start.line,
+									column: endColumn,
+								},
+							},
+							messageId: "layerNameMismatch",
+							data: {
+								name: part,
+								pattern: options.layerNamePattern,
+							},
+						});
+					}
+
+					currentPos += part.length;
+					// add 1 to account for the . symbol
+					if (index < parts.length - 1) {
+						currentPos += 1;
+					}
+				});
 			},
 
 			"Atrule[name=layer]"(node) {


Test patch:
diff --git a/tests/rules/require-baseline.test.js b/tests/rules/require-baseline.test.js
index e94137f..2a463db 100644
--- a/tests/rules/require-baseline.test.js
+++ b/tests/rules/require-baseline.test.js
@@ -62,6 +62,7 @@ ruleTester.run("require-baseline", rule, {
 		}`,
 		"div { cursor: pointer; }",
 		"pre { overflow: auto; }",
+		".highlight, #highlight, highlight { color: red }",
 		{
 			code: `@property --foo {
 				syntax: "*";
diff --git a/tests/rules/use-layers.test.js b/tests/rules/use-layers.test.js
index 6a99e7a..363500c 100644
--- a/tests/rules/use-layers.test.js
+++ b/tests/rules/use-layers.test.js
@@ -57,6 +57,22 @@ ruleTester.run("use-layers", rule, {
 			code: "@import 'foo.css';",
 			options: [{ requireImportLayers: false }],
 		},
+		{
+			code: "@layer foo.bar { a { color: red; } }",
+			options: [{ layerNamePattern: "^(foo|bar)$" }],
+		},
+		{
+			code: "@layer foo.bar.baz { a { color: red; } }",
+			options: [{ layerNamePattern: "^(foo|bar|baz)$" }],
+		},
+		{
+			code: "@import 'foo.css' layer(foo.bar);",
+			options: [{ layerNamePattern: "^(foo|bar)$" }],
+		},
+		{
+			code: "@layer foo, bar.baz, baz.qux;",
+			options: [{ layerNamePattern: "^(foo|bar|baz|qux)$" }],
+		},
 	],
 	invalid: [
 		{
@@ -263,5 +279,134 @@ ruleTester.run("use-layers", rule, {
 				},
 			],
 		},
+		{
+			code: "@layer foo.bar { a { color: red; } }",
+			options: [{ layerNamePattern: "bar" }],
+			errors: [
+				{
+					messageId: "layerNameMismatch",
+					data: {
+						name: "foo",
+						pattern: "bar",
+					},
+					line: 1,
+					column: 8,
+					endLine: 1,
+					endColumn: 11,
+				},
+			],
+		},
+		{
+			code: "@layer foo.baz { a { color: red; } }",
+			options: [{ layerNamePattern: "bar" }],
+			errors: [
+				{
+					messageId: "layerNameMismatch",
+					data: {
+						name: "foo",
+						pattern: "bar",
+					},
+					line: 1,
+					column: 8,
+					endLine: 1,
+					endColumn: 11,
+				},
+				{
+					messageId: "layerNameMismatch",
+					data: {
+						name: "baz",
+						pattern: "bar",
+					},
+					line: 1,
+					column: 12,
+					endLine: 1,
+					endColumn: 15,
+				},
+			],
+		},
+		{
+			code: "@layer foo.bar, baz.qux { a { color: red; } }",
+			options: [{ layerNamePattern: "bar" }],
+			errors: [
+				{
+					messageId: "layerNameMismatch",
+					data: {
+						name: "foo",
+						pattern: "bar",
+					},
+					line: 1,
+					column: 8,
+					endLine: 1,
+					endColumn: 11,
+				},
+				{
+					messageId: "layerNameMismatch",
+					data: {
+						name: "baz",
+						pattern: "bar",
+					},
+					line: 1,
+					column: 17,
+					endLine: 1,
+					endColumn: 20,
+				},
+				{
+					messageId: "layerNameMismatch",
+					data: {
+						name: "qux",
+						pattern: "bar",
+					},
+					line: 1,
+					column: 21,
+					endLine: 1,
+					endColumn: 24,
+				},
+			],
+		},
+		{
+			code: "@import 'style.css' layer(foo.bar);",
+			options: [{ layerNamePattern: "bar" }],
+			errors: [
+				{
+					messageId: "layerNameMismatch",
+					data: {
+						name: "foo",
+						pattern: "bar",
+					},
+					line: 1,
+					column: 27,
+					endLine: 1,
+					endColumn: 30,
+				},
+			],
+		},
+		{
+			code: "@import 'style.css' layer(foo.baz);",
+			options: [{ layerNamePattern: "bar" }],
+			errors: [
+				{
+					messageId: "layerNameMismatch",
+					data: {
+						name: "foo",
+						pattern: "bar",
+					},
+					line: 1,
+					column: 27,
+					endLine: 1,
+					endColumn: 30,
+				},
+				{
+					messageId: "layerNameMismatch",
+					data: {
+						name: "baz",
+						pattern: "bar",
+					},
+					line: 1,
+					column: 31,
+					endLine: 1,
+					endColumn: 34,
+				},
+			],
+		},
 	],
 });