diff --git a/docs/rules/no-duplicate-keyframe-selectors.md b/docs/rules/no-duplicate-keyframe-selectors.md index 37a8ce32..23045b17 100644 --- a/docs/rules/no-duplicate-keyframe-selectors.md +++ b/docs/rules/no-duplicate-keyframe-selectors.md @@ -39,6 +39,17 @@ Examples of **incorrect** code for this rule: } } +@keyframes test { + 0%, + 50% { + opacity: 0; + } + + 50% { + opacity: 1; + } +} + @keyframes test { from { opacity: 0; @@ -97,6 +108,21 @@ Examples of **correct** code for this rule: } } +@keyframes test { + 0% { + opacity: 0; + } + + 30%, + 60% { + opacity: 0.5; + } + + 100% { + opacity: 1; + } +} + @keyframes test { from { opacity: 0; diff --git a/src/rules/no-duplicate-keyframe-selectors.js b/src/rules/no-duplicate-keyframe-selectors.js index a69cc9d4..b68b7379 100644 --- a/src/rules/no-duplicate-keyframe-selectors.js +++ b/src/rules/no-duplicate-keyframe-selectors.js @@ -62,37 +62,57 @@ export default /** @satisfies {DuplicateKeyframeSelectorRuleDefinition} */ ({ } // @ts-ignore - children is a valid property for prelude - const selector = node.prelude.children[0]; + const selectors = node.prelude.children; const value = []; - selector.children.forEach(component => { - if (component.type === "Percentage") { - value.push(`${component.value}%`); - } else if (component.type === "TypeSelector") { - value.push(component.name.toLowerCase()); + selectors.forEach(selector => { + const component = selector.children[0]; + const componentType = component.type; + + if (selector.children.length === 1) { + if (componentType === "Percentage") { + value.push(`${component.value}%`); + } else if (componentType === "TypeSelector") { + value.push(component.name.toLowerCase()); + } + } else { + if ( + selector.children.some( + child => child.type === "Combinator", + ) + ) { + if ( + componentType === "TypeSelector" && + selector.children[1].type === "Combinator" && + selector.children[2].type === "Percentage" + ) { + value.push( + `${component.name.toLowerCase()} ${selector.children[2].value}%`, + ); + } + } } }); - const selectorValue = value.join(" "); - const key = value - .map( - selectorPart => - keyframeSelectorAliases.get(selectorPart) ?? - selectorPart, - ) - .join(" "); - - if (seen.has(key)) { - context.report({ - loc: selector.loc, - messageId: "duplicateKeyframeSelector", - data: { - selector: selectorValue, - }, - }); - } else { - seen.add(key); - } + const keys = value.map( + selectorPart => + keyframeSelectorAliases.get(selectorPart) ?? + selectorPart, + ); + + keys.forEach((key, i) => { + if (seen.has(key)) { + context.report({ + loc: selectors[i].loc, + messageId: "duplicateKeyframeSelector", + data: { + selector: value[i], + }, + }); + } else { + seen.add(key); + } + }); }, }; }, diff --git a/tests/rules/no-duplicate-keyframe-selectors.test.js b/tests/rules/no-duplicate-keyframe-selectors.test.js index 51ed68e8..272b1a6e 100644 --- a/tests/rules/no-duplicate-keyframe-selectors.test.js +++ b/tests/rules/no-duplicate-keyframe-selectors.test.js @@ -49,11 +49,25 @@ ruleTester.run("no-duplicate-keyframe-selectors", rule, { 0% { opacity: 0; } 50% { opacity: 0.5; } 100% { opacity: 1; } + }`, + dedent`@keyframes test { + 0%, 20% { opacity: 0; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } + }`, + dedent`@keyframes test { + 0%, 10%, 20% { opacity: 0; } + 50%, 100% { opacity: 1; } }`, dedent`@keyframes test { from { opacity: 0; } 50% { opacity: 0.5; } to { opacity: 1; } + }`, + dedent`@keyframes test { + from, 30% { opacity: 0; } + 50% { opacity: 0.5; } + to { opacity: 1; } }`, dedent`@keyframes test { }`, @@ -82,6 +96,16 @@ ruleTester.run("no-duplicate-keyframe-selectors", rule, { entry 100% { opacity: 1; } exit 0% { opacity: 1; } exit 100% { opacity: 0; } + }`, + dedent`@keyframes test { + entry 0%, 30% { opacity: 0; } + entry 100% { opacity: 1; } + exit 0%, 50% { opacity: 1; } + exit 100% { opacity: 0; } + }`, + dedent`@keyframes test { + entry 0%, 0% { opacity: 0; } + exit 100%, 100% { opacity: 1; } }`, ], invalid: [ @@ -352,6 +376,85 @@ ruleTester.run("no-duplicate-keyframe-selectors", rule, { }, ], }, + { + code: dedent`@KEYFRAMES test { + 0% { opacity: 0; } + 0%, 50% { opacity: 1; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "0%" }, + line: 3, + column: 5, + endLine: 3, + endColumn: 7, + }, + ], + }, + { + code: dedent`@KEYFRAMES test { + 0%, 50% { opacity: 0; } + 50% { opacity: 1; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "50%" }, + line: 3, + column: 5, + endLine: 3, + endColumn: 8, + }, + ], + }, + { + code: dedent`@KEYFRAMES test { + 50% { opacity: 0; } + 0%, 50% { opacity: 1; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "50%" }, + line: 3, + column: 9, + endLine: 3, + endColumn: 12, + }, + ], + }, + { + code: dedent`@KEYFRAMES test { + 0%, 50% { opacity: 0; } + 70%, 100%, 0% { opacity: 1; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "0%" }, + line: 3, + column: 16, + endLine: 3, + endColumn: 18, + }, + ], + }, + { + code: dedent`@KEYFRAMES test { + 0%, 0% { top: 0; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "0%" }, + line: 2, + column: 9, + endLine: 2, + endColumn: 11, + }, + ], + }, { code: dedent`@Keyframes test { 0% { @@ -466,6 +569,117 @@ ruleTester.run("no-duplicate-keyframe-selectors", rule, { }, ], }, + { + code: dedent`@keyframes test { + entry 0% { opacity: 0; } + entry 0%, 50% { opacity: 1; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "entry 0%" }, + line: 3, + column: 5, + endLine: 3, + endColumn: 13, + }, + ], + }, + { + code: dedent`@keyframes test { + entry 0% { opacity: 0; } + 50%, entry 0% { opacity: 1; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "entry 0%" }, + line: 3, + column: 10, + endLine: 3, + endColumn: 18, + }, + ], + }, + { + code: dedent`@keyframes test { + entry 0% { opacity: 0; } + 50%, 100%, entry 0% { opacity: 1; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "entry 0%" }, + line: 3, + column: 16, + endLine: 3, + endColumn: 24, + }, + ], + }, + { + code: dedent`@keyframes test { + entry 0%, Entry 0% { opacity: 0; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "entry 0%" }, + line: 2, + column: 15, + endLine: 2, + endColumn: 23, + }, + ], + }, + { + code: dedent`@keyframes test { + entry 0%, exit 100% { opacity: 0; } + exit 100% { opacity: 1; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "exit 100%" }, + line: 3, + column: 5, + endLine: 3, + endColumn: 14, + }, + ], + }, + { + code: dedent`@keyframes test { + entry 0%, exit 100% { opacity: 0; } + 50%, 100%, exit 100% { opacity: 1; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "exit 100%" }, + line: 3, + column: 16, + endLine: 3, + endColumn: 25, + }, + ], + }, + { + code: dedent`@keyframes test { + entry 0%, Exit 100% { opacity: 0; } + exit 100% { opacity: 1; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "exit 100%" }, + line: 3, + column: 5, + endLine: 3, + endColumn: 14, + }, + ], + }, { code: dedent`@keyframes test { from { opacity: 0; } @@ -544,5 +758,131 @@ ruleTester.run("no-duplicate-keyframe-selectors", rule, { }, ], }, + { + code: dedent`@keyframes test { + from, 0% { opacity: 0; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "0%" }, + line: 2, + column: 11, + endLine: 2, + endColumn: 13, + }, + ], + }, + { + code: dedent`@keyframes test { + 0%, from { opacity: 0; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "from" }, + line: 2, + column: 9, + endLine: 2, + endColumn: 13, + }, + ], + }, + { + code: dedent`@keyframes test { + from { opacity: 0; } + 0%, 50% { opacity: 1; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "0%" }, + line: 3, + column: 5, + endLine: 3, + endColumn: 7, + }, + ], + }, + { + code: dedent`@keyframes test { + from { opacity: 0; } + 50%, 100%, 0% { opacity: 1; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "0%" }, + line: 3, + column: 16, + endLine: 3, + endColumn: 18, + }, + ], + }, + { + code: dedent`@keyframes test { + from { opacity: 0; } + to, 100% { opacity: 1; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "100%" }, + line: 3, + column: 9, + endLine: 3, + endColumn: 13, + }, + ], + }, + { + code: dedent`@keyframes test { + from, 100% { opacity: 0; } + to { opacity: 1; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "to" }, + line: 3, + column: 5, + endLine: 3, + endColumn: 7, + }, + ], + }, + { + code: dedent`@keyframes test { + from { opacity: 0; } + 100%, to { opacity: 1; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "to" }, + line: 3, + column: 11, + endLine: 3, + endColumn: 13, + }, + ], + }, + { + code: dedent`@keyframes test { + from { opacity: 0; } + 50%, 100%, to { opacity: 1; } + }`, + errors: [ + { + messageId: "duplicateKeyframeSelector", + data: { selector: "to" }, + line: 3, + column: 16, + endLine: 3, + endColumn: 18, + }, + ], + }, ], });