Skip to content

Commit 68592f1

Browse files
committed
feat(require-event-prefix): implemented the rule
1 parent 86115a6 commit 68592f1

File tree

6 files changed

+126
-0
lines changed

6 files changed

+126
-0
lines changed

.changeset/rich-dogs-design.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-svelte': minor
3+
---
4+
5+
feat: added the `require-event-prefix` rule

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
395395
| [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: |
396396
| [svelte/prefer-class-directive](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-class-directive/) | require class directives instead of ternary expressions | :wrench: |
397397
| [svelte/prefer-style-directive](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-style-directive/) | require style directives instead of style attribute | :wrench: |
398+
| [svelte/require-event-prefix](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-prefix/) | require component event names to start with "on" | |
398399
| [svelte/shorthand-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/shorthand-attribute/) | enforce use of shorthand syntax in attribute | :wrench: |
399400
| [svelte/shorthand-directive](https://sveltejs.github.io/eslint-plugin-svelte/rules/shorthand-directive/) | enforce use of shorthand syntax in directives | :wrench: |
400401
| [svelte/sort-attributes](https://sveltejs.github.io/eslint-plugin-svelte/rules/sort-attributes/) | enforce order of attributes | :wrench: |

docs/rules.md

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
9292
| [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: |
9393
| [svelte/prefer-class-directive](./rules/prefer-class-directive.md) | require class directives instead of ternary expressions | :wrench: |
9494
| [svelte/prefer-style-directive](./rules/prefer-style-directive.md) | require style directives instead of style attribute | :wrench: |
95+
| [svelte/require-event-prefix](./rules/require-event-prefix.md) | require component event names to start with "on" | |
9596
| [svelte/shorthand-attribute](./rules/shorthand-attribute.md) | enforce use of shorthand syntax in attribute | :wrench: |
9697
| [svelte/shorthand-directive](./rules/shorthand-directive.md) | enforce use of shorthand syntax in directives | :wrench: |
9798
| [svelte/sort-attributes](./rules/sort-attributes.md) | enforce order of attributes | :wrench: |

packages/eslint-plugin-svelte/src/rule-types.ts

+9
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,11 @@ export interface RuleOptions {
306306
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/
307307
*/
308308
'svelte/require-event-dispatcher-types'?: Linter.RuleEntry<[]>
309+
/**
310+
* require component event names to start with "on"
311+
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-prefix/
312+
*/
313+
'svelte/require-event-prefix'?: Linter.RuleEntry<SvelteRequireEventPrefix>
309314
/**
310315
* require style attributes that can be optimized
311316
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/
@@ -531,6 +536,10 @@ type SveltePreferConst = []|[{
531536
destructuring?: ("any" | "all")
532537
ignoreReadBeforeAssign?: boolean
533538
}]
539+
// ----- svelte/require-event-prefix -----
540+
type SvelteRequireEventPrefix = []|[{
541+
checkAsyncFunctions?: boolean
542+
}]
534543
// ----- svelte/shorthand-attribute -----
535544
type SvelteShorthandAttribute = []|[{
536545
prefer?: ("always" | "never")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { createRule } from '../utils/index.js';
2+
import { type TSTools, getTypeScriptTools } from 'src/utils/ts-utils/index.js';
3+
import {
4+
type MethodSignature,
5+
type Symbol,
6+
SymbolFlags,
7+
SyntaxKind,
8+
type Type,
9+
type TypeReferenceNode
10+
} from 'typescript';
11+
import type { CallExpression } from 'estree';
12+
13+
export default createRule('require-event-prefix', {
14+
meta: {
15+
docs: {
16+
description: 'require component event names to start with "on"',
17+
category: 'Stylistic Issues',
18+
conflictWithPrettier: false,
19+
recommended: false
20+
},
21+
schema: [
22+
{
23+
type: 'object',
24+
properties: {
25+
checkAsyncFunctions: {
26+
type: 'boolean'
27+
}
28+
},
29+
additionalProperties: false
30+
}
31+
],
32+
messages: {
33+
nonPrefixedFunction: 'Component event name must start with "on".'
34+
},
35+
type: 'suggestion',
36+
conditions: [
37+
{
38+
svelteVersions: ['5'],
39+
svelteFileTypes: ['.svelte']
40+
}
41+
]
42+
},
43+
create(context) {
44+
const tsTools = getTypeScriptTools(context);
45+
if (!tsTools) {
46+
return {};
47+
}
48+
49+
const checkAsyncFunctions = context.options[0]?.checkAsyncFunctions ?? false;
50+
51+
return {
52+
CallExpression(node) {
53+
const propsType = getPropsType(node, tsTools);
54+
if (propsType === undefined) {
55+
return;
56+
}
57+
for (const property of propsType.getProperties()) {
58+
if (property.getFlags() & SymbolFlags.Method && !property.getName().startsWith('on')) {
59+
if (!checkAsyncFunctions && isFunctionAsync(property)) {
60+
continue;
61+
}
62+
context.report({
63+
node,
64+
messageId: 'nonPrefixedFunction'
65+
});
66+
}
67+
}
68+
}
69+
};
70+
}
71+
});
72+
73+
function getPropsType(node: CallExpression, tsTools: TSTools): Type | undefined {
74+
if (
75+
node.callee.type !== 'Identifier' ||
76+
node.callee.name !== '$props' ||
77+
node.parent.type !== 'VariableDeclarator'
78+
) {
79+
return undefined;
80+
}
81+
82+
const tsNode = tsTools.service.esTreeNodeToTSNodeMap.get(node.parent.id);
83+
if (tsNode === undefined) {
84+
return undefined;
85+
}
86+
87+
const checker = tsTools.service.program.getTypeChecker();
88+
return checker.getTypeAtLocation(tsNode);
89+
}
90+
91+
function isFunctionAsync(functionSymbol: Symbol): boolean {
92+
return (
93+
functionSymbol.getDeclarations()?.some((declaration) => {
94+
if (declaration.kind !== SyntaxKind.MethodSignature) {
95+
return false;
96+
}
97+
const declarationType = (declaration as MethodSignature).type;
98+
if (declarationType?.kind !== SyntaxKind.TypeReference) {
99+
return false;
100+
}
101+
const declarationTypeName = (declarationType as TypeReferenceNode).typeName;
102+
return (
103+
declarationTypeName.kind === SyntaxKind.Identifier &&
104+
declarationTypeName.escapedText === 'Promise'
105+
);
106+
}) ?? false
107+
);
108+
}

packages/eslint-plugin-svelte/src/utils/rules.ts

+2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import preferDestructuredStoreProps from '../rules/prefer-destructured-store-pro
6060
import preferStyleDirective from '../rules/prefer-style-directive.js';
6161
import requireEachKey from '../rules/require-each-key.js';
6262
import requireEventDispatcherTypes from '../rules/require-event-dispatcher-types.js';
63+
import requireEventPrefix from '../rules/require-event-prefix.js';
6364
import requireOptimizedStyleAttribute from '../rules/require-optimized-style-attribute.js';
6465
import requireStoreCallbacksUseSetParam from '../rules/require-store-callbacks-use-set-param.js';
6566
import requireStoreReactiveAccess from '../rules/require-store-reactive-access.js';
@@ -133,6 +134,7 @@ export const rules = [
133134
preferStyleDirective,
134135
requireEachKey,
135136
requireEventDispatcherTypes,
137+
requireEventPrefix,
136138
requireOptimizedStyleAttribute,
137139
requireStoreCallbacksUseSetParam,
138140
requireStoreReactiveAccess,

0 commit comments

Comments
 (0)