diff --git a/.changeset/ready-views-burn.md b/.changeset/ready-views-burn.md new file mode 100644 index 000000000..a5f11d84b --- /dev/null +++ b/.changeset/ready-views-burn.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': minor +--- + +feat: add `prefer-writable-derived` rule diff --git a/.changeset/rich-dogs-design.md b/.changeset/rich-dogs-design.md new file mode 100644 index 000000000..0ef7fbf2c --- /dev/null +++ b/.changeset/rich-dogs-design.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': minor +--- + +feat: added the `require-event-prefix` rule diff --git a/.changeset/two-hats-ask.md b/.changeset/two-hats-ask.md new file mode 100644 index 000000000..8d0295c45 --- /dev/null +++ b/.changeset/two-hats-ask.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': minor +--- + +feat(consistent-selector-style): added support for dynamic classes and IDs diff --git a/README.md b/README.md index c379a3d82..edc472b33 100644 --- a/README.md +++ b/README.md @@ -310,6 +310,7 @@ These rules relate to better ways of doing things to help you avoid problems: | [svelte/no-useless-mustaches](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :star::wrench: | | [svelte/prefer-const](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-const/) | Require `const` declarations for variables that are never reassigned after declared | :wrench: | | [svelte/prefer-destructured-store-props](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-destructured-store-props/) | destructure values from object stores for better change tracking & fewer redraws | :bulb: | +| [svelte/prefer-writable-derived](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-writable-derived/) | Prefer using writable $derived instead of $state and $effect | :star::bulb: | | [svelte/require-each-key](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-each-key/) | require keyed `{#each}` block | :star: | | [svelte/require-event-dispatcher-types](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/) | require type parameters for `createEventDispatcher` | :star: | | [svelte/require-optimized-style-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/) | require style attributes that can be optimized | | @@ -337,6 +338,7 @@ These rules relate to style guidelines, and are therefore quite subjective: | [svelte/no-spaces-around-equal-signs-in-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-spaces-around-equal-signs-in-attribute/) | disallow spaces around equal signs in attribute | :wrench: | | [svelte/prefer-class-directive](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-class-directive/) | require class directives instead of ternary expressions | :wrench: | | [svelte/prefer-style-directive](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-style-directive/) | require style directives instead of style attribute | :wrench: | +| [svelte/require-event-prefix](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-prefix/) | require component event names to start with "on" | | | [svelte/shorthand-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/shorthand-attribute/) | enforce use of shorthand syntax in attribute | :wrench: | | [svelte/shorthand-directive](https://sveltejs.github.io/eslint-plugin-svelte/rules/shorthand-directive/) | enforce use of shorthand syntax in directives | :wrench: | | [svelte/sort-attributes](https://sveltejs.github.io/eslint-plugin-svelte/rules/sort-attributes/) | enforce order of attributes | :wrench: | diff --git a/docs-svelte-kit/package.json b/docs-svelte-kit/package.json index 6ea86fb29..6137c3995 100644 --- a/docs-svelte-kit/package.json +++ b/docs-svelte-kit/package.json @@ -25,7 +25,7 @@ "@types/babel__core": "^7.20.5", "@types/cross-spawn": "^6.0.6", "@types/escape-html": "^1.0.4", - "@types/eslint-scope": "^3.7.7", + "@types/eslint-scope": "^8.0.0", "@types/eslint-visitor-keys": "^3.3.2", "@types/less": "^3.0.7", "@types/markdown-it": "^14.1.2", diff --git a/docs/rules.md b/docs/rules.md index df2ca5ec4..75dd5ed5e 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -67,6 +67,7 @@ These rules relate to better ways of doing things to help you avoid problems: | [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :star::wrench: | | [svelte/prefer-const](./rules/prefer-const.md) | Require `const` declarations for variables that are never reassigned after declared | :wrench: | | [svelte/prefer-destructured-store-props](./rules/prefer-destructured-store-props.md) | destructure values from object stores for better change tracking & fewer redraws | :bulb: | +| [svelte/prefer-writable-derived](./rules/prefer-writable-derived.md) | Prefer using writable $derived instead of $state and $effect | :star::bulb: | | [svelte/require-each-key](./rules/require-each-key.md) | require keyed `{#each}` block | :star: | | [svelte/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md) | require type parameters for `createEventDispatcher` | :star: | | [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized | | @@ -94,6 +95,7 @@ These rules relate to style guidelines, and are therefore quite subjective: | [svelte/no-spaces-around-equal-signs-in-attribute](./rules/no-spaces-around-equal-signs-in-attribute.md) | disallow spaces around equal signs in attribute | :wrench: | | [svelte/prefer-class-directive](./rules/prefer-class-directive.md) | require class directives instead of ternary expressions | :wrench: | | [svelte/prefer-style-directive](./rules/prefer-style-directive.md) | require style directives instead of style attribute | :wrench: | +| [svelte/require-event-prefix](./rules/require-event-prefix.md) | require component event names to start with "on" | | | [svelte/shorthand-attribute](./rules/shorthand-attribute.md) | enforce use of shorthand syntax in attribute | :wrench: | | [svelte/shorthand-directive](./rules/shorthand-directive.md) | enforce use of shorthand syntax in directives | :wrench: | | [svelte/sort-attributes](./rules/sort-attributes.md) | enforce order of attributes | :wrench: | diff --git a/docs/rules/no-at-html-tags.md b/docs/rules/no-at-html-tags.md index bdec6d96a..aba37a4bb 100644 --- a/docs/rules/no-at-html-tags.md +++ b/docs/rules/no-at-html-tags.md @@ -40,7 +40,7 @@ If you are certain the content passed to `{@html}` is sanitized HTML you can dis ## :books: Further Reading -- [Svelte - Tutorial > 1. Introduction / HTML tags](https://svelte.dev/tutorial/html-tags) +- [Svelte - Tutorial > Basic Svelte / Introduction / HTML tags](https://svelte.dev/tutorial/svelte/html-tags) ## :rocket: Version diff --git a/docs/rules/prefer-class-directive.md b/docs/rules/prefer-class-directive.md index 5b5c6a165..3eb3ed69d 100644 --- a/docs/rules/prefer-class-directive.md +++ b/docs/rules/prefer-class-directive.md @@ -62,7 +62,7 @@ You cannot enforce this style by using [prettier-plugin-svelte]. That is, this r ## :books: Further Reading -- [Svelte - Tutorial > 13. Classes / The class directive](https://svelte.dev/tutorial/classes) +- [Svelte - Tutorial > Basic Svelte / Classes and styles / The class atribute](https://svelte.dev/tutorial/svelte/classes) ## :rocket: Version diff --git a/docs/rules/prefer-writable-derived.md b/docs/rules/prefer-writable-derived.md new file mode 100644 index 000000000..830bec2aa --- /dev/null +++ b/docs/rules/prefer-writable-derived.md @@ -0,0 +1,60 @@ +--- +pageClass: 'rule-details' +sidebarDepth: 0 +title: 'svelte/prefer-writable-derived' +description: 'Prefer using writable $derived instead of $state and $effect' +--- + +# svelte/prefer-writable-derived + +> Prefer using writable $derived instead of $state and $effect + +- :exclamation: **_This rule has not been released yet._** +- :gear: This rule is included in `"plugin:svelte/recommended"`. +- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). + +## :book: Rule Details + +This rule reports when you use a combination of `$state` and `$effect` to create a derived value that can be written to. It encourages using the more concise and clearer `$derived` syntax instead. + + + +```svelte + +``` + +The rule specifically looks for patterns where: + +1. You initialize a variable with `$state()` +2. You then use `$effect()` or `$effect.pre()` to assign a new value to that same variable +3. The effect function contains only a single assignment statement + +When this pattern is detected, the rule suggests refactoring to use `$derived()` instead, which provides the same functionality in a more concise way. + +## :wrench: Options + +Nothing. + +- This rule has no options. + +## :books: Further Reading + +- [Svelte Documentation on Reactivity Primitives](https://svelte.dev/docs/svelte-components#script-2-assignments-are-reactive) +- [Svelte RFC for Reactivity Primitives](https://github.com/sveltejs/rfcs/blob/rfc-better-primitives/text/0000-better-primitives.md) + +## :mag: Implementation + +- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts) +- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/prefer-writable-derived.ts) diff --git a/docs/rules/require-each-key.md b/docs/rules/require-each-key.md index cd0069cb1..0daa7ec84 100644 --- a/docs/rules/require-each-key.md +++ b/docs/rules/require-each-key.md @@ -44,7 +44,7 @@ Nothing. ## :books: Further Reading -- [Svelte - Tutorial > 4. Logic / Keyed each blocks](https://svelte.dev/tutorial/svelte/keyed-each-blocks) +- [Svelte - Tutorial > Basic Svelte / Logic / Keyed each blocks](https://svelte.dev/tutorial/svelte/keyed-each-blocks) ## :rocket: Version diff --git a/docs/rules/require-event-prefix.md b/docs/rules/require-event-prefix.md new file mode 100644 index 000000000..556509c6e --- /dev/null +++ b/docs/rules/require-event-prefix.md @@ -0,0 +1,71 @@ +--- +pageClass: 'rule-details' +sidebarDepth: 0 +title: 'svelte/require-event-prefix' +description: 'require component event names to start with "on"' +--- + +# svelte/require-event-prefix + +> require component event names to start with "on" + +- :exclamation: **_This rule has not been released yet._** + +## :book: Rule Details + +Starting with Svelte 5, component events are just component props that are functions and so can be called like any function. Events for HTML elements all have their name begin with "on" (e.g. `onclick`). This rule enforces that all component events (i.e. function props) also begin with "on". + + + +```svelte + +``` + +```svelte + +``` + +## :wrench: Options + +```json +{ + "svelte/require-event-prefix": [ + "error", + { + "checkAsyncFunctions": false + } + ] +} +``` + +- `checkAsyncFunctions` ... Whether to also report asychronous function properties. Default `false`. + +## :books: Further Reading + +- [Svelte docs on events in version 5](https://svelte.dev/docs/svelte/v5-migration-guide#Event-changes) + +## :mag: Implementation + +- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts) +- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/require-event-prefix.ts) diff --git a/docs/rules/valid-each-key.md b/docs/rules/valid-each-key.md index 5e6aa5795..2515751ba 100644 --- a/docs/rules/valid-each-key.md +++ b/docs/rules/valid-each-key.md @@ -53,7 +53,7 @@ Nothing. ## :books: Further Reading -- [Svelte - Tutorial > 4. Logic / Keyed each blocks](https://svelte.dev/tutorial/keyed-each-blocks) +- [Svelte - Tutorial > Basic Svelte / Logic / Keyed each blocks](https://svelte.dev/tutorial/svelte/keyed-each-blocks) ## :rocket: Version diff --git a/package.json b/package.json index 254352fc5..1b4433bda 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@typescript-eslint/parser": "^8.16.0", "c8": "^10.1.2", "env-cmd": "^10.1.0", - "eslint": "~9.23.0", + "eslint": "~9.26.0", "eslint-config-prettier": "^10.0.0", "eslint-formatter-friendly": "^7.0.0", "eslint-plugin-eslint-plugin": "^6.3.2", @@ -34,15 +34,15 @@ "eslint-plugin-markdown": "^5.1.0", "eslint-plugin-mdx": "^3.1.5", "eslint-plugin-n": "^17.14.0", - "eslint-plugin-node-dependencies": "^0.12.0", + "eslint-plugin-node-dependencies": "^1.0.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-regexp": "^2.7.0", "eslint-plugin-yml": "^1.15.0", - "npm-run-all2": "^7.0.1", + "npm-run-all2": "^8.0.0", "prettier": "^3.4.1", "prettier-plugin-svelte": "^3.3.2", "rimraf": "^6.0.1", - "typescript": "~5.7.2", + "typescript": "~5.8.0", "typescript-eslint": "^8.16.0" }, "publishConfig": { diff --git a/packages/eslint-plugin-svelte/package.json b/packages/eslint-plugin-svelte/package.json index ad41dd414..eb49cb21b 100644 --- a/packages/eslint-plugin-svelte/package.json +++ b/packages/eslint-plugin-svelte/package.json @@ -59,7 +59,7 @@ "@eslint-community/eslint-utils": "^4.4.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", - "known-css-properties": "^0.35.0", + "known-css-properties": "^0.36.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", diff --git a/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts b/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts index b1b2d51ad..7a08befc8 100644 --- a/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts +++ b/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts @@ -37,6 +37,7 @@ const config: Linter.Config[] = [ 'svelte/no-unused-svelte-ignore': 'error', 'svelte/no-useless-children-snippet': 'error', 'svelte/no-useless-mustaches': 'error', + 'svelte/prefer-writable-derived': 'error', 'svelte/require-each-key': 'error', 'svelte/require-event-dispatcher-types': 'error', 'svelte/require-store-reactive-access': 'error', diff --git a/packages/eslint-plugin-svelte/src/meta.ts b/packages/eslint-plugin-svelte/src/meta.ts index 32472e659..e36fbda62 100644 --- a/packages/eslint-plugin-svelte/src/meta.ts +++ b/packages/eslint-plugin-svelte/src/meta.ts @@ -1,5 +1,5 @@ // IMPORTANT! // This file has been automatically generated, // in order to update its content execute "pnpm run update" -export const name = 'eslint-plugin-svelte'; -export const version = '3.5.1'; +export const name = 'eslint-plugin-svelte' as const; +export const version = '3.5.1' as const; diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index 7b5cf238c..ac9f2a94f 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -306,6 +306,11 @@ export interface RuleOptions { * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-style-directive/ */ 'svelte/prefer-style-directive'?: Linter.RuleEntry<[]> + /** + * Prefer using writable $derived instead of $state and $effect + * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-writable-derived/ + */ + 'svelte/prefer-writable-derived'?: Linter.RuleEntry<[]> /** * require keyed `{#each}` block * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-each-key/ @@ -316,6 +321,11 @@ export interface RuleOptions { * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/ */ 'svelte/require-event-dispatcher-types'?: Linter.RuleEntry<[]> + /** + * require component event names to start with "on" + * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-prefix/ + */ + 'svelte/require-event-prefix'?: Linter.RuleEntry /** * require style attributes that can be optimized * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/ @@ -554,6 +564,10 @@ type SveltePreferConst = []|[{ ignoreReadBeforeAssign?: boolean excludedRunes?: string[] }] +// ----- svelte/require-event-prefix ----- +type SvelteRequireEventPrefix = []|[{ + checkAsyncFunctions?: boolean +}] // ----- svelte/shorthand-attribute ----- type SvelteShorthandAttribute = []|[{ prefer?: ("always" | "never") diff --git a/packages/eslint-plugin-svelte/src/rules/block-lang.ts b/packages/eslint-plugin-svelte/src/rules/block-lang.ts index 39a95ca9e..1a673b3ec 100644 --- a/packages/eslint-plugin-svelte/src/rules/block-lang.ts +++ b/packages/eslint-plugin-svelte/src/rules/block-lang.ts @@ -1,6 +1,6 @@ import { createRule } from '../utils/index.js'; import { findAttribute, getLangValue } from '../utils/ast-utils.js'; -import type { SvelteScriptElement, SvelteStyleElement } from 'svelte-eslint-parser/lib/ast'; +import type { AST } from 'svelte-eslint-parser'; import type { SuggestionReportDescriptor, SourceCode } from '../types.js'; export default createRule('block-lang', { @@ -68,13 +68,13 @@ export default createRule('block-lang', { const allowedScriptLangs: (string | null)[] = Array.isArray(scriptOption) ? scriptOption : [scriptOption]; - const scriptNodes: SvelteScriptElement[] = []; + const scriptNodes: AST.SvelteScriptElement[] = []; const styleOption: string | null | (string | null)[] = context.options[0]?.style ?? null; const allowedStyleLangs: (string | null)[] = Array.isArray(styleOption) ? styleOption : [styleOption]; - const styleNodes: SvelteStyleElement[] = []; + const styleNodes: AST.SvelteStyleElement[] = []; return { SvelteScriptElement(node) { @@ -153,7 +153,7 @@ function buildAddLangSuggestions( function buildReplaceLangSuggestions( langs: (string | null)[], - node: SvelteScriptElement | SvelteStyleElement + node: AST.SvelteScriptElement | AST.SvelteStyleElement ): SuggestionReportDescriptor[] { const tagName = node.name.name; const langAttribute = findAttribute(node, 'lang'); 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 93f2a2cba..1f64ee0da 100644 --- a/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts +++ b/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts @@ -7,8 +7,19 @@ import type { Tag as SelectorTag } from 'postcss-selector-parser'; import { findClassesInAttribute } from '../utils/ast-utils.js'; +import { + extractExpressionPrefixLiteral, + extractExpressionSuffixLiteral +} from '../utils/expression-affixes.js'; import { createRule } from '../utils/index.js'; +interface Selections { + exact: Map; + // [prefix, suffix] + affixes: Map<[string | null, string | null], AST.SvelteHTMLElement[]>; + universalSelector: boolean; +} + export default createRule('consistent-selector-style', { meta: { docs: { @@ -62,9 +73,24 @@ export default createRule('consistent-selector-style', { const style = context.options[0]?.style ?? ['type', 'id', 'class']; const whitelistedClasses: string[] = []; - const classSelections: Map = new Map(); - const idSelections: Map = new Map(); - const typeSelections: Map = new Map(); + + const selections: { + class: Selections; + id: Selections; + type: Map; + } = { + class: { + exact: new Map(), + affixes: new Map(), + universalSelector: false + }, + id: { + exact: new Map(), + affixes: new Map(), + universalSelector: false + }, + type: new Map() + }; /** * Checks selectors in a given PostCSS node @@ -109,10 +135,10 @@ export default createRule('consistent-selector-style', { * Checks a class selector */ function checkClassSelector(node: SelectorClass): void { - if (whitelistedClasses.includes(node.value)) { + if (selections.class.universalSelector || whitelistedClasses.includes(node.value)) { return; } - const selection = classSelections.get(node.value) ?? []; + const selection = matchSelection(selections.class, node.value); for (const styleValue of style) { if (styleValue === 'class') { return; @@ -124,7 +150,7 @@ export default createRule('consistent-selector-style', { }); return; } - if (styleValue === 'type' && canUseTypeSelector(selection, typeSelections)) { + if (styleValue === 'type' && canUseTypeSelector(selection, selections.type)) { context.report({ messageId: 'classShouldBeType', loc: styleSelectorNodeLoc(node) as AST.SourceLocation @@ -138,7 +164,10 @@ export default createRule('consistent-selector-style', { * Checks an ID selector */ function checkIdSelector(node: SelectorIdentifier): void { - const selection = idSelections.get(node.value) ?? []; + if (selections.id.universalSelector) { + return; + } + const selection = matchSelection(selections.id, node.value); for (const styleValue of style) { if (styleValue === 'class') { context.report({ @@ -150,7 +179,7 @@ export default createRule('consistent-selector-style', { if (styleValue === 'id') { return; } - if (styleValue === 'type' && canUseTypeSelector(selection, typeSelections)) { + if (styleValue === 'type' && canUseTypeSelector(selection, selections.type)) { context.report({ messageId: 'idShouldBeType', loc: styleSelectorNodeLoc(node) as AST.SourceLocation @@ -164,7 +193,7 @@ export default createRule('consistent-selector-style', { * Checks a type selector */ function checkTypeSelector(node: SelectorTag): void { - const selection = typeSelections.get(node.value) ?? []; + const selection = selections.type.get(node.value) ?? []; for (const styleValue of style) { if (styleValue === 'class') { context.report({ @@ -191,21 +220,39 @@ export default createRule('consistent-selector-style', { if (node.kind !== 'html') { return; } - addToArrayMap(typeSelections, node.name.name, node); - const classes = node.startTag.attributes.flatMap(findClassesInAttribute); - for (const className of classes) { - addToArrayMap(classSelections, className, node); - } + addToArrayMap(selections.type, node.name.name, node); for (const attribute of node.startTag.attributes) { if (attribute.type === 'SvelteDirective' && attribute.kind === 'Class') { whitelistedClasses.push(attribute.key.name.name); } - if (attribute.type !== 'SvelteAttribute' || attribute.key.name !== 'id') { + for (const className of findClassesInAttribute(attribute)) { + addToArrayMap(selections.class.exact, className, node); + } + if (attribute.type !== 'SvelteAttribute') { continue; } for (const value of attribute.value) { - if (value.type === 'SvelteLiteral') { - addToArrayMap(idSelections, value.value, node); + if (attribute.key.name === 'class' && value.type === 'SvelteMustacheTag') { + const prefix = extractExpressionPrefixLiteral(context, value.expression); + const suffix = extractExpressionSuffixLiteral(context, value.expression); + if (prefix === null && suffix === null) { + selections.class.universalSelector = true; + } else { + addToArrayMap(selections.class.affixes, [prefix, suffix], node); + } + } + if (attribute.key.name === 'id') { + if (value.type === 'SvelteLiteral') { + addToArrayMap(selections.id.exact, value.value, node); + } else if (value.type === 'SvelteMustacheTag') { + const prefix = extractExpressionPrefixLiteral(context, value.expression); + const suffix = extractExpressionSuffixLiteral(context, value.expression); + if (prefix === null && suffix === null) { + selections.id.universalSelector = true; + } else { + addToArrayMap(selections.id.affixes, [prefix, suffix], node); + } + } } } } @@ -227,14 +274,27 @@ export default createRule('consistent-selector-style', { /** * Helper function to add a value to a Map of arrays */ -function addToArrayMap( - map: Map, - key: string, +function addToArrayMap( + map: Map, + key: T, value: AST.SvelteHTMLElement ): void { map.set(key, (map.get(key) ?? []).concat(value)); } +/** + * 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) ?? []; + selections.affixes.forEach((nodes, [prefix, suffix]) => { + if ((prefix === null || key.startsWith(prefix)) && (suffix === null || key.endsWith(suffix))) { + selection.push(...nodes); + } + }); + return selection; +} + /** * Checks whether a given selection could be obtained using an ID selector */ diff --git a/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts b/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts index 00f73d02c..2bebe6d16 100644 --- a/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts +++ b/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts @@ -2,8 +2,9 @@ import type { TSESTree } from '@typescript-eslint/types'; import { createRule } from '../utils/index.js'; import { ReferenceTracker } from '@eslint-community/eslint-utils'; import { findVariable } from '../utils/ast-utils.js'; +import { extractExpressionPrefixVariable } from '../utils/expression-affixes.js'; import type { RuleContext } from '../types.js'; -import type { SvelteLiteral } from 'svelte-eslint-parser/lib/ast'; +import type { AST } from 'svelte-eslint-parser'; export default createRule('no-navigation-without-base', { meta: { @@ -221,87 +222,8 @@ function expressionStartsWithBase( url: TSESTree.Expression, basePathNames: Set ): boolean { - switch (url.type) { - case 'BinaryExpression': - return binaryExpressionStartsWithBase(context, url, basePathNames); - case 'Identifier': - return variableStartsWithBase(context, url, basePathNames); - case 'MemberExpression': - return memberExpressionStartsWithBase(url, basePathNames); - case 'TemplateLiteral': - return templateLiteralStartsWithBase(context, url, basePathNames); - default: - return false; - } -} - -function binaryExpressionStartsWithBase( - context: RuleContext, - url: TSESTree.BinaryExpression, - basePathNames: Set -): boolean { - return ( - url.left.type !== 'PrivateIdentifier' && - expressionStartsWithBase(context, url.left, basePathNames) - ); -} - -function memberExpressionStartsWithBase( - url: TSESTree.MemberExpression, - basePathNames: Set -): boolean { - return url.property.type === 'Identifier' && basePathNames.has(url.property); -} - -function variableStartsWithBase( - context: RuleContext, - url: TSESTree.Identifier, - basePathNames: Set -): boolean { - if (basePathNames.has(url)) { - return true; - } - const variable = findVariable(context, url); - if ( - variable === null || - variable.identifiers.length !== 1 || - variable.identifiers[0].parent.type !== 'VariableDeclarator' || - variable.identifiers[0].parent.init === null - ) { - return false; - } - return expressionStartsWithBase(context, variable.identifiers[0].parent.init, basePathNames); -} - -function templateLiteralStartsWithBase( - context: RuleContext, - url: TSESTree.TemplateLiteral, - basePathNames: Set -): boolean { - const startingIdentifier = extractLiteralStartingExpression(url); - return ( - startingIdentifier !== undefined && - expressionStartsWithBase(context, startingIdentifier, basePathNames) - ); -} - -function extractLiteralStartingExpression( - templateLiteral: TSESTree.TemplateLiteral -): TSESTree.Expression | undefined { - const literalParts = [...templateLiteral.expressions, ...templateLiteral.quasis].sort((a, b) => - a.range[0] < b.range[0] ? -1 : 1 - ); - for (const part of literalParts) { - if (part.type === 'TemplateElement' && part.value.raw === '') { - // Skip empty quasi in the begining - continue; - } - if (part.type !== 'TemplateElement') { - return part; - } - return undefined; - } - return undefined; + const prefixVariable = extractExpressionPrefixVariable(context, url); + return prefixVariable !== null && basePathNames.has(prefixVariable); } function expressionIsEmpty(url: TSESTree.Expression): boolean { @@ -314,7 +236,7 @@ function expressionIsEmpty(url: TSESTree.Expression): boolean { ); } -function expressionIsAbsolute(url: SvelteLiteral | TSESTree.Expression): boolean { +function expressionIsAbsolute(url: AST.SvelteLiteral | TSESTree.Expression): boolean { switch (url.type) { case 'BinaryExpression': return binaryExpressionIsAbsolute(url); @@ -347,7 +269,7 @@ function urlValueIsAbsolute(url: string): boolean { return url.includes('://'); } -function expressionIsFragment(url: SvelteLiteral | TSESTree.Expression): boolean { +function expressionIsFragment(url: AST.SvelteLiteral | TSESTree.Expression): boolean { switch (url.type) { case 'BinaryExpression': return binaryExpressionIsFragment(url); diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts new file mode 100644 index 000000000..c7f859dbd --- /dev/null +++ b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts @@ -0,0 +1,147 @@ +import type { TSESTree } from '@typescript-eslint/types'; +import { createRule } from '../utils/index.js'; +import { getScope } from '../utils/ast-utils.js'; +import { VERSION as SVELTE_VERSION } from 'svelte/compiler'; +import semver from 'semver'; + +// Writable derived were introduced in Svelte 5.25.0 +const shouldRun = semver.satisfies(SVELTE_VERSION, '>=5.25.0'); + +type ValidFunctionType = TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression; +type ValidFunction = ValidFunctionType & { + body: TSESTree.BlockStatement; +}; + +type ValidAssignmentExpression = TSESTree.AssignmentExpression & { + operator: '='; + left: TSESTree.Identifier; +}; + +type ValidExpressionStatement = TSESTree.ExpressionStatement & { + expression: ValidAssignmentExpression; +}; + +function isEffectOrEffectPre(node: TSESTree.CallExpression) { + if (node.callee.type === 'Identifier') { + return node.callee.name === '$effect'; + } + if (node.callee.type === 'MemberExpression') { + return ( + node.callee.object.type === 'Identifier' && + node.callee.object.name === '$effect' && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'pre' + ); + } + + return false; +} + +function isValidFunctionArgument(argument: TSESTree.Node): argument is ValidFunction { + if ( + (argument.type !== 'FunctionExpression' && argument.type !== 'ArrowFunctionExpression') || + argument.params.length !== 0 + ) { + return false; + } + + if (argument.body.type !== 'BlockStatement') { + return false; + } + + return argument.body.body.length === 1; +} + +function isValidAssignment(statement: TSESTree.Statement): statement is ValidExpressionStatement { + if (statement.type !== 'ExpressionStatement') return false; + + const { expression } = statement; + return ( + expression.type === 'AssignmentExpression' && + expression.operator === '=' && + expression.left.type === 'Identifier' + ); +} + +function isStateVariable(init: TSESTree.Expression | null): init is TSESTree.CallExpression { + return ( + init?.type === 'CallExpression' && + init.callee.type === 'Identifier' && + init.callee.name === '$state' + ); +} + +export default createRule('prefer-writable-derived', { + meta: { + docs: { + description: 'Prefer using writable $derived instead of $state and $effect', + category: 'Best Practices', + recommended: true + }, + schema: [], + messages: { + unexpected: 'Prefer using writable $derived instead of $state and $effect', + suggestRewrite: 'Rewrite $state and $effect to $derived' + }, + type: 'suggestion', + conditions: [ + { + svelteVersions: ['5'], + runes: [true, 'undetermined'] + } + ], + hasSuggestions: true + }, + create(context) { + if (!shouldRun) { + return {}; + } + return { + CallExpression: (node: TSESTree.CallExpression) => { + if (!isEffectOrEffectPre(node) || node.arguments.length !== 1) { + return; + } + + const argument = node.arguments[0]; + if (!isValidFunctionArgument(argument)) { + return; + } + + const statement = argument.body.body[0]; + if (!isValidAssignment(statement)) { + return; + } + + const { left, right } = statement.expression; + const scope = getScope(context, statement); + const reference = scope.references.find( + (ref) => ref.identifier.type === 'Identifier' && ref.identifier.name === left.name + ); + + const def = reference?.resolved?.defs?.[0]; + if (!def || def.type !== 'Variable' || def.node.type !== 'VariableDeclarator') { + return; + } + + const { init } = def.node; + if (!isStateVariable(init)) { + return; + } + + context.report({ + node: def.node, + messageId: 'unexpected', + suggest: [ + { + messageId: 'suggestRewrite', + fix: (fixer) => { + const rightCode = context.sourceCode.getText(right); + return [fixer.replaceText(init, `$derived(${rightCode})`), fixer.remove(node)]; + } + } + ] + }); + } + }; + } +}); diff --git a/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts b/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts new file mode 100644 index 000000000..ed3c3ad08 --- /dev/null +++ b/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts @@ -0,0 +1,123 @@ +import { createRule } from '../utils/index.js'; +import { + type TSTools, + getTypeScriptTools, + isMethodSymbol, + isPropertySignatureKind, + isFunctionTypeKind, + isMethodSignatureKind, + isTypeReferenceKind, + isIdentifierKind +} from '../utils/ts-utils/index.js'; +import type { Symbol, Type } from 'typescript'; +import type { CallExpression } from 'estree'; + +export default createRule('require-event-prefix', { + meta: { + docs: { + description: 'require component event names to start with "on"', + category: 'Stylistic Issues', + conflictWithPrettier: false, + recommended: false + }, + schema: [ + { + type: 'object', + properties: { + checkAsyncFunctions: { + type: 'boolean' + } + }, + additionalProperties: false + } + ], + messages: { + nonPrefixedFunction: 'Component event name must start with "on".' + }, + type: 'suggestion', + conditions: [ + { + svelteVersions: ['5'], + svelteFileTypes: ['.svelte'] + } + ] + }, + create(context) { + const tsTools = getTypeScriptTools(context); + if (!tsTools) { + return {}; + } + + const checkAsyncFunctions = context.options[0]?.checkAsyncFunctions ?? false; + + return { + CallExpression(node) { + const propsType = getPropsType(node, tsTools); + if (propsType === undefined) { + return; + } + for (const property of propsType.getProperties()) { + if ( + isFunctionLike(property, tsTools) && + !property.getName().startsWith('on') && + (checkAsyncFunctions || !isFunctionAsync(property, tsTools)) + ) { + const declarationTsNode = property.getDeclarations()?.[0]; + const declarationEstreeNode = + declarationTsNode !== undefined + ? tsTools.service.tsNodeToESTreeNodeMap.get(declarationTsNode) + : undefined; + context.report({ + node: declarationEstreeNode ?? node, + messageId: 'nonPrefixedFunction' + }); + } + } + } + }; + } +}); + +function getPropsType(node: CallExpression, tsTools: TSTools): Type | undefined { + if ( + node.callee.type !== 'Identifier' || + node.callee.name !== '$props' || + node.parent.type !== 'VariableDeclarator' + ) { + return undefined; + } + + const tsNode = tsTools.service.esTreeNodeToTSNodeMap.get(node.parent.id); + if (tsNode === undefined) { + return undefined; + } + + return tsTools.service.program.getTypeChecker().getTypeAtLocation(tsNode); +} + +function isFunctionLike(functionSymbol: Symbol, tsTools: TSTools): boolean { + return ( + isMethodSymbol(functionSymbol, tsTools.ts) || + (functionSymbol.valueDeclaration !== undefined && + isPropertySignatureKind(functionSymbol.valueDeclaration, tsTools.ts) && + functionSymbol.valueDeclaration.type !== undefined && + isFunctionTypeKind(functionSymbol.valueDeclaration.type, tsTools.ts)) + ); +} + +function isFunctionAsync(functionSymbol: Symbol, tsTools: TSTools): boolean { + return ( + functionSymbol.getDeclarations()?.some((declaration) => { + if (!isMethodSignatureKind(declaration, tsTools.ts)) { + return false; + } + if (declaration.type === undefined || !isTypeReferenceKind(declaration.type, tsTools.ts)) { + return false; + } + return ( + isIdentifierKind(declaration.type.typeName, tsTools.ts) && + declaration.type.typeName.escapedText === 'Promise' + ); + }) ?? false + ); +} diff --git a/packages/eslint-plugin-svelte/src/utils/expression-affixes.ts b/packages/eslint-plugin-svelte/src/utils/expression-affixes.ts new file mode 100644 index 000000000..53b6562bc --- /dev/null +++ b/packages/eslint-plugin-svelte/src/utils/expression-affixes.ts @@ -0,0 +1,209 @@ +import type { TSESTree } from '@typescript-eslint/types'; +import { findVariable } from './ast-utils.js'; +import type { RuleContext } from '../types.js'; +import type { AST } from 'svelte-eslint-parser'; + +// Variable prefix extraction + +export function extractExpressionPrefixVariable( + context: RuleContext, + expression: TSESTree.Expression +): TSESTree.Identifier | null { + switch (expression.type) { + case 'BinaryExpression': + return extractBinaryExpressionPrefixVariable(context, expression); + case 'Identifier': + return extractVariablePrefixVariable(context, expression); + case 'MemberExpression': + return extractMemberExpressionPrefixVariable(expression); + case 'TemplateLiteral': + return extractTemplateLiteralPrefixVariable(context, expression); + default: + return null; + } +} + +function extractBinaryExpressionPrefixVariable( + context: RuleContext, + expression: TSESTree.BinaryExpression +): TSESTree.Identifier | null { + return expression.left.type !== 'PrivateIdentifier' + ? extractExpressionPrefixVariable(context, expression.left) + : null; +} + +function extractVariablePrefixVariable( + context: RuleContext, + expression: TSESTree.Identifier +): TSESTree.Identifier | null { + const variable = findVariable(context, expression); + if ( + variable === null || + variable.identifiers.length !== 1 || + variable.identifiers[0].parent.type !== 'VariableDeclarator' || + variable.identifiers[0].parent.init === null + ) { + return expression; + } + return ( + extractExpressionPrefixVariable(context, variable.identifiers[0].parent.init) ?? expression + ); +} + +function extractMemberExpressionPrefixVariable( + expression: TSESTree.MemberExpression +): TSESTree.Identifier | null { + return expression.property.type === 'Identifier' ? expression.property : null; +} + +function extractTemplateLiteralPrefixVariable( + context: RuleContext, + expression: TSESTree.TemplateLiteral +): TSESTree.Identifier | null { + const literalParts = [...expression.expressions, ...expression.quasis].sort((a, b) => + a.range[0] < b.range[0] ? -1 : 1 + ); + for (const part of literalParts) { + if (part.type === 'TemplateElement' && part.value.raw === '') { + // Skip empty quasi in the begining + continue; + } + if (part.type !== 'TemplateElement') { + return extractExpressionPrefixVariable(context, part); + } + return null; + } + return null; +} + +// Literal prefix extraction + +export function extractExpressionPrefixLiteral( + context: RuleContext, + expression: AST.SvelteLiteral | TSESTree.Node +): string | null { + switch (expression.type) { + case 'BinaryExpression': + return extractBinaryExpressionPrefixLiteral(context, expression); + case 'Identifier': + return extractVariablePrefixLiteral(context, expression); + case 'Literal': + return typeof expression.value === 'string' ? expression.value : null; + case 'SvelteLiteral': + return expression.value; + case 'TemplateLiteral': + return extractTemplateLiteralPrefixLiteral(context, expression); + default: + return null; + } +} + +function extractBinaryExpressionPrefixLiteral( + context: RuleContext, + expression: TSESTree.BinaryExpression +): string | null { + return expression.left.type !== 'PrivateIdentifier' + ? extractExpressionPrefixLiteral(context, expression.left) + : null; +} + +function extractVariablePrefixLiteral( + context: RuleContext, + expression: TSESTree.Identifier +): string | null { + const variable = findVariable(context, expression); + if ( + variable === null || + variable.identifiers.length !== 1 || + variable.identifiers[0].parent.type !== 'VariableDeclarator' || + variable.identifiers[0].parent.init === null + ) { + return null; + } + return extractExpressionPrefixLiteral(context, variable.identifiers[0].parent.init); +} + +function extractTemplateLiteralPrefixLiteral( + context: RuleContext, + expression: TSESTree.TemplateLiteral +): string | null { + const literalParts = [...expression.expressions, ...expression.quasis].sort((a, b) => + a.range[0] < b.range[0] ? -1 : 1 + ); + for (const part of literalParts) { + if (part.type === 'TemplateElement') { + if (part.value.raw === '') { + // Skip empty quasi + continue; + } + return part.value.raw; + } + return extractExpressionPrefixLiteral(context, part); + } + return null; +} + +// Literal suffix extraction + +export function extractExpressionSuffixLiteral( + context: RuleContext, + expression: AST.SvelteLiteral | TSESTree.Node +): string | null { + switch (expression.type) { + case 'BinaryExpression': + return extractBinaryExpressionSuffixLiteral(context, expression); + case 'Identifier': + return extractVariableSuffixLiteral(context, expression); + case 'Literal': + return typeof expression.value === 'string' ? expression.value : null; + case 'SvelteLiteral': + return expression.value; + case 'TemplateLiteral': + return extractTemplateLiteralSuffixLiteral(context, expression); + default: + return null; + } +} + +function extractBinaryExpressionSuffixLiteral( + context: RuleContext, + expression: TSESTree.BinaryExpression +): string | null { + return extractExpressionSuffixLiteral(context, expression.right); +} + +function extractVariableSuffixLiteral( + context: RuleContext, + expression: TSESTree.Identifier +): string | null { + const variable = findVariable(context, expression); + if ( + variable === null || + variable.identifiers.length !== 1 || + variable.identifiers[0].parent.type !== 'VariableDeclarator' || + variable.identifiers[0].parent.init === null + ) { + return null; + } + return extractExpressionSuffixLiteral(context, variable.identifiers[0].parent.init); +} + +function extractTemplateLiteralSuffixLiteral( + context: RuleContext, + expression: TSESTree.TemplateLiteral +): string | null { + const literalParts = [...expression.expressions, ...expression.quasis].sort((a, b) => + a.range[0] < b.range[0] ? -1 : 1 + ); + for (const part of literalParts.reverse()) { + if (part.type === 'TemplateElement') { + if (part.value.raw === '') { + // Skip empty quasi + continue; + } + return part.value.raw; + } + return extractExpressionSuffixLiteral(context, part); + } + return null; +} diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts index 3151d17f1..e79cd969a 100644 --- a/packages/eslint-plugin-svelte/src/utils/rules.ts +++ b/packages/eslint-plugin-svelte/src/utils/rules.ts @@ -60,8 +60,10 @@ import preferClassDirective from '../rules/prefer-class-directive.js'; import preferConst from '../rules/prefer-const.js'; import preferDestructuredStoreProps from '../rules/prefer-destructured-store-props.js'; import preferStyleDirective from '../rules/prefer-style-directive.js'; +import preferWritableDerived from '../rules/prefer-writable-derived.js'; import requireEachKey from '../rules/require-each-key.js'; import requireEventDispatcherTypes from '../rules/require-event-dispatcher-types.js'; +import requireEventPrefix from '../rules/require-event-prefix.js'; import requireOptimizedStyleAttribute from '../rules/require-optimized-style-attribute.js'; import requireStoreCallbacksUseSetParam from '../rules/require-store-callbacks-use-set-param.js'; import requireStoreReactiveAccess from '../rules/require-store-reactive-access.js'; @@ -135,8 +137,10 @@ export const rules = [ preferConst, preferDestructuredStoreProps, preferStyleDirective, + preferWritableDerived, requireEachKey, requireEventDispatcherTypes, + requireEventPrefix, requireOptimizedStyleAttribute, requireStoreCallbacksUseSetParam, requireStoreReactiveAccess, diff --git a/packages/eslint-plugin-svelte/src/utils/ts-utils/index.ts b/packages/eslint-plugin-svelte/src/utils/ts-utils/index.ts index f386698b5..00762becc 100644 --- a/packages/eslint-plugin-svelte/src/utils/ts-utils/index.ts +++ b/packages/eslint-plugin-svelte/src/utils/ts-utils/index.ts @@ -306,3 +306,48 @@ export function getTypeOfPropertyOfType( // eslint-disable-next-line @typescript-eslint/no-explicit-any -- getTypeOfPropertyOfType is an internal API of TS. return (checker as any).getTypeOfPropertyOfType(type, name); } + +/** + * Check whether the given symbol is a method type or not. + */ +export function isMethodSymbol(type: TS.Symbol, ts: TypeScript): boolean { + return (type.getFlags() & ts.SymbolFlags.Method) !== 0; +} + +/** + * Check whether the given node is a property signature kind or not. + */ +export function isPropertySignatureKind( + node: TS.Node, + ts: TypeScript +): node is TS.PropertySignature { + return node.kind === ts.SyntaxKind.PropertySignature; +} + +/** + * Check whether the given node is a function type kind or not. + */ +export function isFunctionTypeKind(node: TS.Node, ts: TypeScript): node is TS.FunctionTypeNode { + return node.kind === ts.SyntaxKind.FunctionType; +} + +/** + * Check whether the given node is a method signature kind or not. + */ +export function isMethodSignatureKind(node: TS.Node, ts: TypeScript): node is TS.MethodSignature { + return node.kind === ts.SyntaxKind.MethodSignature; +} + +/** + * Check whether the given node is a type reference kind or not. + */ +export function isTypeReferenceKind(node: TS.Node, ts: TypeScript): node is TS.TypeReferenceNode { + return node.kind === ts.SyntaxKind.TypeReference; +} + +/** + * Check whether the given node is an identifier kind or not. + */ +export function isIdentifierKind(node: TS.Node, ts: TypeScript): node is TS.Identifier { + return node.kind === ts.SyntaxKind.Identifier; +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-prefix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-prefix01-input.svelte new file mode 100644 index 000000000..d3437cb3b --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-prefix01-input.svelte @@ -0,0 +1,33 @@ + + +Click me! + +Click me two! + +Click me two! + +Click me three! + +Click me three! + +Click me four! + +Click me four! + + 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 new file mode 100644 index 000000000..5d6b127a3 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-suffix01-input.svelte @@ -0,0 +1,34 @@ + + +Click me! + +Click me two! + +Click me two! + +Click me three! + +Click me three! + + +Click me four! + +Click me four! + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-universal01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-universal01-input.svelte new file mode 100644 index 000000000..72ea834f2 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-universal01-input.svelte @@ -0,0 +1,23 @@ + + +Click me! + +Click me two! + +Click me two! + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-prefix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-prefix01-input.svelte new file mode 100644 index 000000000..3e6743a71 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-prefix01-input.svelte @@ -0,0 +1,27 @@ + + +Click me! + +Click me two! + +Click me three! + +Click me four! + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-suffix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-suffix01-input.svelte new file mode 100644 index 000000000..dc701edaf --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-suffix01-input.svelte @@ -0,0 +1,27 @@ + + +Click me! + +Click me two! + +Click me three! + +Click me four! + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-universal01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-universal01-input.svelte new file mode 100644 index 000000000..457936530 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-universal01-input.svelte @@ -0,0 +1,23 @@ + + +Click me! + +Click me two! + +Click me two! + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/_requirements.json new file mode 100644 index 000000000..0192b1098 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/_requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0-0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-errors.yaml new file mode 100644 index 000000000..600473adf --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-errors.yaml @@ -0,0 +1,15 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-input.svelte new file mode 100644 index 000000000..51523f5c3 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-input.svelte @@ -0,0 +1,10 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-errors.yaml new file mode 100644 index 000000000..b89390b86 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-errors.yaml @@ -0,0 +1,15 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-input.svelte new file mode 100644 index 000000000..e8b6769c6 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-input.svelte @@ -0,0 +1,10 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-errors.yaml new file mode 100644 index 000000000..600473adf --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-errors.yaml @@ -0,0 +1,15 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-input.svelte new file mode 100644 index 000000000..fdafad808 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-input.svelte @@ -0,0 +1,10 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-errors.yaml new file mode 100644 index 000000000..b89390b86 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-errors.yaml @@ -0,0 +1,15 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-input.svelte new file mode 100644 index 000000000..ee0e91b2b --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-input.svelte @@ -0,0 +1,10 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-errors.yaml new file mode 100644 index 000000000..f443ce89a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-errors.yaml @@ -0,0 +1,19 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-input.svelte new file mode 100644 index 000000000..52fce48ad --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-input.svelte @@ -0,0 +1,14 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-errors.yaml new file mode 100644 index 000000000..d77136489 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-errors.yaml @@ -0,0 +1,19 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + { + newAlbumName = value; + }} + /> diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-input.svelte new file mode 100644 index 000000000..9f42e657a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-input.svelte @@ -0,0 +1,14 @@ + + + { + newAlbumName = value; + }} +/> diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-errors.yaml new file mode 100644 index 000000000..0118d5e29 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-errors.yaml @@ -0,0 +1,38 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-input.svelte new file mode 100644 index 000000000..bb6169e60 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-input.svelte @@ -0,0 +1,14 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/_requirements.json new file mode 100644 index 000000000..0192b1098 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/_requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0-0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition1-input.svelte new file mode 100644 index 000000000..57bd65848 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition1-input.svelte @@ -0,0 +1,13 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition2-input.svelte new file mode 100644 index 000000000..9083881f1 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition2-input.svelte @@ -0,0 +1,17 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/_requirements.json new file mode 100644 index 000000000..498661308 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/_requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_config.json new file mode 100644 index 000000000..a0f52ed6e --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_config.json @@ -0,0 +1,3 @@ +{ + "options": [{ "checkAsyncFunctions": true }] +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_requirements.json new file mode 100644 index 000000000..498661308 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-errors.yaml new file mode 100644 index 000000000..4c26d4e62 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-errors.yaml @@ -0,0 +1,4 @@ +- message: Component event name must start with "on". + line: 3 + column: 5 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-input.svelte new file mode 100644 index 000000000..0b0502984 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-errors.yaml new file mode 100644 index 000000000..4c26d4e62 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-errors.yaml @@ -0,0 +1,4 @@ +- message: Component event name must start with "on". + line: 3 + column: 5 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-input.svelte new file mode 100644 index 000000000..4f6f3ee49 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-errors.yaml new file mode 100644 index 000000000..4c26d4e62 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-errors.yaml @@ -0,0 +1,4 @@ +- message: Component event name must start with "on". + line: 3 + column: 5 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-input.svelte new file mode 100644 index 000000000..7874e51da --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-errors.yaml new file mode 100644 index 000000000..affa6e169 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-errors.yaml @@ -0,0 +1,4 @@ +- message: Component event name must start with "on". + line: 2 + column: 21 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-input.svelte new file mode 100644 index 000000000..3fc3c616b --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-errors.yaml new file mode 100644 index 000000000..4c26d4e62 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-errors.yaml @@ -0,0 +1,4 @@ +- message: Component event name must start with "on". + line: 3 + column: 5 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-input.svelte new file mode 100644 index 000000000..036a1d65b --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/_requirements.json new file mode 100644 index 000000000..498661308 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/_requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/any01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/any01-input.svelte new file mode 100644 index 000000000..deeb82509 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/any01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/async01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/async01-input.svelte new file mode 100644 index 000000000..4f6f3ee49 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/async01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/non-function01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/non-function01-input.svelte new file mode 100644 index 000000000..d23e387a1 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/non-function01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/with-prefix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/with-prefix01-input.svelte new file mode 100644 index 000000000..e3396af33 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/with-prefix01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/src/rules/prefer-writable-derived.ts b/packages/eslint-plugin-svelte/tests/src/rules/prefer-writable-derived.ts new file mode 100644 index 000000000..80e9065a7 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/src/rules/prefer-writable-derived.ts @@ -0,0 +1,12 @@ +import { RuleTester } from '../../utils/eslint-compat.js'; +import rule from '../../../src/rules/prefer-writable-derived.js'; +import { loadTestCases } from '../../utils/utils.js'; + +const tester = new RuleTester({ + languageOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}); + +tester.run('prefer-writable-derived', rule as any, loadTestCases('prefer-writable-derived')); diff --git a/packages/eslint-plugin-svelte/tests/src/rules/require-event-prefix.ts b/packages/eslint-plugin-svelte/tests/src/rules/require-event-prefix.ts new file mode 100644 index 000000000..7818735b5 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/src/rules/require-event-prefix.ts @@ -0,0 +1,12 @@ +import { RuleTester } from '../../utils/eslint-compat.js'; +import rule from '../../../src/rules/require-event-prefix.js'; +import { loadTestCases } from '../../utils/utils.js'; + +const tester = new RuleTester({ + languageOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}); + +tester.run('require-event-prefix', rule as any, loadTestCases('require-event-prefix'));