Repository: eslint/css (js)

Issue:
Bug: `use-layers` rule's `layerNamePattern` does not support period-separated layer names
### Environment

ESLint version: v9.22.0
@eslint/css version: v0.5.0
Node version: v22.9.0
npm version: 10.5.0
Operating System: macOS 14.7


### Which language are you using?

stylesheet

### What did you do?

Tested `css/use-layers` rule with `layerNamePattern` option.

<details>
<summary>Configuration</summary>

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

const eslintConfig = [
  {
    files: ["**/*.css"],
    language: "css/css",
    ...css.configs.recommended,
    rules: {
      "css/use-layers": ["error", { layerNamePattern: "^(reset|theme|base)$" }]
    },
  },
];

export default eslintConfig;
```
</details>

```css
@layer reset, base, theme;

@import url("reset.css") layer(base.theme);
@layer base.theme {
  a { color: red; }
}
```

### What did you expect to happen?

[Period-separated layer names](https://drafts.csswg.org/css-cascade-5/#layer-names) should be treated separately. In this case, `base` and `theme` should be handled as individual layers, and since both match the pattern, no warning should appear.

### What actually happened?

An error message appears:

```console
Expected layer name 'theme.base' to match pattern '^(reset|theme|base)$'
```

### Link to Minimal Reproducible Example

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

### Participation

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

### Additional comments

Reference to the spec: [CSS Cascade and Inheritance Level 5 - Layer Names](https://drafts.csswg.org/css-cascade-5/#layer-names)
> When multiple identifiers are concatenated with a period, this is a shorthand representing those layers nested in order.

Pull request description:
fix: Add support period-separated layer names in use-layers 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 period-separated layer names in the `use-layers` rule.  

#### What changes did you make? (Give an overview)

- Added support for period-separated layer names in the `use-layers` rule.  
- Added tests.
- Updated documentation.

#### Related Issues

<!-- include tags like "fixes #123" or "refs #123" -->
fixes #88 

#### Is there anything you'd like reviewers to focus on?

<!-- 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/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/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,
+				},
+			],
+		},
 	],
 });