Skip to content

Commit dfe9c05

Browse files
authored
feat: add new rule no-duplicate-keyframe-selectors (#143)
* feat: add new rule `no-duplicate-keyframe-selectors` * docs: update * chore: refactor * chore: refactor * chore: update eslint config for css files * refactor: rule logic * refactor: improve traversing logic
1 parent 1db0b1a commit dfe9c05

File tree

6 files changed

+449
-14
lines changed

6 files changed

+449
-14
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,6 @@ pnpm-lock.yaml
5757

5858
# Build
5959
dist/
60+
6061
src/build
62+
test.css

README.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,20 @@ export default defineConfig([
6565

6666
<!-- Rule Table Start -->
6767

68-
| **Rule Name** | **Description** | **Recommended** |
69-
| :----------------------------------------------------------------------------- | :------------------------------------- | :-------------: |
70-
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
71-
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
72-
| [`no-important`](./docs/rules/no-important.md) | Disallow !important flags | yes |
73-
| [`no-invalid-at-rule-placement`](./docs/rules/no-invalid-at-rule-placement.md) | Disallow invalid placement of at-rules | yes |
74-
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
75-
| [`no-invalid-named-grid-areas`](./docs/rules/no-invalid-named-grid-areas.md) | Disallow invalid named grid areas | yes |
76-
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
77-
| [`prefer-logical-properties`](./docs/rules/prefer-logical-properties.md) | Enforce the use of logical properties | no |
78-
| [`relative-font-units`](./docs/rules/relative-font-units.md) | Enforce the use of relative font units | no |
79-
| [`use-baseline`](./docs/rules/use-baseline.md) | Enforce the use of baseline features | yes |
80-
| [`use-layers`](./docs/rules/use-layers.md) | Require use of layers | no |
68+
| **Rule Name** | **Description** | **Recommended** |
69+
| :----------------------------------------------------------------------------------- | :-------------------------------------------------- | :-------------: |
70+
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
71+
| [`no-duplicate-keyframe-selectors`](./docs/rules/no-duplicate-keyframe-selectors.md) | Disallow duplicate selectors within keyframe blocks | yes |
72+
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
73+
| [`no-important`](./docs/rules/no-important.md) | Disallow !important flags | yes |
74+
| [`no-invalid-at-rule-placement`](./docs/rules/no-invalid-at-rule-placement.md) | Disallow invalid placement of at-rules | yes |
75+
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
76+
| [`no-invalid-named-grid-areas`](./docs/rules/no-invalid-named-grid-areas.md) | Disallow invalid named grid areas | yes |
77+
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
78+
| [`prefer-logical-properties`](./docs/rules/prefer-logical-properties.md) | Enforce the use of logical properties | no |
79+
| [`relative-font-units`](./docs/rules/relative-font-units.md) | Enforce the use of relative font units | no |
80+
| [`use-baseline`](./docs/rules/use-baseline.md) | Enforce the use of baseline features | yes |
81+
| [`use-layers`](./docs/rules/use-layers.md) | Require use of layers | no |
8182

8283
<!-- Rule Table End -->
8384

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# no-duplicate-keyframe-selectors
2+
3+
Disallow duplicate selectors within keyframe blocks.
4+
5+
## Background
6+
7+
The [`@keyframes` at-rule](https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes) in CSS defines intermediate steps in an animation sequence. Each keyframe selector (like `0%`, `50%`, `100%`, `from`, or `to`) represents a point in the animation timeline and contains styles to apply at that point.
8+
9+
```css
10+
@keyframes test {
11+
0% {
12+
opacity: 0;
13+
}
14+
15+
100% {
16+
opacity: 1;
17+
}
18+
}
19+
```
20+
21+
If a selector is repeated within the same @keyframes block, the last declaration wins, potentially causing unintentional overrides or confusion.
22+
23+
## Rule Details
24+
25+
This rule warns when it finds a keyframe block that contains duplicate selectors.
26+
27+
Examples of **incorrect** code for this rule:
28+
29+
```css
30+
/* eslint css/no-duplicate-keyframe-selectors: "error" */
31+
32+
@keyframes test {
33+
0% {
34+
opacity: 0;
35+
}
36+
37+
0% {
38+
opacity: 1;
39+
}
40+
}
41+
42+
@keyframes test {
43+
from {
44+
opacity: 0;
45+
}
46+
47+
from {
48+
opacity: 1;
49+
}
50+
}
51+
52+
@keyframes test {
53+
from {
54+
opacity: 0;
55+
}
56+
57+
from {
58+
opacity: 1;
59+
}
60+
}
61+
```
62+
63+
Examples of **correct** code for this rule:
64+
65+
```css
66+
/* eslint css/no-duplicate-keyframe-selectors: "error" */
67+
68+
@keyframes test {
69+
0% {
70+
opacity: 0;
71+
}
72+
73+
100% {
74+
opacity: 1;
75+
}
76+
}
77+
78+
@keyframes test {
79+
from {
80+
opacity: 0;
81+
}
82+
83+
to {
84+
opacity: 1;
85+
}
86+
}
87+
```
88+
89+
## When Not to Use It
90+
91+
If you aren't concerned with duplicate selectors within keyframe blocks, you can safely disable this rule.
92+
93+
## Prior Art
94+
95+
- [`keyframe-block-no-duplicate-selectors`](https://stylelint.io/user-guide/rules/keyframe-block-no-duplicate-selectors/)

eslint.config.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import eslintConfigESLint from "eslint-config-eslint";
1111
import eslintPlugin from "eslint-plugin-eslint-plugin";
1212
import json from "@eslint/json";
1313
import { defineConfig, globalIgnores } from "eslint/config";
14+
import css from "./src/index.js";
1415

1516
//-----------------------------------------------------------------------------
1617
// Helpers
@@ -30,7 +31,7 @@ const eslintPluginTestsRecommendedConfig =
3031
//-----------------------------------------------------------------------------
3132

3233
export default defineConfig([
33-
globalIgnores(["**/tests/fixtures/", "**/dist/"]),
34+
globalIgnores(["**/tests/fixtures/", "**/dist/", "test.css"]),
3435

3536
...eslintConfigESLint.map(config => ({
3637
files: ["**/*.js"],
@@ -115,6 +116,12 @@ export default defineConfig([
115116
"eslint-plugin/test-case-shorthand-strings": "error",
116117
},
117118
},
119+
{
120+
files: ["**/*.css"],
121+
language: "css/css",
122+
plugins: { css },
123+
extends: ["css/recommended"],
124+
},
118125
{
119126
files: ["tools/**/*.js"],
120127
rules: {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* @fileoverview Rule to disallow duplicate selectors within keyframe blocks.
3+
* @author Nitin Kumar
4+
*/
5+
6+
//-----------------------------------------------------------------------------
7+
// Type Definitions
8+
//-----------------------------------------------------------------------------
9+
10+
/**
11+
* @import { CSSRuleDefinition } from "../types.js"
12+
* @typedef {"duplicateKeyframeSelector"} DuplicateKeyframeSelectorMessageIds
13+
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: DuplicateKeyframeSelectorMessageIds }>} DuplicateKeyframeSelectorRuleDefinition
14+
*/
15+
16+
//-----------------------------------------------------------------------------
17+
// Rule Definition
18+
//-----------------------------------------------------------------------------
19+
20+
/** @type {DuplicateKeyframeSelectorRuleDefinition} */
21+
export default {
22+
meta: {
23+
type: "problem",
24+
25+
docs: {
26+
description: "Disallow duplicate selectors within keyframe blocks",
27+
recommended: true,
28+
url: "https://github.com/eslint/css/blob/main/docs/rules/no-duplicate-keyframe-selectors.md",
29+
},
30+
31+
messages: {
32+
duplicateKeyframeSelector:
33+
"Unexpected duplicate selector '{{selector}}' found within keyframe block.",
34+
},
35+
},
36+
37+
create(context) {
38+
let insideKeyframes = false;
39+
const seen = new Map();
40+
41+
return {
42+
"Atrule[name=keyframes]"() {
43+
insideKeyframes = true;
44+
seen.clear();
45+
},
46+
47+
"Atrule[name=keyframes]:exit"() {
48+
insideKeyframes = false;
49+
},
50+
51+
Rule(node) {
52+
if (!insideKeyframes) {
53+
return;
54+
}
55+
56+
// @ts-ignore - children is a valid property for prelude
57+
const selector = node.prelude.children[0].children[0];
58+
let value;
59+
if (selector.type === "Percentage") {
60+
value = `${selector.value}%`;
61+
} else if (selector.type === "TypeSelector") {
62+
value = selector.name.toLowerCase();
63+
} else {
64+
value = selector.value;
65+
}
66+
67+
if (seen.has(value)) {
68+
context.report({
69+
loc: selector.loc,
70+
messageId: "duplicateKeyframeSelector",
71+
data: {
72+
selector: value,
73+
},
74+
});
75+
} else {
76+
seen.set(value, true);
77+
}
78+
},
79+
};
80+
},
81+
};

0 commit comments

Comments
 (0)