Skip to content

Commit 7633e3c

Browse files
committed
feat(require-event-prefix): implemented the rule
1 parent beb0184 commit 7633e3c

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
@@ -337,6 +337,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
337337
| [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: |
338338
| [svelte/prefer-class-directive](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-class-directive/) | require class directives instead of ternary expressions | :wrench: |
339339
| [svelte/prefer-style-directive](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-style-directive/) | require style directives instead of style attribute | :wrench: |
340+
| [svelte/require-event-prefix](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-prefix/) | require component event names to start with "on" | |
340341
| [svelte/shorthand-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/shorthand-attribute/) | enforce use of shorthand syntax in attribute | :wrench: |
341342
| [svelte/shorthand-directive](https://sveltejs.github.io/eslint-plugin-svelte/rules/shorthand-directive/) | enforce use of shorthand syntax in directives | :wrench: |
342343
| [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
@@ -94,6 +94,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
9494
| [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: |
9595
| [svelte/prefer-class-directive](./rules/prefer-class-directive.md) | require class directives instead of ternary expressions | :wrench: |
9696
| [svelte/prefer-style-directive](./rules/prefer-style-directive.md) | require style directives instead of style attribute | :wrench: |
97+
| [svelte/require-event-prefix](./rules/require-event-prefix.md) | require component event names to start with "on" | |
9798
| [svelte/shorthand-attribute](./rules/shorthand-attribute.md) | enforce use of shorthand syntax in attribute | :wrench: |
9899
| [svelte/shorthand-directive](./rules/shorthand-directive.md) | enforce use of shorthand syntax in directives | :wrench: |
99100
| [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
@@ -316,6 +316,11 @@ export interface RuleOptions {
316316
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/
317317
*/
318318
'svelte/require-event-dispatcher-types'?: Linter.RuleEntry<[]>
319+
/**
320+
* require component event names to start with "on"
321+
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-prefix/
322+
*/
323+
'svelte/require-event-prefix'?: Linter.RuleEntry<SvelteRequireEventPrefix>
319324
/**
320325
* require style attributes that can be optimized
321326
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/
@@ -553,6 +558,10 @@ type SveltePreferConst = []|[{
553558
ignoreReadBeforeAssign?: boolean
554559
excludedRunes?: string[]
555560
}]
561+
// ----- svelte/require-event-prefix -----
562+
type SvelteRequireEventPrefix = []|[{
563+
checkAsyncFunctions?: boolean
564+
}]
556565
// ----- svelte/shorthand-attribute -----
557566
type SvelteShorthandAttribute = []|[{
558567
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
@@ -62,6 +62,7 @@ import preferDestructuredStoreProps from '../rules/prefer-destructured-store-pro
6262
import preferStyleDirective from '../rules/prefer-style-directive.js';
6363
import requireEachKey from '../rules/require-each-key.js';
6464
import requireEventDispatcherTypes from '../rules/require-event-dispatcher-types.js';
65+
import requireEventPrefix from '../rules/require-event-prefix.js';
6566
import requireOptimizedStyleAttribute from '../rules/require-optimized-style-attribute.js';
6667
import requireStoreCallbacksUseSetParam from '../rules/require-store-callbacks-use-set-param.js';
6768
import requireStoreReactiveAccess from '../rules/require-store-reactive-access.js';
@@ -137,6 +138,7 @@ export const rules = [
137138
preferStyleDirective,
138139
requireEachKey,
139140
requireEventDispatcherTypes,
141+
requireEventPrefix,
140142
requireOptimizedStyleAttribute,
141143
requireStoreCallbacksUseSetParam,
142144
requireStoreReactiveAccess,

0 commit comments

Comments
 (0)