Skip to content

Commit 0681f90

Browse files
authored
fix(consistent-selector-style): Fixed detections of repeated elements such as in {#each} (#1231)
1 parent 04ca0d1 commit 0681f90

File tree

7 files changed

+130
-8
lines changed

7 files changed

+130
-8
lines changed

.changeset/fast-birds-crash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-svelte': patch
3+
---
4+
5+
fix(consistent-selector-style): Fixed detections of repeated elements such as in {#each}

packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
extractExpressionSuffixLiteral
1313
} from '../utils/expression-affixes.js';
1414
import { createRule } from '../utils/index.js';
15+
import { ElementOccurenceCount, elementOccurrenceCount } from '../utils/element-occurences.js';
1516

1617
interface Selections {
1718
exact: Map<string, AST.SvelteHTMLElement[]>;
@@ -143,7 +144,7 @@ export default createRule('consistent-selector-style', {
143144
if (styleValue === 'class') {
144145
return;
145146
}
146-
if (styleValue === 'id' && canUseIdSelector(selection)) {
147+
if (styleValue === 'id' && canUseIdSelector(selection.map(([elem]) => elem))) {
147148
context.report({
148149
messageId: 'classShouldBeId',
149150
loc: styleSelectorNodeLoc(node) as AST.SourceLocation
@@ -285,11 +286,13 @@ function addToArrayMap<T>(
285286
/**
286287
* Finds all nodes in selections that could be matched by key
287288
*/
288-
function matchSelection(selections: Selections, key: string): AST.SvelteHTMLElement[] {
289-
const selection = selections.exact.get(key) ?? [];
289+
function matchSelection(selections: Selections, key: string): [AST.SvelteHTMLElement, boolean][] {
290+
const selection = (selections.exact.get(key) ?? []).map<[AST.SvelteHTMLElement, boolean]>(
291+
(elem) => [elem, true]
292+
);
290293
selections.affixes.forEach((nodes, [prefix, suffix]) => {
291294
if ((prefix === null || key.startsWith(prefix)) && (suffix === null || key.endsWith(suffix))) {
292-
selection.push(...nodes);
295+
selection.push(...nodes.map<[AST.SvelteHTMLElement, boolean]>((elem) => [elem, false]));
293296
}
294297
});
295298
return selection;
@@ -299,26 +302,43 @@ function matchSelection(selections: Selections, key: string): AST.SvelteHTMLElem
299302
* Checks whether a given selection could be obtained using an ID selector
300303
*/
301304
function canUseIdSelector(selection: AST.SvelteHTMLElement[]): boolean {
302-
return selection.length <= 1;
305+
return (
306+
selection.length === 0 ||
307+
(selection.length === 1 &&
308+
elementOccurrenceCount(selection[0]) !== ElementOccurenceCount.ZeroToInf)
309+
);
303310
}
304311

305312
/**
306313
* Checks whether a given selection could be obtained using a type selector
307314
*/
308315
function canUseTypeSelector(
309-
selection: AST.SvelteHTMLElement[],
316+
selection: [AST.SvelteHTMLElement, boolean][],
310317
typeSelections: Map<string, AST.SvelteHTMLElement[]>
311318
): boolean {
312-
const types = new Set(selection.map((node) => node.name.name));
319+
const types = new Set(selection.map(([node]) => node.name.name));
313320
if (types.size > 1) {
314321
return false;
315322
}
316323
if (types.size < 1) {
317324
return true;
318325
}
326+
if (
327+
selection.some(
328+
([elem, exact]) => !exact && elementOccurrenceCount(elem) === ElementOccurenceCount.ZeroToInf
329+
)
330+
) {
331+
return false;
332+
}
319333
const type = [...types][0];
320334
const typeSelection = typeSelections.get(type);
321-
return typeSelection !== undefined && arrayEquals(typeSelection, selection);
335+
return (
336+
typeSelection !== undefined &&
337+
arrayEquals(
338+
typeSelection,
339+
selection.map(([elem]) => elem)
340+
)
341+
);
322342
}
323343

324344
/**
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { AST } from 'svelte-eslint-parser';
2+
3+
export enum ElementOccurenceCount {
4+
ZeroOrOne,
5+
One,
6+
ZeroToInf
7+
}
8+
9+
function multiplyCounts(
10+
left: ElementOccurenceCount,
11+
right: ElementOccurenceCount
12+
): ElementOccurenceCount {
13+
if (left === ElementOccurenceCount.One) {
14+
return right;
15+
}
16+
if (right === ElementOccurenceCount.One) {
17+
return left;
18+
}
19+
if (left === right) {
20+
return left;
21+
}
22+
return ElementOccurenceCount.ZeroToInf;
23+
}
24+
25+
function childElementOccurenceCount(parent: AST.SvelteHTMLNode | null): ElementOccurenceCount {
26+
if (parent === null) {
27+
return ElementOccurenceCount.One;
28+
}
29+
if (
30+
[
31+
'SvelteIfBlock',
32+
'SvelteElseBlock',
33+
'SvelteAwaitBlock',
34+
'SvelteAwaitPendingBlock',
35+
'SvelteAwaitThenBlock',
36+
'SvelteAwaitCatchBlock'
37+
].includes(parent.type)
38+
) {
39+
return ElementOccurenceCount.ZeroOrOne;
40+
}
41+
if (
42+
['SvelteEachBlock', 'SvelteSnippetBlock'].includes(parent.type) ||
43+
(parent.type === 'SvelteElement' && parent.kind === 'component')
44+
) {
45+
return ElementOccurenceCount.ZeroToInf;
46+
}
47+
return ElementOccurenceCount.One;
48+
}
49+
50+
export function elementOccurrenceCount(element: AST.SvelteHTMLNode): ElementOccurenceCount {
51+
const parentCount =
52+
element.parent !== null ? elementOccurrenceCount(element.parent) : ElementOccurenceCount.One;
53+
const parentChildCount = childElementOccurenceCount(element.parent);
54+
return multiplyCounts(parentCount, parentChildCount);
55+
}

packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/class01-input.svelte

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010

1111
<b class:conditional={true}>Text 3</b>
1212

13+
{#each ["one", "two"] as iter}
14+
<span class="iterated-each">{iter}</span>
15+
{/each}
16+
17+
<CustomComponent>
18+
<span class="iterated-component">Text 5</span>
19+
</CustomComponent>
20+
1321
<style>
1422
.link {
1523
color: red;
@@ -38,4 +46,12 @@
3846
.conditional {
3947
color: red;
4048
}
49+
50+
.iterated-each {
51+
color: red;
52+
}
53+
54+
.iterated-component {
55+
color: red;
56+
}
4157
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"svelte": ">=5.0.0"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<span>Outside</span>
2+
3+
{#snippet iterated()}
4+
<span class="iterated-snippet">Text 4</span>
5+
{/snippet}
6+
7+
<style>
8+
.iterated-snippet {
9+
color: red;
10+
}
11+
</style>

packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-suffix01-input.svelte

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919

2020
<a class={derived}>Click me four!</a>
2121

22+
{#each ["one", "two"] as count}
23+
<b class={"bold-" + count}>Bold in each</b>
24+
{/each}
25+
2226
<style>
2327
.foo-link-one {
2428
color: red;
@@ -31,4 +35,12 @@
3135
.foo-link-three {
3236
color: red;
3337
}
38+
39+
.bold-one {
40+
color: red;
41+
}
42+
43+
.bold-two {
44+
color: blue;
45+
}
3446
</style>

0 commit comments

Comments
 (0)