diff --git a/packages/eslint-plugin-svelte/CHANGELOG.md b/packages/eslint-plugin-svelte/CHANGELOG.md index bfb66a08d..5e27f2038 100644 --- a/packages/eslint-plugin-svelte/CHANGELOG.md +++ b/packages/eslint-plugin-svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # eslint-plugin-svelte +## 3.9.0 + +### Minor Changes + +- [#1235](https://github.com/sveltejs/eslint-plugin-svelte/pull/1235) [`6e86e30`](https://github.com/sveltejs/eslint-plugin-svelte/commit/6e86e30cd766181dce5849ae739eedd2adfd8d8e) Thanks [@43081j](https://github.com/43081j)! - Improve performance of ignore comment extraction and add support for comma-separated ignore codes + +## 3.8.2 + +### Patch Changes + +- [#1231](https://github.com/sveltejs/eslint-plugin-svelte/pull/1231) [`0681f90`](https://github.com/sveltejs/eslint-plugin-svelte/commit/0681f901196cf81a87169155f8f632bf12666908) Thanks [@marekdedic](https://github.com/marekdedic)! - fix(consistent-selector-style): Fixed detections of repeated elements such as in {#each} + ## 3.8.1 ### Patch Changes diff --git a/packages/eslint-plugin-svelte/package.json b/packages/eslint-plugin-svelte/package.json index a1664721a..a65f5e31d 100644 --- a/packages/eslint-plugin-svelte/package.json +++ b/packages/eslint-plugin-svelte/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-svelte", - "version": "3.8.1", + "version": "3.9.0", "description": "ESLint plugin for Svelte using AST", "repository": "git+https://github.com/sveltejs/eslint-plugin-svelte.git", "homepage": "https://sveltejs.github.io/eslint-plugin-svelte", diff --git a/packages/eslint-plugin-svelte/src/meta.ts b/packages/eslint-plugin-svelte/src/meta.ts index a2ecf1aa4..a4c93e9c9 100644 --- a/packages/eslint-plugin-svelte/src/meta.ts +++ b/packages/eslint-plugin-svelte/src/meta.ts @@ -2,4 +2,4 @@ // This file has been automatically generated, // in order to update its content execute "pnpm run update" export const name = 'eslint-plugin-svelte' as const; -export const version = '3.8.1' as const; +export const version = '3.9.0' as const; 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/shared/svelte-compile-warns/ignore-comment.ts b/packages/eslint-plugin-svelte/src/shared/svelte-compile-warns/ignore-comment.ts index 2655c789e..77cf5a0c4 100644 --- a/packages/eslint-plugin-svelte/src/shared/svelte-compile-warns/ignore-comment.ts +++ b/packages/eslint-plugin-svelte/src/shared/svelte-compile-warns/ignore-comment.ts @@ -1,7 +1,7 @@ import type { AST } from 'svelte-eslint-parser'; import type { RuleContext } from '../../types.js'; -const SVELTE_IGNORE_PATTERN = /^\s*svelte-ignore/m; +const SVELTE_IGNORE_PATTERN = /^\s*svelte-ignore\s+/; /** * Map of legacy code -> new code @@ -37,29 +37,45 @@ export function getSvelteIgnoreItems(context: RuleContext): (IgnoreItem | Ignore const ignoreComments: (IgnoreItem | IgnoreItemWithoutCode)[] = []; for (const comment of sourceCode.getAllComments()) { - const ignores = extractSvelteIgnore(comment.value, comment.range[0] + 2, comment); - if (ignores) { - ignoreComments.push(...ignores); - } else if (hasMissingCodeIgnore(comment.value)) { + const match = SVELTE_IGNORE_PATTERN.exec(comment.value); + if (!match) { + continue; + } + const codeListStart = match.index + match[0].length; + const codeList = comment.value.slice(codeListStart); + if (hasMissingCodeIgnore(codeList)) { ignoreComments.push({ range: comment.range, code: null, token: comment }); + } else { + const ignores = extractSvelteIgnore(comment.range[0] + 2, comment, codeList, codeListStart); + if (ignores) { + ignoreComments.push(...ignores); + } } } for (const token of sourceCode.ast.tokens) { if (token.type === 'HTMLComment') { const text = token.value.slice(4, -3); - const ignores = extractSvelteIgnore(text, token.range[0] + 4, token); - if (ignores) { - ignoreComments.push(...ignores); - } else if (hasMissingCodeIgnore(text)) { + const match = SVELTE_IGNORE_PATTERN.exec(text); + if (!match) { + continue; + } + const codeListStart = match.index + match[0].length; + const codeList = text.slice(codeListStart); + if (hasMissingCodeIgnore(codeList)) { ignoreComments.push({ range: token.range, code: null, token }); + } else { + const ignores = extractSvelteIgnore(token.range[0] + 4, token, codeList, codeListStart); + if (ignores) { + ignoreComments.push(...ignores); + } } } } @@ -69,46 +85,42 @@ export function getSvelteIgnoreItems(context: RuleContext): (IgnoreItem | Ignore /** Extract svelte-ignore rule names */ function extractSvelteIgnore( - text: string, startIndex: number, - token: AST.Token | AST.Comment + token: AST.Token | AST.Comment, + codeList: string, + ignoreStart: number ): IgnoreItem[] | null { - const m1 = SVELTE_IGNORE_PATTERN.exec(text); - if (!m1) { - return null; - } - const ignoreStart = m1.index + m1[0].length; - const beforeText = text.slice(ignoreStart); - if (!/^\s/.test(beforeText) || !beforeText.trim()) { - return null; - } - let start = startIndex + ignoreStart; - + const start = startIndex + ignoreStart; const results: IgnoreItem[] = []; - for (const code of beforeText.split(/\s/)) { - const end = start + code.length; - const trimmed = code.trim(); - if (trimmed) { + const separatorPattern = /\s*[\s,]\s*/g; + const separators = codeList.matchAll(separatorPattern); + let lastSeparatorEnd = 0; + for (const separator of separators) { + const code = codeList.slice(lastSeparatorEnd, separator.index); + if (code) { results.push({ - code: trimmed, - codeForV5: V5_REPLACEMENTS[trimmed] || trimmed.replace(/-/gu, '_'), - range: [start, end], + code, + codeForV5: V5_REPLACEMENTS[code] || code.replace(/-/gu, '_'), + range: [start + lastSeparatorEnd, start + separator.index], token }); } - start = end + 1; /* space */ + lastSeparatorEnd = separator.index + separator[0].length; + } + if (results.length === 0) { + const code = codeList; + results.push({ + code, + codeForV5: V5_REPLACEMENTS[code] || code.replace(/-/gu, '_'), + range: [start, start + code.length], + token + }); } return results; } /** Checks whether given comment has missing code svelte-ignore */ -function hasMissingCodeIgnore(text: string) { - const m1 = SVELTE_IGNORE_PATTERN.exec(text); - if (!m1) { - return false; - } - const ignoreStart = m1.index + m1[0].length; - const beforeText = text.slice(ignoreStart); - return !beforeText.trim(); +function hasMissingCodeIgnore(codeList: string) { + return !codeList.trim(); } 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} + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-input.svelte new file mode 100644 index 000000000..7e2559f55 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-input.svelte @@ -0,0 +1,5 @@ + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-requirements.json new file mode 100644 index 000000000..0192b1098 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-svelte-ignore/valid/svelte-ignore-comma-separated-requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0-0" +}