Skip to content

Commit 4eda8e5

Browse files
committed
feat(require-event-prefix): implemented the rule
1 parent 26ce06b commit 4eda8e5

File tree

6 files changed

+140
-0
lines changed

6 files changed

+140
-0
lines changed

Diff for: .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

Diff for: README.md

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

Diff for: 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: |

Diff for: 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/
@@ -532,6 +537,10 @@ type SveltePreferConst = []|[{
532537
ignoreReadBeforeAssign?: boolean
533538
excludedRunes?: string[]
534539
}]
540+
// ----- svelte/require-event-prefix -----
541+
type SvelteRequireEventPrefix = []|[{
542+
checkAsyncFunctions?: boolean
543+
}]
535544
// ----- svelte/shorthand-attribute -----
536545
type SvelteShorthandAttribute = []|[{
537546
prefer?: ("always" | "never")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { createRule } from '../utils/index.js';
2+
import { type TSTools, getTypeScriptTools } from '../utils/ts-utils/index.js';
3+
import {
4+
type MethodSignature,
5+
type Symbol,
6+
SymbolFlags,
7+
SyntaxKind,
8+
type Type,
9+
type TypeReferenceNode,
10+
type PropertySignature
11+
} from 'typescript';
12+
import type { CallExpression } from 'estree';
13+
14+
export default createRule('require-event-prefix', {
15+
meta: {
16+
docs: {
17+
description: 'require component event names to start with "on"',
18+
category: 'Stylistic Issues',
19+
conflictWithPrettier: false,
20+
recommended: false
21+
},
22+
schema: [
23+
{
24+
type: 'object',
25+
properties: {
26+
checkAsyncFunctions: {
27+
type: 'boolean'
28+
}
29+
},
30+
additionalProperties: false
31+
}
32+
],
33+
messages: {
34+
nonPrefixedFunction: 'Component event name must start with "on".'
35+
},
36+
type: 'suggestion',
37+
conditions: [
38+
{
39+
svelteVersions: ['5'],
40+
svelteFileTypes: ['.svelte']
41+
}
42+
]
43+
},
44+
create(context) {
45+
const tsTools = getTypeScriptTools(context);
46+
if (!tsTools) {
47+
return {};
48+
}
49+
50+
const checkAsyncFunctions = context.options[0]?.checkAsyncFunctions ?? false;
51+
52+
return {
53+
CallExpression(node) {
54+
const propsType = getPropsType(node, tsTools);
55+
if (propsType === undefined) {
56+
return;
57+
}
58+
for (const property of propsType.getProperties()) {
59+
if (
60+
isFunctionLike(property) &&
61+
!property.getName().startsWith('on') &&
62+
(checkAsyncFunctions || !isFunctionAsync(property))
63+
) {
64+
const declarationTsNode = property.getDeclarations()?.[0];
65+
const declarationEstreeNode =
66+
declarationTsNode !== undefined
67+
? tsTools.service.tsNodeToESTreeNodeMap.get(declarationTsNode)
68+
: undefined;
69+
context.report({
70+
node: declarationEstreeNode ?? node,
71+
messageId: 'nonPrefixedFunction'
72+
});
73+
}
74+
}
75+
}
76+
};
77+
}
78+
});
79+
80+
function getPropsType(node: CallExpression, tsTools: TSTools): Type | undefined {
81+
if (
82+
node.callee.type !== 'Identifier' ||
83+
node.callee.name !== '$props' ||
84+
node.parent.type !== 'VariableDeclarator'
85+
) {
86+
return undefined;
87+
}
88+
89+
const tsNode = tsTools.service.esTreeNodeToTSNodeMap.get(node.parent.id);
90+
if (tsNode === undefined) {
91+
return undefined;
92+
}
93+
94+
return tsTools.service.program.getTypeChecker().getTypeAtLocation(tsNode);
95+
}
96+
97+
function isFunctionLike(functionSymbol: Symbol): boolean {
98+
return (
99+
(functionSymbol.getFlags() & SymbolFlags.Method) !== 0 ||
100+
(functionSymbol.valueDeclaration?.kind === SyntaxKind.PropertySignature &&
101+
(functionSymbol.valueDeclaration as PropertySignature).type?.kind === SyntaxKind.FunctionType)
102+
);
103+
}
104+
105+
function isFunctionAsync(functionSymbol: Symbol): boolean {
106+
return (
107+
functionSymbol.getDeclarations()?.some((declaration) => {
108+
if (declaration.kind !== SyntaxKind.MethodSignature) {
109+
return false;
110+
}
111+
const declarationType = (declaration as MethodSignature).type;
112+
if (declarationType?.kind !== SyntaxKind.TypeReference) {
113+
return false;
114+
}
115+
const declarationTypeName = (declarationType as TypeReferenceNode).typeName;
116+
return (
117+
declarationTypeName.kind === SyntaxKind.Identifier &&
118+
declarationTypeName.escapedText === 'Promise'
119+
);
120+
}) ?? false
121+
);
122+
}

Diff for: 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)