diff --git a/.changeset/fast-birds-crash.md b/.changeset/fast-birds-crash.md new file mode 100644 index 000000000..0e61e514c --- /dev/null +++ b/.changeset/fast-birds-crash.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': patch +--- + +fix(consistent-selector-style): Fixed detections of repeated elements such as in {#each} diff --git a/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts b/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts index 1f64ee0da..0864dc6a9 100644 --- a/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts +++ b/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts @@ -12,6 +12,7 @@ import { extractExpressionSuffixLiteral } from '../utils/expression-affixes.js'; import { createRule } from '../utils/index.js'; +import { ElementOccurenceCount, elementOccurrenceCount } from '../utils/element-occurences.js'; interface Selections { exact: Map; @@ -143,7 +144,7 @@ export default createRule('consistent-selector-style', { if (styleValue === 'class') { return; } - if (styleValue === 'id' && canUseIdSelector(selection)) { + if (styleValue === 'id' && canUseIdSelector(selection.map(([elem]) => elem))) { context.report({ messageId: 'classShouldBeId', loc: styleSelectorNodeLoc(node) as AST.SourceLocation @@ -285,11 +286,13 @@ function addToArrayMap( /** * Finds all nodes in selections that could be matched by key */ -function matchSelection(selections: Selections, key: string): AST.SvelteHTMLElement[] { - const selection = selections.exact.get(key) ?? []; +function matchSelection(selections: Selections, key: string): [AST.SvelteHTMLElement, boolean][] { + const selection = (selections.exact.get(key) ?? []).map<[AST.SvelteHTMLElement, boolean]>( + (elem) => [elem, true] + ); selections.affixes.forEach((nodes, [prefix, suffix]) => { if ((prefix === null || key.startsWith(prefix)) && (suffix === null || key.endsWith(suffix))) { - selection.push(...nodes); + selection.push(...nodes.map<[AST.SvelteHTMLElement, boolean]>((elem) => [elem, false])); } }); return selection; @@ -299,26 +302,43 @@ function matchSelection(selections: Selections, key: string): AST.SvelteHTMLElem * Checks whether a given selection could be obtained using an ID selector */ function canUseIdSelector(selection: AST.SvelteHTMLElement[]): boolean { - return selection.length <= 1; + return ( + selection.length === 0 || + (selection.length === 1 && + elementOccurrenceCount(selection[0]) !== ElementOccurenceCount.ZeroToInf) + ); } /** * Checks whether a given selection could be obtained using a type selector */ function canUseTypeSelector( - selection: AST.SvelteHTMLElement[], + selection: [AST.SvelteHTMLElement, boolean][], typeSelections: Map ): boolean { - const types = new Set(selection.map((node) => node.name.name)); + const types = new Set(selection.map(([node]) => node.name.name)); if (types.size > 1) { return false; } if (types.size < 1) { return true; } + if ( + selection.some( + ([elem, exact]) => !exact && elementOccurrenceCount(elem) === ElementOccurenceCount.ZeroToInf + ) + ) { + return false; + } const type = [...types][0]; const typeSelection = typeSelections.get(type); - return typeSelection !== undefined && arrayEquals(typeSelection, selection); + return ( + typeSelection !== undefined && + arrayEquals( + typeSelection, + selection.map(([elem]) => elem) + ) + ); } /** diff --git a/packages/eslint-plugin-svelte/src/utils/element-occurences.ts b/packages/eslint-plugin-svelte/src/utils/element-occurences.ts new file mode 100644 index 000000000..6bc30fa06 --- /dev/null +++ b/packages/eslint-plugin-svelte/src/utils/element-occurences.ts @@ -0,0 +1,55 @@ +import type { AST } from 'svelte-eslint-parser'; + +export enum ElementOccurenceCount { + ZeroOrOne, + One, + ZeroToInf +} + +function multiplyCounts( + left: ElementOccurenceCount, + right: ElementOccurenceCount +): ElementOccurenceCount { + if (left === ElementOccurenceCount.One) { + return right; + } + if (right === ElementOccurenceCount.One) { + return left; + } + if (left === right) { + return left; + } + return ElementOccurenceCount.ZeroToInf; +} + +function childElementOccurenceCount(parent: AST.SvelteHTMLNode | null): ElementOccurenceCount { + if (parent === null) { + return ElementOccurenceCount.One; + } + if ( + [ + 'SvelteIfBlock', + 'SvelteElseBlock', + 'SvelteAwaitBlock', + 'SvelteAwaitPendingBlock', + 'SvelteAwaitThenBlock', + 'SvelteAwaitCatchBlock' + ].includes(parent.type) + ) { + return ElementOccurenceCount.ZeroOrOne; + } + if ( + ['SvelteEachBlock', 'SvelteSnippetBlock'].includes(parent.type) || + (parent.type === 'SvelteElement' && parent.kind === 'component') + ) { + return ElementOccurenceCount.ZeroToInf; + } + return ElementOccurenceCount.One; +} + +export function elementOccurrenceCount(element: AST.SvelteHTMLNode): ElementOccurenceCount { + const parentCount = + element.parent !== null ? elementOccurrenceCount(element.parent) : ElementOccurenceCount.One; + const parentChildCount = childElementOccurenceCount(element.parent); + return multiplyCounts(parentCount, parentChildCount); +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/class01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/class01-input.svelte index b849abc73..8109e77f1 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/class01-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/class01-input.svelte @@ -10,6 +10,14 @@ Text 3 +{#each ["one", "two"] as iter} + {iter} +{/each} + + + Text 5 + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/_requirements.json new file mode 100644 index 000000000..498661308 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/_requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/class01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/class01-input.svelte new file mode 100644 index 000000000..a44da9098 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/svelte-5/class01-input.svelte @@ -0,0 +1,11 @@ +Outside + +{#snippet iterated()} + Text 4 +{/snippet} + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-suffix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-suffix01-input.svelte index 5d6b127a3..2dd97dc31 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-suffix01-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-suffix01-input.svelte @@ -19,6 +19,10 @@ Click me four! +{#each ["one", "two"] as count} + Bold in each +{/each} +