Skip to content

Commit 54b7da5

Browse files
authored
fix: Add support period-separated layer names in use-layers rule (#92)
* fix: support period-separated layer names in use-layers rule * update docs
1 parent 61a9d46 commit 54b7da5

File tree

3 files changed

+192
-11
lines changed

3 files changed

+192
-11
lines changed

docs/rules/use-layers.md

+14-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ When `allowUnnamedLayers` is set to `true`, the following code is **correct**:
9191

9292
#### `layerNamePattern`
9393

94-
The `layerNamePattern` is a regular expression string that allows you to validate the name of layers and prevent misspellings.
94+
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).
9595

9696
Here's an example of **incorrect** code:
9797

@@ -106,8 +106,21 @@ Here's an example of **incorrect** code:
106106
color: red;
107107
}
108108
}
109+
110+
/* unknown period-separated layer name */
111+
@layer theme.custom {
112+
a {
113+
color: red;
114+
}
115+
}
109116
```
110117

118+
Each part of a period-separated layer name is validated individually against the pattern. For example, with `layerNamePattern: "^(reset|theme|base)$"`:
119+
120+
- `theme.base` is valid (both parts match the pattern)
121+
- `theme.custom` is invalid (`custom` doesn't match the pattern)
122+
- `other.base` is invalid (`other` doesn't match the pattern)
123+
111124
#### `requireImportLayers: false`
112125

113126
When `requireImportLayers` is set to `false`, the following code is **correct**:

src/rules/use-layers.js

+33-10
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,39 @@ export default {
8989
return;
9090
}
9191

92-
if (!layerNameRegex.test(node.name)) {
93-
context.report({
94-
loc: node.loc,
95-
messageId: "layerNameMismatch",
96-
data: {
97-
name: node.name,
98-
pattern: options.layerNamePattern,
99-
},
100-
});
101-
}
92+
const parts = node.name.split(".");
93+
let currentPos = 0;
94+
95+
parts.forEach((part, index) => {
96+
if (!layerNameRegex.test(part)) {
97+
const startColumn = node.loc.start.column + currentPos;
98+
const endColumn = startColumn + part.length;
99+
100+
context.report({
101+
loc: {
102+
start: {
103+
line: node.loc.start.line,
104+
column: startColumn,
105+
},
106+
end: {
107+
line: node.loc.start.line,
108+
column: endColumn,
109+
},
110+
},
111+
messageId: "layerNameMismatch",
112+
data: {
113+
name: part,
114+
pattern: options.layerNamePattern,
115+
},
116+
});
117+
}
118+
119+
currentPos += part.length;
120+
// add 1 to account for the . symbol
121+
if (index < parts.length - 1) {
122+
currentPos += 1;
123+
}
124+
});
102125
},
103126

104127
"Atrule[name=layer]"(node) {

tests/rules/use-layers.test.js

+145
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@ ruleTester.run("use-layers", rule, {
5757
code: "@import 'foo.css';",
5858
options: [{ requireImportLayers: false }],
5959
},
60+
{
61+
code: "@layer foo.bar { a { color: red; } }",
62+
options: [{ layerNamePattern: "^(foo|bar)$" }],
63+
},
64+
{
65+
code: "@layer foo.bar.baz { a { color: red; } }",
66+
options: [{ layerNamePattern: "^(foo|bar|baz)$" }],
67+
},
68+
{
69+
code: "@import 'foo.css' layer(foo.bar);",
70+
options: [{ layerNamePattern: "^(foo|bar)$" }],
71+
},
72+
{
73+
code: "@layer foo, bar.baz, baz.qux;",
74+
options: [{ layerNamePattern: "^(foo|bar|baz|qux)$" }],
75+
},
6076
],
6177
invalid: [
6278
{
@@ -263,5 +279,134 @@ ruleTester.run("use-layers", rule, {
263279
},
264280
],
265281
},
282+
{
283+
code: "@layer foo.bar { a { color: red; } }",
284+
options: [{ layerNamePattern: "bar" }],
285+
errors: [
286+
{
287+
messageId: "layerNameMismatch",
288+
data: {
289+
name: "foo",
290+
pattern: "bar",
291+
},
292+
line: 1,
293+
column: 8,
294+
endLine: 1,
295+
endColumn: 11,
296+
},
297+
],
298+
},
299+
{
300+
code: "@layer foo.baz { a { color: red; } }",
301+
options: [{ layerNamePattern: "bar" }],
302+
errors: [
303+
{
304+
messageId: "layerNameMismatch",
305+
data: {
306+
name: "foo",
307+
pattern: "bar",
308+
},
309+
line: 1,
310+
column: 8,
311+
endLine: 1,
312+
endColumn: 11,
313+
},
314+
{
315+
messageId: "layerNameMismatch",
316+
data: {
317+
name: "baz",
318+
pattern: "bar",
319+
},
320+
line: 1,
321+
column: 12,
322+
endLine: 1,
323+
endColumn: 15,
324+
},
325+
],
326+
},
327+
{
328+
code: "@layer foo.bar, baz.qux { a { color: red; } }",
329+
options: [{ layerNamePattern: "bar" }],
330+
errors: [
331+
{
332+
messageId: "layerNameMismatch",
333+
data: {
334+
name: "foo",
335+
pattern: "bar",
336+
},
337+
line: 1,
338+
column: 8,
339+
endLine: 1,
340+
endColumn: 11,
341+
},
342+
{
343+
messageId: "layerNameMismatch",
344+
data: {
345+
name: "baz",
346+
pattern: "bar",
347+
},
348+
line: 1,
349+
column: 17,
350+
endLine: 1,
351+
endColumn: 20,
352+
},
353+
{
354+
messageId: "layerNameMismatch",
355+
data: {
356+
name: "qux",
357+
pattern: "bar",
358+
},
359+
line: 1,
360+
column: 21,
361+
endLine: 1,
362+
endColumn: 24,
363+
},
364+
],
365+
},
366+
{
367+
code: "@import 'style.css' layer(foo.bar);",
368+
options: [{ layerNamePattern: "bar" }],
369+
errors: [
370+
{
371+
messageId: "layerNameMismatch",
372+
data: {
373+
name: "foo",
374+
pattern: "bar",
375+
},
376+
line: 1,
377+
column: 27,
378+
endLine: 1,
379+
endColumn: 30,
380+
},
381+
],
382+
},
383+
{
384+
code: "@import 'style.css' layer(foo.baz);",
385+
options: [{ layerNamePattern: "bar" }],
386+
errors: [
387+
{
388+
messageId: "layerNameMismatch",
389+
data: {
390+
name: "foo",
391+
pattern: "bar",
392+
},
393+
line: 1,
394+
column: 27,
395+
endLine: 1,
396+
endColumn: 30,
397+
},
398+
{
399+
messageId: "layerNameMismatch",
400+
data: {
401+
name: "baz",
402+
pattern: "bar",
403+
},
404+
line: 1,
405+
column: 31,
406+
endLine: 1,
407+
endColumn: 34,
408+
},
409+
],
410+
},
266411
],
267412
});

0 commit comments

Comments
 (0)