From beb0184285bf0e59f51ebc32547058cc059360f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 10 Feb 2025 14:03:51 +0100 Subject: [PATCH 1/5] test(require-event-prefix): added tests --- .../require-event-prefix/invalid/_requirements.json | 3 +++ .../invalid/checkAsyncFunctions/_config.json | 3 +++ .../invalid/checkAsyncFunctions/_requirements.json | 3 +++ .../checkAsyncFunctions/async-arrow01-errors.yaml | 4 ++++ .../checkAsyncFunctions/async-arrow01-input.svelte | 9 +++++++++ .../invalid/checkAsyncFunctions/async01-errors.yaml | 4 ++++ .../invalid/checkAsyncFunctions/async01-input.svelte | 9 +++++++++ .../invalid/no-prefix-arrow01-errors.yaml | 4 ++++ .../invalid/no-prefix-arrow01-input.svelte | 9 +++++++++ .../invalid/no-prefix-inline-type01-errors.yaml | 4 ++++ .../invalid/no-prefix-inline-type01-input.svelte | 5 +++++ .../invalid/no-prefix01-errors.yaml | 4 ++++ .../invalid/no-prefix01-input.svelte | 9 +++++++++ .../require-event-prefix/valid/_requirements.json | 3 +++ .../require-event-prefix/valid/any01-input.svelte | 9 +++++++++ .../require-event-prefix/valid/async01-input.svelte | 9 +++++++++ .../valid/non-function01-input.svelte | 9 +++++++++ .../valid/with-prefix01-input.svelte | 9 +++++++++ .../tests/src/rules/require-event-prefix.ts | 12 ++++++++++++ 19 files changed, 121 insertions(+) create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/_requirements.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_requirements.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/_requirements.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/any01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/async01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/non-function01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/with-prefix01-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/src/rules/require-event-prefix.ts 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/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')); From 7633e3c9b31afff171d5a61e5f3e78cebd7f35e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 10 Feb 2025 18:48:59 +0100 Subject: [PATCH 2/5] feat(require-event-prefix): implemented the rule --- .changeset/rich-dogs-design.md | 5 + README.md | 1 + docs/rules.md | 1 + .../eslint-plugin-svelte/src/rule-types.ts | 9 ++ .../src/rules/require-event-prefix.ts | 122 ++++++++++++++++++ .../eslint-plugin-svelte/src/utils/rules.ts | 2 + 6 files changed, 140 insertions(+) create mode 100644 .changeset/rich-dogs-design.md create mode 100644 packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts 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/README.md b/README.md index db731dd26..ddda4b3e9 100644 --- a/README.md +++ b/README.md @@ -337,6 +337,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/rules.md b/docs/rules.md index 58d896b10..61d96dcbf 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -94,6 +94,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/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index 19e30a345..412df0fae 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -316,6 +316,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/ @@ -553,6 +558,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/require-event-prefix.ts b/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts new file mode 100644 index 000000000..65870e3ae --- /dev/null +++ b/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts @@ -0,0 +1,122 @@ +import { createRule } from '../utils/index.js'; +import { type TSTools, getTypeScriptTools } from '../utils/ts-utils/index.js'; +import { + type MethodSignature, + type Symbol, + SymbolFlags, + SyntaxKind, + type Type, + type TypeReferenceNode, + type PropertySignature +} 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) && + !property.getName().startsWith('on') && + (checkAsyncFunctions || !isFunctionAsync(property)) + ) { + 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): boolean { + return ( + (functionSymbol.getFlags() & SymbolFlags.Method) !== 0 || + (functionSymbol.valueDeclaration?.kind === SyntaxKind.PropertySignature && + (functionSymbol.valueDeclaration as PropertySignature).type?.kind === SyntaxKind.FunctionType) + ); +} + +function isFunctionAsync(functionSymbol: Symbol): boolean { + return ( + functionSymbol.getDeclarations()?.some((declaration) => { + if (declaration.kind !== SyntaxKind.MethodSignature) { + return false; + } + const declarationType = (declaration as MethodSignature).type; + if (declarationType?.kind !== SyntaxKind.TypeReference) { + return false; + } + const declarationTypeName = (declarationType as TypeReferenceNode).typeName; + return ( + declarationTypeName.kind === SyntaxKind.Identifier && + declarationTypeName.escapedText === 'Promise' + ); + }) ?? false + ); +} diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts index 3151d17f1..825edea9e 100644 --- a/packages/eslint-plugin-svelte/src/utils/rules.ts +++ b/packages/eslint-plugin-svelte/src/utils/rules.ts @@ -62,6 +62,7 @@ import preferDestructuredStoreProps from '../rules/prefer-destructured-store-pro import preferStyleDirective from '../rules/prefer-style-directive.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'; @@ -137,6 +138,7 @@ export const rules = [ preferStyleDirective, requireEachKey, requireEventDispatcherTypes, + requireEventPrefix, requireOptimizedStyleAttribute, requireStoreCallbacksUseSetParam, requireStoreReactiveAccess, From d428bfda8b0a28463f0c2c57d811d24838e1e5d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Mon, 10 Feb 2025 19:56:55 +0100 Subject: [PATCH 3/5] docs(require-event-prefix): added rule docs --- docs/rules/require-event-prefix.md | 71 ++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 docs/rules/require-event-prefix.md 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) From 96ad1dbe203fa98fec5b04b675ccd2bcc2569038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Tue, 18 Mar 2025 21:22:03 +0100 Subject: [PATCH 4/5] chore: extracted isMethodSymbol to ts-utils --- .../src/rules/require-event-prefix.ts | 9 ++++----- .../eslint-plugin-svelte/src/utils/ts-utils/index.ts | 7 +++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts b/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts index 65870e3ae..e5121f318 100644 --- a/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts +++ b/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts @@ -1,9 +1,8 @@ import { createRule } from '../utils/index.js'; -import { type TSTools, getTypeScriptTools } from '../utils/ts-utils/index.js'; +import { type TSTools, getTypeScriptTools, isMethodSymbol } from '../utils/ts-utils/index.js'; import { type MethodSignature, type Symbol, - SymbolFlags, SyntaxKind, type Type, type TypeReferenceNode, @@ -57,7 +56,7 @@ export default createRule('require-event-prefix', { } for (const property of propsType.getProperties()) { if ( - isFunctionLike(property) && + isFunctionLike(property, tsTools) && !property.getName().startsWith('on') && (checkAsyncFunctions || !isFunctionAsync(property)) ) { @@ -94,9 +93,9 @@ function getPropsType(node: CallExpression, tsTools: TSTools): Type | undefined return tsTools.service.program.getTypeChecker().getTypeAtLocation(tsNode); } -function isFunctionLike(functionSymbol: Symbol): boolean { +function isFunctionLike(functionSymbol: Symbol, tsTools: TSTools): boolean { return ( - (functionSymbol.getFlags() & SymbolFlags.Method) !== 0 || + isMethodSymbol(functionSymbol, tsTools.ts) || (functionSymbol.valueDeclaration?.kind === SyntaxKind.PropertySignature && (functionSymbol.valueDeclaration as PropertySignature).type?.kind === SyntaxKind.FunctionType) ); 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 cbe430c99..c248bf0e4 100644 --- a/packages/eslint-plugin-svelte/src/utils/ts-utils/index.ts +++ b/packages/eslint-plugin-svelte/src/utils/ts-utils/index.ts @@ -307,3 +307,10 @@ 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; +} From aedca993063c1caf1c72cb7900c91a974d0d6ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Tue, 18 Mar 2025 21:41:14 +0100 Subject: [PATCH 5/5] chore: extracted node type checking functions to ts-utils --- .../src/rules/require-event-prefix.ts | 38 ++++++++++--------- .../src/utils/ts-utils/index.ts | 38 +++++++++++++++++++ 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts b/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts index e5121f318..ed3c3ad08 100644 --- a/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts +++ b/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts @@ -1,13 +1,15 @@ import { createRule } from '../utils/index.js'; -import { type TSTools, getTypeScriptTools, isMethodSymbol } from '../utils/ts-utils/index.js'; import { - type MethodSignature, - type Symbol, - SyntaxKind, - type Type, - type TypeReferenceNode, - type PropertySignature -} from 'typescript'; + 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', { @@ -58,7 +60,7 @@ export default createRule('require-event-prefix', { if ( isFunctionLike(property, tsTools) && !property.getName().startsWith('on') && - (checkAsyncFunctions || !isFunctionAsync(property)) + (checkAsyncFunctions || !isFunctionAsync(property, tsTools)) ) { const declarationTsNode = property.getDeclarations()?.[0]; const declarationEstreeNode = @@ -96,25 +98,25 @@ function getPropsType(node: CallExpression, tsTools: TSTools): Type | undefined function isFunctionLike(functionSymbol: Symbol, tsTools: TSTools): boolean { return ( isMethodSymbol(functionSymbol, tsTools.ts) || - (functionSymbol.valueDeclaration?.kind === SyntaxKind.PropertySignature && - (functionSymbol.valueDeclaration as PropertySignature).type?.kind === SyntaxKind.FunctionType) + (functionSymbol.valueDeclaration !== undefined && + isPropertySignatureKind(functionSymbol.valueDeclaration, tsTools.ts) && + functionSymbol.valueDeclaration.type !== undefined && + isFunctionTypeKind(functionSymbol.valueDeclaration.type, tsTools.ts)) ); } -function isFunctionAsync(functionSymbol: Symbol): boolean { +function isFunctionAsync(functionSymbol: Symbol, tsTools: TSTools): boolean { return ( functionSymbol.getDeclarations()?.some((declaration) => { - if (declaration.kind !== SyntaxKind.MethodSignature) { + if (!isMethodSignatureKind(declaration, tsTools.ts)) { return false; } - const declarationType = (declaration as MethodSignature).type; - if (declarationType?.kind !== SyntaxKind.TypeReference) { + if (declaration.type === undefined || !isTypeReferenceKind(declaration.type, tsTools.ts)) { return false; } - const declarationTypeName = (declarationType as TypeReferenceNode).typeName; return ( - declarationTypeName.kind === SyntaxKind.Identifier && - declarationTypeName.escapedText === 'Promise' + isIdentifierKind(declaration.type.typeName, tsTools.ts) && + declaration.type.typeName.escapedText === 'Promise' ); }) ?? false ); 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 c248bf0e4..f8085947a 100644 --- a/packages/eslint-plugin-svelte/src/utils/ts-utils/index.ts +++ b/packages/eslint-plugin-svelte/src/utils/ts-utils/index.ts @@ -314,3 +314,41 @@ export function getTypeOfPropertyOfType( 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; +}