-
-
Notifications
You must be signed in to change notification settings - Fork 19
feat: add new rule no-duplicate-keyframe-selectors
#143
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
57d184f
470e3a9
ae83968
53d4a68
9ac7fbc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -57,3 +57,5 @@ pnpm-lock.yaml | |
|
||
# Build | ||
dist/ | ||
|
||
test.css |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
# no-duplicate-keyframe-selectors | ||
|
||
Disallow duplicate selectors within keyframe blocks. | ||
|
||
## Background | ||
|
||
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. | ||
|
||
```css | ||
@keyframes test { | ||
0% { | ||
opacity: 0; | ||
} | ||
|
||
100% { | ||
opacity: 1; | ||
} | ||
} | ||
``` | ||
|
||
If a selector is repeated within the same @keyframes block, the last declaration wins, potentially causing unintentional overrides or confusion. | ||
|
||
## Rule Details | ||
|
||
This rule warns when it finds a keyframe block that contains duplicate selectors. | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
```css | ||
/* eslint css/no-duplicate-keyframe-selectors: "error" */ | ||
|
||
@keyframes test { | ||
0% { | ||
opacity: 0; | ||
} | ||
|
||
0% { | ||
opacity: 1; | ||
} | ||
} | ||
|
||
@keyframes test { | ||
from { | ||
opacity: 0; | ||
} | ||
|
||
from { | ||
opacity: 1; | ||
} | ||
} | ||
|
||
@keyframes test { | ||
from { | ||
opacity: 0; | ||
} | ||
|
||
from { | ||
opacity: 1; | ||
} | ||
} | ||
``` | ||
|
||
Examples of **correct** code for this rule: | ||
|
||
```css | ||
/* eslint css/no-duplicate-keyframe-selectors: "error" */ | ||
|
||
@keyframes test { | ||
0% { | ||
opacity: 0; | ||
} | ||
|
||
100% { | ||
opacity: 1; | ||
} | ||
} | ||
|
||
@keyframes test { | ||
from { | ||
opacity: 0; | ||
} | ||
|
||
to { | ||
opacity: 1; | ||
} | ||
} | ||
``` | ||
|
||
## When Not to Use It | ||
|
||
If you aren't concerned with duplicate selectors within keyframe blocks, you can safely disable this rule. | ||
|
||
## Prior Art | ||
|
||
- [`keyframe-block-no-duplicate-selectors`](https://stylelint.io/user-guide/rules/keyframe-block-no-duplicate-selectors/) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ import eslintConfigESLint from "eslint-config-eslint"; | |
import eslintPlugin from "eslint-plugin-eslint-plugin"; | ||
import json from "@eslint/json"; | ||
import { defineConfig, globalIgnores } from "eslint/config"; | ||
import css from "./src/index.js"; | ||
|
||
//----------------------------------------------------------------------------- | ||
// Helpers | ||
|
@@ -30,7 +31,7 @@ const eslintPluginTestsRecommendedConfig = | |
//----------------------------------------------------------------------------- | ||
|
||
export default defineConfig([ | ||
globalIgnores(["**/tests/fixtures/", "**/dist/"]), | ||
globalIgnores(["**/tests/fixtures/", "**/dist/", "test.css"]), | ||
|
||
...eslintConfigESLint.map(config => ({ | ||
files: ["**/*.js"], | ||
|
@@ -115,4 +116,9 @@ export default defineConfig([ | |
"eslint-plugin/test-case-shorthand-strings": "error", | ||
}, | ||
}, | ||
{ | ||
files: ["**/*.css"], | ||
language: "css/css", | ||
...css.configs.recommended, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we use |
||
}, | ||
]); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
/** | ||
* @fileoverview Rule to disallow duplicate selectors within keyframe blocks. | ||
* @author Nitin Kumar | ||
*/ | ||
|
||
//----------------------------------------------------------------------------- | ||
// Type Definitions | ||
//----------------------------------------------------------------------------- | ||
|
||
/** | ||
* @import { CSSRuleDefinition } from "../types.js" | ||
* @typedef {"duplicateKeyframeSelector"} DuplicateKeyframeSelectorMessageIds | ||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: DuplicateKeyframeSelectorMessageIds }>} DuplicateKeyframeSelectorRuleDefinition | ||
*/ | ||
|
||
//----------------------------------------------------------------------------- | ||
// Rule Definition | ||
//----------------------------------------------------------------------------- | ||
|
||
/** @type {DuplicateKeyframeSelectorRuleDefinition} */ | ||
export default { | ||
meta: { | ||
type: "problem", | ||
|
||
docs: { | ||
description: "Disallow duplicate selectors within keyframe blocks", | ||
recommended: true, | ||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-duplicate-keyframe-selectors.md", | ||
}, | ||
|
||
messages: { | ||
duplicateKeyframeSelector: | ||
"Unexpected duplicate selector '{{selector}}' found within keyframe block.", | ||
}, | ||
}, | ||
|
||
create(context) { | ||
return { | ||
Atrule(node) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is traversing the at-rule itself instead of letting the rule do so. It would be more efficient to do See https://github.com/eslint/css/blob/main/src/rules/use-layers.js as an example. |
||
if (node.name === "keyframes" && node.block) { | ||
const selectorNodes = node.block.children.map(child => { | ||
const selector = | ||
// eslint-disable-next-line dot-notation -- bracket notation to avoid type error even though it's valid | ||
child["prelude"].children[0].children[0]; | ||
let value; | ||
if (selector.type === "Percentage") { | ||
value = `${selector.value}%`; | ||
} else if (selector.type === "TypeSelector") { | ||
value = selector.name.toLowerCase(); | ||
} else { | ||
value = selector.value; | ||
} | ||
return { value, loc: selector.loc }; | ||
}); | ||
|
||
const seen = new Map(); | ||
selectorNodes.forEach((selectorNode, index) => { | ||
if (seen.has(selectorNode.value)) { | ||
context.report({ | ||
loc: selectorNode.loc, | ||
messageId: "duplicateKeyframeSelector", | ||
data: { | ||
selector: selectorNode.value, | ||
}, | ||
}); | ||
} else { | ||
seen.set(selectorNode.value, index); | ||
} | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#143 (comment)