@@ -3,79 +3,102 @@ import { isDOMElementName } from "../utils";
3
3
4
4
const { getStaticValue } = ASTUtils ;
5
5
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
+ ) ;
66
75
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" ,
74
78
} ;
75
79
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
+
76
93
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 } ?]
79
102
> = {
80
103
meta : {
81
104
type : "problem" ,
@@ -97,6 +120,12 @@ const rule: TSESLint.RuleModule<
97
120
"if true, don't warn on ambiguously named event handlers like `onclick` or `onchange`" ,
98
121
default : false ,
99
122
} ,
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
+ } ,
100
129
} ,
101
130
additionalProperties : false ,
102
131
} ,
@@ -107,6 +136,8 @@ const rule: TSESLint.RuleModule<
107
136
naming :
108
137
"The {{name}} prop is ambiguous. If it is an event handler, change it to {{handlerName}}. If it is an attribute, change it to {{attrName}}." ,
109
138
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." ,
110
141
"make-handler" : "Change the {{name}} prop to {{handlerName}}." ,
111
142
"make-attr" : "Change the {{name}} prop to {{attrName}}." ,
112
143
"spread-handler" :
@@ -127,14 +158,13 @@ const rule: TSESLint.RuleModule<
127
158
}
128
159
129
160
if ( node . name . type === "JSXNamespacedName" ) {
130
- return ; // bail early on attr:, on:, etc. props
161
+ return ; // bail early on attr:, on:, oncapture:, etc. props
131
162
}
132
163
133
164
// string name of the name node
134
165
const { name } = node . name ;
135
166
136
- const match = / ^ o n ( [ a - z A - Z ] .* ) $ / . exec ( name ) ;
137
- if ( ! match ) {
167
+ if ( ! / ^ o n [ a - z A - Z ] .* $ / . test ( name ) ) {
138
168
return ; // bail if Solid doesn't consider the prop name an event handler
139
169
}
140
170
@@ -175,9 +205,17 @@ const rule: TSESLint.RuleModule<
175
205
} ,
176
206
} ) ;
177
207
} 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 ) ;
181
219
if ( fixedName !== name ) {
182
220
// For common DOM event names, we know the user intended the prop to be an event handler.
183
221
// Fix it to have an uppercase third letter and be properly camel-cased.
@@ -192,7 +230,7 @@ const rule: TSESLint.RuleModule<
192
230
// this includes words like `only` and `ongoing` as well as unknown handlers like `onfoobar`.
193
231
// Enforce using either /^on[A-Z]/ (event handler) or /^attr:on[a-z]/ (forced regular attribute)
194
232
// 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 ) } ` ;
196
234
const attrName = `attr:${ name } ` ;
197
235
context . report ( {
198
236
node : node . name ,
@@ -214,30 +252,45 @@ const rule: TSESLint.RuleModule<
214
252
}
215
253
}
216
254
} ,
217
- "JSXSpreadAttribute > ObjectExpression > Property" ( node : T . Property ) {
218
- const openingElement = node . parent ! . parent ! . parent as T . JSXOpeningElement ;
255
+ Property ( node : T . Property ) {
219
256
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"
222
261
) {
223
- if ( node . key . type === "Identifier" && / ^ o n / . 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" && / ^ o n / . 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
+ }
241
294
}
242
295
}
243
296
} ,
0 commit comments