Skip to content

Commit d6f21e6

Browse files
committed
Fix onDblClick -> onDoubleClick, put spread event handler warning behind a flag.
1 parent dbe2007 commit d6f21e6

File tree

3 files changed

+239
-98
lines changed

3 files changed

+239
-98
lines changed

docs/event-handlers.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Options shown here are the defaults.
1919
"solid/event-handlers": ["warn", {
2020
// if true, don't warn on ambiguously named event handlers like `onclick` or `onchange`
2121
ignoreCase: false,
22+
// if true, warn when spreading event handlers onto JSX. Enable for Solid < v1.6.
23+
warnOnSpread: false,
2224
}]
2325
}
2426
```
@@ -60,6 +62,39 @@ let el = <div onLy={"string"} />;
6062
const string = "string";
6163
let el = <div onLy={string} />;
6264

65+
let el = <div onDoubleClick={() => {}} />;
66+
// after eslint --fix:
67+
let el = <div onDblClick={() => {}} />;
68+
69+
let el = <div ondoubleclick={() => {}} />;
70+
// after eslint --fix:
71+
let el = <div onDblClick={() => {}} />;
72+
73+
let el = <div ondblclick={() => {}} />;
74+
// after eslint --fix:
75+
let el = <div onDblClick={() => {}} />;
76+
77+
/* eslint solid/event-handlers: ["error", { "warnOnSpread": true }] */
78+
const handleClick = () => 42;
79+
let el = <div {...{ onClick: handleClick, foo }} />;
80+
// after eslint --fix:
81+
const handleClick = () => 42;
82+
let el = <div {...{ foo }} onClick={handleClick} />;
83+
84+
/* eslint solid/event-handlers: ["error", { "warnOnSpread": true }] */
85+
const handleClick = () => 42;
86+
let el = <div {...{ foo, onClick: handleClick }} />;
87+
// after eslint --fix:
88+
const handleClick = () => 42;
89+
let el = <div {...{ foo }} onClick={handleClick} />;
90+
91+
/* eslint solid/event-handlers: ["error", { "warnOnSpread": true }] */
92+
const handleClick = () => 42;
93+
let el = <div {...{ onClick: handleClick }} />;
94+
// after eslint --fix:
95+
const handleClick = () => 42;
96+
let el = <div onClick={handleClick} />;
97+
6398
```
6499

65100
### Valid Examples
@@ -89,6 +124,11 @@ let el = <div on:ly={() => {}} />;
89124

90125
let el = <foo.bar only="true" />;
91126

127+
let el = <div onDblClick={() => {}} />;
128+
129+
const onClick = () => 42;
130+
let el = <div {...{ onClick }} />;
131+
92132
/* eslint solid/event-handlers: ["error", { "ignoreCase": true }] */
93133
let el = <div onclick={onclick} />;
94134

src/rules/event-handlers.ts

Lines changed: 151 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -3,79 +3,102 @@ import { isDOMElementName } from "../utils";
33

44
const { getStaticValue } = ASTUtils;
55

6-
const COMMON_EVENTS: Record<string, string | null> = {
7-
animationend: "AnimationEnd",
8-
animationiteration: "AnimationIteration",
9-
animationstart: "AnimationStart",
10-
beforeinput: "BeforeInput",
11-
blur: null,
12-
change: null,
13-
click: null,
14-
contextmenu: "ContextMenu",
15-
copy: null,
16-
cut: null,
17-
dblclick: "DoubleClick",
18-
drag: null,
19-
dragend: "DragEnd",
20-
dragenter: "DragEnter",
21-
dragexit: "DragExit",
22-
dragleave: "DragLeave",
23-
dragover: "DragOver",
24-
dragstart: "DragStart",
25-
drop: null,
26-
error: null,
27-
focus: null,
28-
focusin: "FocusIn",
29-
focusout: "FocusOut",
30-
gotpointercapture: "GotPointerCapture",
31-
input: null,
32-
invalid: null,
33-
keydown: "KeyDown",
34-
keypress: "KeyPress",
35-
keyup: "KeyUp",
36-
load: null,
37-
lostpointercapture: "LostPointerCapture",
38-
mousedown: "MouseDown",
39-
mouseenter: "MouseEnter",
40-
mouseleave: "MouseLeave",
41-
mousemove: "MouseMove",
42-
mouseout: "MouseOut",
43-
mouseover: "MouseOver",
44-
mouseup: "MouseUp",
45-
paste: null,
46-
pointercancel: "PointerCancel",
47-
pointerdown: "PointerDown",
48-
pointerenter: "PointerEnter",
49-
pointerleave: "PointerLeave",
50-
pointermove: "PointerMove",
51-
pointerout: "PointerOut",
52-
pointerover: "PointerOver",
53-
pointerup: "PointerUp",
54-
reset: null,
55-
scroll: null,
56-
select: null,
57-
submit: null,
58-
toggle: null,
59-
touchcancel: "TouchCancel",
60-
touchend: "TouchEnd",
61-
touchmove: "TouchMove",
62-
touchstart: "TouchStart",
63-
transitionend: "TransitionEnd",
64-
wheel: null,
65-
};
6+
const COMMON_EVENTS = [
7+
"onAnimationEnd",
8+
"onAnimationIteration",
9+
"onAnimationStart",
10+
"onBeforeInput",
11+
"onBlur",
12+
"onChange",
13+
"onClick",
14+
"onContextMenu",
15+
"onCopy",
16+
"onCut",
17+
"onDblClick",
18+
"onDrag",
19+
"onDragEnd",
20+
"onDragEnter",
21+
"onDragExit",
22+
"onDragLeave",
23+
"onDragOver",
24+
"onDragStart",
25+
"onDrop",
26+
"onError",
27+
"onFocus",
28+
"onFocusIn",
29+
"onFocusOut",
30+
"onGotPointerCapture",
31+
"onInput",
32+
"onInvalid",
33+
"onKeyDown",
34+
"onKeyPress",
35+
"onKeyUp",
36+
"onLoad",
37+
"onLostPointerCapture",
38+
"onMouseDown",
39+
"onMouseEnter",
40+
"onMouseLeave",
41+
"onMouseMove",
42+
"onMouseOut",
43+
"onMouseOver",
44+
"onMouseUp",
45+
"onPaste",
46+
"onPointerCancel",
47+
"onPointerDown",
48+
"onPointerEnter",
49+
"onPointerLeave",
50+
"onPointerMove",
51+
"onPointerOut",
52+
"onPointerOver",
53+
"onPointerUp",
54+
"onReset",
55+
"onScroll",
56+
"onSelect",
57+
"onSubmit",
58+
"onToggle",
59+
"onTouchCancel",
60+
"onTouchEnd",
61+
"onTouchMove",
62+
"onTouchStart",
63+
"onTransitionEnd",
64+
"onWheel",
65+
] as const;
66+
type CommonEvent = typeof COMMON_EVENTS[number];
67+
68+
const COMMON_EVENTS_MAP = new Map<string, CommonEvent>(
69+
(function* () {
70+
for (const event of COMMON_EVENTS) {
71+
yield [event.toLowerCase(), event] as const;
72+
}
73+
})()
74+
);
6675

67-
const isCommonEventName = (lowercaseEventName: string) =>
68-
Object.prototype.hasOwnProperty.call(COMMON_EVENTS, lowercaseEventName);
69-
const getCommonEventHandlerName = (lowercaseEventName: string) => {
70-
return `on${
71-
COMMON_EVENTS[lowercaseEventName] ??
72-
lowercaseEventName[0].toUpperCase() + lowercaseEventName.slice(1).toLowerCase()
73-
}`;
76+
const NONSTANDARD_EVENTS_MAP = {
77+
ondoubleclick: "onDblClick",
7478
};
7579

80+
const isCommonHandlerName = (
81+
lowercaseHandlerName: string
82+
): lowercaseHandlerName is Lowercase<CommonEvent> => COMMON_EVENTS_MAP.has(lowercaseHandlerName);
83+
const getCommonEventHandlerName = (lowercaseHandlerName: Lowercase<CommonEvent>): CommonEvent =>
84+
COMMON_EVENTS_MAP.get(lowercaseHandlerName)!;
85+
86+
const isNonstandardEventName = (
87+
lowercaseEventName: string
88+
): lowercaseEventName is keyof typeof NONSTANDARD_EVENTS_MAP =>
89+
Boolean((NONSTANDARD_EVENTS_MAP as Record<string, string>)[lowercaseEventName]);
90+
const getStandardEventHandlerName = (lowercaseEventName: keyof typeof NONSTANDARD_EVENTS_MAP) =>
91+
NONSTANDARD_EVENTS_MAP[lowercaseEventName];
92+
7693
const rule: TSESLint.RuleModule<
77-
"naming" | "capitalization" | "make-handler" | "make-attr" | "detected-attr" | "spread-handler",
78-
[{ ignoreCase?: boolean }?]
94+
| "naming"
95+
| "capitalization"
96+
| "nonstandard"
97+
| "make-handler"
98+
| "make-attr"
99+
| "detected-attr"
100+
| "spread-handler",
101+
[{ ignoreCase?: boolean; warnOnSpread?: boolean }?]
79102
> = {
80103
meta: {
81104
type: "problem",
@@ -97,6 +120,12 @@ const rule: TSESLint.RuleModule<
97120
"if true, don't warn on ambiguously named event handlers like `onclick` or `onchange`",
98121
default: false,
99122
},
123+
warnOnSpread: {
124+
type: "boolean",
125+
description:
126+
"if true, warn when spreading event handlers onto JSX. Enable for Solid < v1.6.",
127+
default: false,
128+
},
100129
},
101130
additionalProperties: false,
102131
},
@@ -107,6 +136,8 @@ const rule: TSESLint.RuleModule<
107136
naming:
108137
"The {{name}} prop is ambiguous. If it is an event handler, change it to {{handlerName}}. If it is an attribute, change it to {{attrName}}.",
109138
capitalization: "The {{name}} prop should be renamed to {{fixedName}} for readability.",
139+
nonstandard:
140+
"The {{name}} prop should be renamed to {{fixedName}}, because it's not a standard event handler.",
110141
"make-handler": "Change the {{name}} prop to {{handlerName}}.",
111142
"make-attr": "Change the {{name}} prop to {{attrName}}.",
112143
"spread-handler":
@@ -127,14 +158,13 @@ const rule: TSESLint.RuleModule<
127158
}
128159

129160
if (node.name.type === "JSXNamespacedName") {
130-
return; // bail early on attr:, on:, etc. props
161+
return; // bail early on attr:, on:, oncapture:, etc. props
131162
}
132163

133164
// string name of the name node
134165
const { name } = node.name;
135166

136-
const match = /^on([a-zA-Z].*)$/.exec(name);
137-
if (!match) {
167+
if (!/^on[a-zA-Z].*$/.test(name)) {
138168
return; // bail if Solid doesn't consider the prop name an event handler
139169
}
140170

@@ -175,9 +205,17 @@ const rule: TSESLint.RuleModule<
175205
},
176206
});
177207
} else if (!context.options[0]?.ignoreCase) {
178-
const lowercaseEventName = match[1].toLowerCase();
179-
if (isCommonEventName(lowercaseEventName)) {
180-
const fixedName = getCommonEventHandlerName(lowercaseEventName);
208+
const lowercaseHandlerName = name.toLowerCase();
209+
if (isNonstandardEventName(lowercaseHandlerName)) {
210+
const fixedName = getStandardEventHandlerName(lowercaseHandlerName);
211+
context.report({
212+
node: node.name,
213+
messageId: "nonstandard",
214+
data: { name, fixedName },
215+
fix: (fixer) => fixer.replaceText(node.name, fixedName),
216+
});
217+
} else if (isCommonHandlerName(lowercaseHandlerName)) {
218+
const fixedName = getCommonEventHandlerName(lowercaseHandlerName);
181219
if (fixedName !== name) {
182220
// For common DOM event names, we know the user intended the prop to be an event handler.
183221
// Fix it to have an uppercase third letter and be properly camel-cased.
@@ -192,7 +230,7 @@ const rule: TSESLint.RuleModule<
192230
// this includes words like `only` and `ongoing` as well as unknown handlers like `onfoobar`.
193231
// Enforce using either /^on[A-Z]/ (event handler) or /^attr:on[a-z]/ (forced regular attribute)
194232
// to make user intent clear and code maximally readable
195-
const handlerName = `on${match[1][0].toUpperCase()}${match[1].slice(1)}`;
233+
const handlerName = `on${name[2].toUpperCase()}${name.slice(3)}`;
196234
const attrName = `attr:${name}`;
197235
context.report({
198236
node: node.name,
@@ -214,30 +252,45 @@ const rule: TSESLint.RuleModule<
214252
}
215253
}
216254
},
217-
"JSXSpreadAttribute > ObjectExpression > Property"(node: T.Property) {
218-
const openingElement = node.parent!.parent!.parent as T.JSXOpeningElement;
255+
Property(node: T.Property) {
219256
if (
220-
openingElement.name.type === "JSXIdentifier" &&
221-
isDOMElementName(openingElement.name.name)
257+
context.options[0]?.warnOnSpread &&
258+
node.parent?.type === "ObjectExpression" &&
259+
node.parent.parent?.type === "JSXSpreadAttribute" &&
260+
node.parent.parent.parent?.type === "JSXOpeningElement"
222261
) {
223-
if (node.key.type === "Identifier" && /^on/.test(node.key.name)) {
224-
const handlerName = node.key.name;
225-
// An event handler is being spread in (ex. <button {...{ onClick }} />), which doesn't
226-
// actually add an event listener, just a plain attribute.
227-
context.report({
228-
node,
229-
messageId: "spread-handler",
230-
data: {
231-
name: node.key.name,
232-
},
233-
fix: (fixer) => [
234-
fixer.remove(node),
235-
fixer.insertTextAfter(
236-
node.parent!.parent!,
237-
` ${handlerName}={${sourceCode.getText(node.value)}}`
238-
),
239-
],
240-
});
262+
const openingElement = node.parent.parent.parent;
263+
if (
264+
openingElement.name.type === "JSXIdentifier" &&
265+
isDOMElementName(openingElement.name.name)
266+
) {
267+
if (node.key.type === "Identifier" && /^on/.test(node.key.name)) {
268+
const handlerName = node.key.name;
269+
// An event handler is being spread in (ex. <button {...{ onClick }} />), which doesn't
270+
// actually add an event listener, just a plain attribute.
271+
context.report({
272+
node,
273+
messageId: "spread-handler",
274+
data: {
275+
name: node.key.name,
276+
},
277+
*fix(fixer) {
278+
const commaAfter = sourceCode.getTokenAfter(node);
279+
yield fixer.remove(
280+
(node.parent as T.ObjectExpression).properties.length === 1
281+
? node.parent!.parent!
282+
: node
283+
);
284+
if (commaAfter?.value === ",") {
285+
yield fixer.remove(commaAfter);
286+
}
287+
yield fixer.insertTextAfter(
288+
node.parent!.parent!,
289+
` ${handlerName}={${sourceCode.getText(node.value)}}`
290+
);
291+
},
292+
});
293+
}
241294
}
242295
}
243296
},

0 commit comments

Comments
 (0)