@qq99/eslint-plugin-solid 0.18.0-no-reactive-read-in-effect-callback.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +289 -0
  3. package/dist/chunk-4UZNJ5MS.mjs +32 -0
  4. package/dist/chunk-4UZNJ5MS.mjs.map +1 -0
  5. package/dist/chunk-EBU4C5NV.mjs +77 -0
  6. package/dist/chunk-EBU4C5NV.mjs.map +1 -0
  7. package/dist/chunk-LE6SW2M3.mjs +6231 -0
  8. package/dist/chunk-LE6SW2M3.mjs.map +1 -0
  9. package/dist/chunk-MHDQJMLE.mjs +39 -0
  10. package/dist/chunk-MHDQJMLE.mjs.map +1 -0
  11. package/dist/configs/recommended.d.mts +109 -0
  12. package/dist/configs/recommended.d.ts +109 -0
  13. package/dist/configs/recommended.js +5877 -0
  14. package/dist/configs/recommended.js.map +1 -0
  15. package/dist/configs/recommended.mjs +5 -0
  16. package/dist/configs/recommended.mjs.map +1 -0
  17. package/dist/configs/typescript.d.mts +103 -0
  18. package/dist/configs/typescript.d.ts +103 -0
  19. package/dist/configs/typescript.js +6246 -0
  20. package/dist/configs/typescript.js.map +1 -0
  21. package/dist/configs/typescript.mjs +6 -0
  22. package/dist/configs/typescript.mjs.map +1 -0
  23. package/dist/configs/v2-strict.d.mts +130 -0
  24. package/dist/configs/v2-strict.d.ts +130 -0
  25. package/dist/configs/v2-strict.js +6317 -0
  26. package/dist/configs/v2-strict.js.map +1 -0
  27. package/dist/configs/v2-strict.mjs +7 -0
  28. package/dist/configs/v2-strict.mjs.map +1 -0
  29. package/dist/configs/v2.d.mts +127 -0
  30. package/dist/configs/v2.d.ts +127 -0
  31. package/dist/configs/v2.js +6289 -0
  32. package/dist/configs/v2.js.map +1 -0
  33. package/dist/configs/v2.mjs +6 -0
  34. package/dist/configs/v2.mjs.map +1 -0
  35. package/dist/index.d.mts +740 -0
  36. package/dist/index.d.ts +740 -0
  37. package/dist/index.js +6365 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/index.mjs +42 -0
  40. package/dist/index.mjs.map +1 -0
  41. package/package.json +109 -0
  42. package/src/compat.ts +53 -0
  43. package/src/configs/recommended.ts +48 -0
  44. package/src/configs/typescript.ts +21 -0
  45. package/src/configs/v2-strict.ts +28 -0
  46. package/src/configs/v2.ts +68 -0
  47. package/src/deps.d.ts +1 -0
  48. package/src/index.ts +29 -0
  49. package/src/plugin.ts +92 -0
  50. package/src/rules/components-return-once.ts +209 -0
  51. package/src/rules/event-handlers.ts +397 -0
  52. package/src/rules/imports.ts +338 -0
  53. package/src/rules/jsx-no-duplicate-props.ts +110 -0
  54. package/src/rules/jsx-no-script-url.ts +61 -0
  55. package/src/rules/jsx-no-undef.ts +230 -0
  56. package/src/rules/jsx-uses-vars.ts +66 -0
  57. package/src/rules/no-accessor-as-prop.ts +147 -0
  58. package/src/rules/no-array-handlers.ts +57 -0
  59. package/src/rules/no-boolean-enumerated-attribute.ts +209 -0
  60. package/src/rules/no-browser-globals-in-server-function.ts +108 -0
  61. package/src/rules/no-destructure.ts +245 -0
  62. package/src/rules/no-innerhtml.ts +143 -0
  63. package/src/rules/no-invalid-server-capture.ts +104 -0
  64. package/src/rules/no-module-scope-reactive-primitive.ts +69 -0
  65. package/src/rules/no-proxy-apis.ts +96 -0
  66. package/src/rules/no-react-deps.ts +68 -0
  67. package/src/rules/no-react-specific-props.ts +60 -0
  68. package/src/rules/no-reactive-read-in-effect-callback.ts +570 -0
  69. package/src/rules/no-restated-default-options.ts +96 -0
  70. package/src/rules/no-single-arg-create-effect.ts +56 -0
  71. package/src/rules/no-store-mutation-outside-setter.ts +137 -0
  72. package/src/rules/no-unknown-namespaces.ts +140 -0
  73. package/src/rules/no-unused-signal.ts +119 -0
  74. package/src/rules/no-write-in-pure-computation.ts +231 -0
  75. package/src/rules/prefer-classlist.ts +93 -0
  76. package/src/rules/prefer-for.ts +93 -0
  77. package/src/rules/prefer-onSettled-for-side-effects.ts +101 -0
  78. package/src/rules/prefer-show.ts +102 -0
  79. package/src/rules/prefer-structured-class.ts +117 -0
  80. package/src/rules/reactivity.ts +1730 -0
  81. package/src/rules/removed-api.ts +212 -0
  82. package/src/rules/require-async-server-function.ts +122 -0
  83. package/src/rules/self-closing-comp.ts +150 -0
  84. package/src/rules/style-prop.ts +140 -0
  85. package/src/rules/valid-use-server.ts +192 -0
  86. package/src/rules/validate-jsx-nesting.ts +1 -0
  87. package/src/utils/reactive-imports.ts +382 -0
  88. package/src/utils/reactive-types.ts +272 -0
  89. package/src/utils/reactive-values.ts +162 -0
  90. package/src/utils.ts +442 -0
@@ -0,0 +1,209 @@
1
+ /**
2
+ * FIXME: remove this comments and import when below issue is fixed.
3
+ * This import is necessary for type generation due to a bug in the TypeScript compiler.
4
+ * See: https://github.com/microsoft/TypeScript/issues/42873
5
+ */
6
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
7
+ import type { TSESLint } from "@typescript-eslint/utils";
8
+
9
+ import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils";
10
+ import { getFunctionName, type FunctionNode } from "../utils";
11
+ import { getSourceCode } from "../compat";
12
+
13
+ const createRule = ESLintUtils.RuleCreator.withoutDocs;
14
+
15
+ const isNothing = (node?: T.Node): boolean => {
16
+ if (!node) {
17
+ return true;
18
+ }
19
+ switch (node.type) {
20
+ case "Literal":
21
+ return ([null, undefined, false, ""] as Array<unknown>).includes(node.value);
22
+ case "JSXFragment":
23
+ return !node.children || node.children.every(isNothing);
24
+ default:
25
+ return false;
26
+ }
27
+ };
28
+
29
+ const getLineLength = (loc: T.SourceLocation) => loc.end.line - loc.start.line + 1;
30
+
31
+ export default createRule({
32
+ meta: {
33
+ type: "problem",
34
+ docs: {
35
+ description:
36
+ "Disallow early returns in components. Solid components only run once, and so conditionals should be inside JSX.",
37
+ url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/components-return-once.md",
38
+ },
39
+ fixable: "code",
40
+ schema: [],
41
+ messages: {
42
+ noEarlyReturn:
43
+ "Solid components run once, so an early return breaks reactivity. Move the condition inside a JSX element, such as a fragment or <Show />.",
44
+ noConditionalReturn:
45
+ "Solid components run once, so a conditional return breaks reactivity. Move the condition inside a JSX element, such as a fragment or <Show />.",
46
+ },
47
+ },
48
+ defaultOptions: [],
49
+ create(context) {
50
+ const functionStack: Array<{
51
+ /** switched to true by :exit if the current function is detected to be a component */
52
+ isComponent: boolean;
53
+ lastReturn: T.ReturnStatement | undefined;
54
+ earlyReturns: Array<T.ReturnStatement>;
55
+ }> = [];
56
+ const putIntoJSX = (node: T.Node): string => {
57
+ const text = getSourceCode(context).getText(node);
58
+ return node.type === "JSXElement" || node.type === "JSXFragment" ? text : `{${text}}`;
59
+ };
60
+ const currentFunction = () => functionStack[functionStack.length - 1];
61
+ const onFunctionEnter = (node: FunctionNode) => {
62
+ let lastReturn: T.ReturnStatement | undefined;
63
+ if (node.body.type === "BlockStatement") {
64
+ // find last statement, ignoring function/class/variable declarations (hoisting)
65
+ const last = node.body.body.findLast((node) => !node.type.endsWith("Declaration"));
66
+ // if it's a return, store it
67
+ if (last && last.type === "ReturnStatement") {
68
+ lastReturn = last;
69
+ }
70
+ }
71
+ functionStack.push({ isComponent: false, lastReturn, earlyReturns: [] });
72
+ };
73
+
74
+ const onFunctionExit = (node: FunctionNode) => {
75
+ if (
76
+ // "render props" aren't components
77
+ getFunctionName(node)?.match(/^[a-z]/) ||
78
+ node.parent?.type === "JSXExpressionContainer" ||
79
+ // ignore createMemo(() => conditional JSX), report HOC(() => conditional JSX)
80
+ (node.parent?.type === "CallExpression" &&
81
+ node.parent.arguments.some((n) => n === node) &&
82
+ !(node.parent.callee as T.Identifier).name?.match(/^[A-Z]/))
83
+ ) {
84
+ currentFunction().isComponent = false;
85
+ }
86
+ if (currentFunction().isComponent) {
87
+ // Warn on each early return
88
+ currentFunction().earlyReturns.forEach((earlyReturn) => {
89
+ context.report({
90
+ node: earlyReturn,
91
+ messageId: "noEarlyReturn",
92
+ });
93
+ });
94
+
95
+ const argument = currentFunction().lastReturn?.argument;
96
+ if (argument?.type === "ConditionalExpression") {
97
+ const sourceCode = getSourceCode(context);
98
+ context.report({
99
+ node: argument.parent!,
100
+ messageId: "noConditionalReturn",
101
+ fix: (fixer) => {
102
+ const { test, consequent, alternate } = argument;
103
+ const conditions = [{ test, consequent }];
104
+ let fallback = alternate;
105
+
106
+ while (fallback.type === "ConditionalExpression") {
107
+ conditions.push({ test: fallback.test, consequent: fallback.consequent });
108
+ fallback = fallback.alternate;
109
+ }
110
+
111
+ if (conditions.length >= 2) {
112
+ // we have a nested ternary, use <Switch><Match /></Switch>
113
+ const fallbackStr = !isNothing(fallback)
114
+ ? ` fallback={${sourceCode.getText(fallback)}}`
115
+ : "";
116
+ return fixer.replaceText(
117
+ argument,
118
+ `<Switch${fallbackStr}>\n${conditions
119
+ .map(
120
+ ({ test, consequent }) =>
121
+ `<Match when={${sourceCode.getText(test)}}>${putIntoJSX(
122
+ consequent
123
+ )}</Match>`
124
+ )
125
+ .join("\n")}\n</Switch>`
126
+ );
127
+ }
128
+ if (isNothing(consequent)) {
129
+ // we have a single ternary and the consequent is nothing. Negate the condition and use a <Show>.
130
+ return fixer.replaceText(
131
+ argument,
132
+ `<Show when={!(${sourceCode.getText(test)})}>${putIntoJSX(alternate)}</Show>`
133
+ );
134
+ }
135
+ if (
136
+ isNothing(fallback) ||
137
+ getLineLength(consequent.loc) >= getLineLength(alternate.loc) * 1.5
138
+ ) {
139
+ // we have a standard ternary, and the alternate is a bit shorter in LOC than the consequent, which
140
+ // should be enough to tell that it's logically a fallback instead of an equal branch.
141
+ const fallbackStr = !isNothing(fallback)
142
+ ? ` fallback={${sourceCode.getText(fallback)}}`
143
+ : "";
144
+ return fixer.replaceText(
145
+ argument,
146
+ `<Show when={${sourceCode.getText(test)}}${fallbackStr}>${putIntoJSX(
147
+ consequent
148
+ )}</Show>`
149
+ );
150
+ }
151
+
152
+ // we have a standard ternary, but no signal from the user as to which branch is the "fallback" and
153
+ // which is the children. Move the whole conditional inside a JSX fragment.
154
+ return fixer.replaceText(argument, `<>${putIntoJSX(argument)}</>`);
155
+ },
156
+ });
157
+ } else if (argument?.type === "LogicalExpression") {
158
+ if (argument.operator === "&&") {
159
+ const sourceCode = getSourceCode(context);
160
+ // we have a `return condition && expression`--put that in a <Show />
161
+ context.report({
162
+ node: argument,
163
+ messageId: "noConditionalReturn",
164
+ fix: (fixer) => {
165
+ const { left: test, right: consequent } = argument;
166
+ return fixer.replaceText(
167
+ argument,
168
+ `<Show when={${sourceCode.getText(test)}}>${putIntoJSX(consequent)}</Show>`
169
+ );
170
+ },
171
+ });
172
+ } else {
173
+ // we have some other kind of conditional, warn
174
+ context.report({
175
+ node: argument,
176
+ messageId: "noConditionalReturn",
177
+ });
178
+ }
179
+ }
180
+ }
181
+
182
+ // Pop on exit
183
+ functionStack.pop();
184
+ };
185
+ return {
186
+ FunctionDeclaration: onFunctionEnter,
187
+ FunctionExpression: onFunctionEnter,
188
+ ArrowFunctionExpression: onFunctionEnter,
189
+ "FunctionDeclaration:exit": onFunctionExit,
190
+ "FunctionExpression:exit": onFunctionExit,
191
+ "ArrowFunctionExpression:exit": onFunctionExit,
192
+ JSXElement() {
193
+ if (functionStack.length) {
194
+ currentFunction().isComponent = true;
195
+ }
196
+ },
197
+ JSXFragment() {
198
+ if (functionStack.length) {
199
+ currentFunction().isComponent = true;
200
+ }
201
+ },
202
+ ReturnStatement(node) {
203
+ if (functionStack.length && node !== currentFunction().lastReturn) {
204
+ currentFunction().earlyReturns.push(node);
205
+ }
206
+ },
207
+ };
208
+ },
209
+ });
@@ -0,0 +1,397 @@
1
+ /**
2
+ * FIXME: remove this comments and import when below issue is fixed.
3
+ * This import is necessary for type generation due to a bug in the TypeScript compiler.
4
+ * See: https://github.com/microsoft/TypeScript/issues/42873
5
+ */
6
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
7
+ import type { TSESLint } from "@typescript-eslint/utils";
8
+
9
+ import { TSESTree as T, ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
10
+ import { isDOMElementName, isSolidV2 } from "../utils";
11
+ import { getScope, getSourceCode } from "../compat";
12
+
13
+ const createRule = ESLintUtils.RuleCreator.withoutDocs;
14
+ const { getStaticValue } = ASTUtils;
15
+
16
+ const COMMON_EVENTS = [
17
+ "onAnimationEnd",
18
+ "onAnimationIteration",
19
+ "onAnimationStart",
20
+ "onBeforeInput",
21
+ "onBlur",
22
+ "onChange",
23
+ "onClick",
24
+ "onContextMenu",
25
+ "onCopy",
26
+ "onCut",
27
+ "onDblClick",
28
+ "onDrag",
29
+ "onDragEnd",
30
+ "onDragEnter",
31
+ "onDragExit",
32
+ "onDragLeave",
33
+ "onDragOver",
34
+ "onDragStart",
35
+ "onDrop",
36
+ "onError",
37
+ "onFocus",
38
+ "onFocusIn",
39
+ "onFocusOut",
40
+ "onGotPointerCapture",
41
+ "onInput",
42
+ "onInvalid",
43
+ "onKeyDown",
44
+ "onKeyPress",
45
+ "onKeyUp",
46
+ "onLoad",
47
+ "onLostPointerCapture",
48
+ "onMouseDown",
49
+ "onMouseEnter",
50
+ "onMouseLeave",
51
+ "onMouseMove",
52
+ "onMouseOut",
53
+ "onMouseOver",
54
+ "onMouseUp",
55
+ "onPaste",
56
+ "onPointerCancel",
57
+ "onPointerDown",
58
+ "onPointerEnter",
59
+ "onPointerLeave",
60
+ "onPointerMove",
61
+ "onPointerOut",
62
+ "onPointerOver",
63
+ "onPointerUp",
64
+ "onReset",
65
+ "onScroll",
66
+ "onSelect",
67
+ "onSubmit",
68
+ "onToggle",
69
+ "onTouchCancel",
70
+ "onTouchEnd",
71
+ "onTouchMove",
72
+ "onTouchStart",
73
+ "onTransitionEnd",
74
+ "onWheel",
75
+ ] as const;
76
+ type CommonEvent = (typeof COMMON_EVENTS)[number];
77
+
78
+ const COMMON_EVENTS_MAP = new Map<string, CommonEvent>(
79
+ (function* () {
80
+ for (const event of COMMON_EVENTS) {
81
+ yield [event.toLowerCase(), event] as const;
82
+ }
83
+ })()
84
+ );
85
+
86
+ const NONSTANDARD_EVENTS_MAP = {
87
+ ondoubleclick: "onDblClick",
88
+ };
89
+
90
+ const isCommonHandlerName = (
91
+ lowercaseHandlerName: string
92
+ ): lowercaseHandlerName is Lowercase<CommonEvent> => COMMON_EVENTS_MAP.has(lowercaseHandlerName);
93
+ const getCommonEventHandlerName = (lowercaseHandlerName: Lowercase<CommonEvent>): CommonEvent =>
94
+ COMMON_EVENTS_MAP.get(lowercaseHandlerName)!;
95
+
96
+ const isNonstandardEventName = (
97
+ lowercaseEventName: string
98
+ ): lowercaseEventName is keyof typeof NONSTANDARD_EVENTS_MAP =>
99
+ Boolean((NONSTANDARD_EVENTS_MAP as Record<string, string>)[lowercaseEventName]);
100
+ const getStandardEventHandlerName = (lowercaseEventName: keyof typeof NONSTANDARD_EVENTS_MAP) =>
101
+ NONSTANDARD_EVENTS_MAP[lowercaseEventName];
102
+
103
+ type MessageIds =
104
+ | "naming"
105
+ | "capitalization"
106
+ | "nonstandard"
107
+ | "make-handler"
108
+ | "make-attr"
109
+ | "detected-attr"
110
+ | "spread-handler"
111
+ | "lowercase-attribute-v2"
112
+ | "static-handler-v2";
113
+ type Options = [{ ignoreCase?: boolean; warnOnSpread?: boolean }?];
114
+
115
+ export default createRule<Options, MessageIds>({
116
+ meta: {
117
+ type: "problem",
118
+ docs: {
119
+ description:
120
+ "Enforce naming DOM element event handlers consistently and prevent Solid's analysis from misunderstanding whether a prop should be an event handler.",
121
+ url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/event-handlers.md",
122
+ },
123
+ fixable: "code",
124
+ hasSuggestions: true,
125
+ schema: [
126
+ {
127
+ type: "object",
128
+ properties: {
129
+ ignoreCase: {
130
+ type: "boolean",
131
+ description:
132
+ "if true, don't warn on ambiguously named event handlers like `onclick` or `onchange`",
133
+ default: false,
134
+ },
135
+ warnOnSpread: {
136
+ type: "boolean",
137
+ description:
138
+ "if true, warn when spreading event handlers onto JSX. Enable for Solid < v1.6.",
139
+ default: false,
140
+ },
141
+ },
142
+ additionalProperties: false,
143
+ },
144
+ ],
145
+ messages: {
146
+ "detected-attr":
147
+ 'The {{name}} prop is named as an event handler (starts with "on"), but Solid knows its value ({{staticValue}}) is a string or number, so it will be treated as an attribute. If this is intentional, name this prop attr:{{name}}.',
148
+ naming:
149
+ "The {{name}} prop is ambiguous. If it is an event handler, change it to {{handlerName}}. If it is an attribute, change it to {{attrName}}.",
150
+ capitalization: "The {{name}} prop should be renamed to {{fixedName}} for readability.",
151
+ nonstandard:
152
+ "The {{name}} prop should be renamed to {{fixedName}}, because it's not a standard event handler.",
153
+ "make-handler": "Change the {{name}} prop to {{handlerName}}.",
154
+ "make-attr": "Change the {{name}} prop to {{attrName}}.",
155
+ "spread-handler":
156
+ "The {{name}} prop should be added as a JSX attribute, not spread in. Solid doesn't add listeners when spreading into JSX.",
157
+ "lowercase-attribute-v2":
158
+ "In Solid 2.0, {{name}} is a literal attribute, not an event handler — this listener will never fire. Rename it to {{fixedName}}.",
159
+ "static-handler-v2":
160
+ "In Solid 2.0, {{name}} is an event handler and must be a function, but its value is the static value {{staticValue}}. For a literal attribute, use lowercase {{attrName}}.",
161
+ },
162
+ },
163
+ defaultOptions: [],
164
+ create(context) {
165
+ const sourceCode = getSourceCode(context);
166
+ const v2 = isSolidV2(context);
167
+
168
+ return {
169
+ JSXAttribute(node) {
170
+ const openingElement = node.parent as T.JSXOpeningElement;
171
+ if (
172
+ openingElement.name.type !== "JSXIdentifier" ||
173
+ !isDOMElementName(openingElement.name.name)
174
+ ) {
175
+ return; // bail if this is not a DOM/SVG element or web component
176
+ }
177
+
178
+ if (node.name.type === "JSXNamespacedName") {
179
+ return; // bail early on attr:, on:, oncapture:, etc. props
180
+ }
181
+
182
+ // string name of the name node
183
+ const { name } = node.name;
184
+
185
+ if (!/^on[a-zA-Z]/.test(name)) {
186
+ return; // bail if Solid doesn't consider the prop name an event handler
187
+ }
188
+
189
+ if (v2) {
190
+ // In Solid 2.0 only the camelCase form (`onClick`) is an event handler;
191
+ // lowercase names (`onclick`) are literal attributes.
192
+ const isCamelCase = name[2] === name[2].toUpperCase();
193
+ const lowercaseName = name.toLowerCase();
194
+
195
+ if (isCamelCase) {
196
+ // Event handler position: a static string/number value is a bug.
197
+ let camelStaticValue: ReturnType<typeof getStaticValue> = null;
198
+ if (
199
+ node.value?.type === "JSXExpressionContainer" &&
200
+ node.value.expression.type !== "JSXEmptyExpression" &&
201
+ node.value.expression.type !== "ArrayExpression"
202
+ ) {
203
+ camelStaticValue = getStaticValue(node.value.expression, getScope(context, node));
204
+ }
205
+ if (node.value === null || node.value?.type === "Literal") {
206
+ camelStaticValue = { value: node.value !== null ? node.value.value : true };
207
+ }
208
+ if (
209
+ camelStaticValue !== null &&
210
+ (typeof camelStaticValue.value === "string" ||
211
+ typeof camelStaticValue.value === "number" ||
212
+ typeof camelStaticValue.value === "boolean")
213
+ ) {
214
+ context.report({
215
+ node,
216
+ messageId: "static-handler-v2",
217
+ data: { name, staticValue: camelStaticValue.value, attrName: lowercaseName },
218
+ });
219
+ } else if (isNonstandardEventName(lowercaseName)) {
220
+ // e.g. onDoubleClick — lowercasing yields no real DOM event, so
221
+ // the listener would silently never fire.
222
+ const fixedName = getStandardEventHandlerName(lowercaseName);
223
+ context.report({
224
+ node: node.name,
225
+ messageId: "nonstandard",
226
+ data: { name, fixedName },
227
+ fix: (fixer) => fixer.replaceText(node.name, fixedName),
228
+ });
229
+ }
230
+ } else {
231
+ // Literal attribute position: string values are fine, functions are
232
+ // a near-certain intended event handler that will never fire.
233
+ const isStaticAttributeValue =
234
+ node.value === null ||
235
+ node.value.type === "Literal" ||
236
+ (node.value.type === "JSXExpressionContainer" &&
237
+ node.value.expression.type !== "JSXEmptyExpression" &&
238
+ node.value.expression.type !== "ArrayExpression" &&
239
+ getStaticValue(node.value.expression, getScope(context, node)) !== null);
240
+ if (!isStaticAttributeValue) {
241
+ const fixedName = isNonstandardEventName(lowercaseName)
242
+ ? getStandardEventHandlerName(lowercaseName)
243
+ : isCommonHandlerName(lowercaseName)
244
+ ? getCommonEventHandlerName(lowercaseName)
245
+ : `on${name[2].toUpperCase()}${name.slice(3)}`;
246
+ const known =
247
+ isNonstandardEventName(lowercaseName) || isCommonHandlerName(lowercaseName);
248
+ context.report({
249
+ node: node.name,
250
+ messageId: "lowercase-attribute-v2",
251
+ data: { name, fixedName },
252
+ // Autofix when the event is a known DOM event; suggest otherwise.
253
+ fix: known ? (fixer) => fixer.replaceText(node.name, fixedName) : undefined,
254
+ suggest: known
255
+ ? undefined
256
+ : [
257
+ {
258
+ messageId: "make-handler",
259
+ data: { name, handlerName: fixedName },
260
+ fix: (fixer) => fixer.replaceText(node.name, fixedName),
261
+ },
262
+ ],
263
+ });
264
+ }
265
+ }
266
+ return;
267
+ }
268
+
269
+ let staticValue: ReturnType<typeof getStaticValue>;
270
+ if (
271
+ node.value?.type === "JSXExpressionContainer" &&
272
+ node.value.expression.type !== "JSXEmptyExpression" &&
273
+ node.value.expression.type !== "ArrayExpression" && // array syntax prevents inlining
274
+ (staticValue = getStaticValue(node.value.expression, getScope(context, node))) !== null &&
275
+ (typeof staticValue.value === "string" || typeof staticValue.value === "number")
276
+ ) {
277
+ // One of the first things Solid (actually babel-plugin-dom-expressions) does with an
278
+ // attribute is determine if it can be inlined into a template string instead of
279
+ // injected programmatically. It runs
280
+ // `attribute.get("value").get("expression").evaluate().value` on attributes with
281
+ // JSXExpressionContainers, and if the statically evaluated value is a string or number,
282
+ // it inlines it. This runs even for attributes that follow the naming convention for
283
+ // event handlers. By starting an attribute name with "on", the user has signalled that
284
+ // they intend the attribute to be an event handler. If the attribute value would be
285
+ // inlined, report that.
286
+ // https://github.com/ryansolid/dom-expressions/blob/cb3be7558c731e2a442e9c7e07d25373c40cf2be/packages/babel-plugin-jsx-dom-expressions/src/dom/element.js#L347
287
+ context.report({
288
+ node,
289
+ messageId: "detected-attr",
290
+ data: {
291
+ name,
292
+ staticValue: staticValue.value,
293
+ },
294
+ });
295
+ } else if (node.value === null || node.value?.type === "Literal") {
296
+ // Check for same as above for literal values
297
+ context.report({
298
+ node,
299
+ messageId: "detected-attr",
300
+ data: {
301
+ name,
302
+ staticValue: node.value !== null ? node.value.value : true,
303
+ },
304
+ });
305
+ } else if (!context.options[0]?.ignoreCase) {
306
+ const lowercaseHandlerName = name.toLowerCase();
307
+ if (isNonstandardEventName(lowercaseHandlerName)) {
308
+ const fixedName = getStandardEventHandlerName(lowercaseHandlerName);
309
+ context.report({
310
+ node: node.name,
311
+ messageId: "nonstandard",
312
+ data: { name, fixedName },
313
+ fix: (fixer) => fixer.replaceText(node.name, fixedName),
314
+ });
315
+ } else if (isCommonHandlerName(lowercaseHandlerName)) {
316
+ const fixedName = getCommonEventHandlerName(lowercaseHandlerName);
317
+ if (fixedName !== name) {
318
+ // For common DOM event names, we know the user intended the prop to be an event handler.
319
+ // Fix it to have an uppercase third letter and be properly camel-cased.
320
+ context.report({
321
+ node: node.name,
322
+ messageId: "capitalization",
323
+ data: { name, fixedName },
324
+ fix: (fixer) => fixer.replaceText(node.name, fixedName),
325
+ });
326
+ }
327
+ } else if (name[2] === name[2].toLowerCase()) {
328
+ // this includes words like `only` and `ongoing` as well as unknown handlers like `onfoobar`.
329
+ // Enforce using either /^on[A-Z]/ (event handler) or /^attr:on[a-z]/ (forced regular attribute)
330
+ // to make user intent clear and code maximally readable
331
+ const handlerName = `on${name[2].toUpperCase()}${name.slice(3)}`;
332
+ const attrName = `attr:${name}`;
333
+ context.report({
334
+ node: node.name,
335
+ messageId: "naming",
336
+ data: { name, attrName, handlerName },
337
+ suggest: [
338
+ {
339
+ messageId: "make-handler",
340
+ data: { name, handlerName },
341
+ fix: (fixer) => fixer.replaceText(node.name, handlerName),
342
+ },
343
+ {
344
+ messageId: "make-attr",
345
+ data: { name, attrName },
346
+ fix: (fixer) => fixer.replaceText(node.name, attrName),
347
+ },
348
+ ],
349
+ });
350
+ }
351
+ }
352
+ },
353
+ Property(node: T.Property) {
354
+ if (
355
+ context.options[0]?.warnOnSpread &&
356
+ node.parent?.type === "ObjectExpression" &&
357
+ node.parent.parent?.type === "JSXSpreadAttribute" &&
358
+ node.parent.parent.parent?.type === "JSXOpeningElement"
359
+ ) {
360
+ const openingElement = node.parent.parent.parent;
361
+ if (
362
+ openingElement.name.type === "JSXIdentifier" &&
363
+ isDOMElementName(openingElement.name.name)
364
+ ) {
365
+ if (node.key.type === "Identifier" && /^on/.test(node.key.name)) {
366
+ const handlerName = node.key.name;
367
+ // An event handler is being spread in (ex. <button {...{ onClick }} />), which doesn't
368
+ // actually add an event listener, just a plain attribute.
369
+ context.report({
370
+ node,
371
+ messageId: "spread-handler",
372
+ data: {
373
+ name: node.key.name,
374
+ },
375
+ *fix(fixer) {
376
+ const commaAfter = sourceCode.getTokenAfter(node);
377
+ yield fixer.remove(
378
+ (node.parent as T.ObjectExpression).properties.length === 1
379
+ ? node.parent!.parent!
380
+ : node
381
+ );
382
+ if (commaAfter?.value === ",") {
383
+ yield fixer.remove(commaAfter);
384
+ }
385
+ yield fixer.insertTextAfter(
386
+ node.parent!.parent!,
387
+ ` ${handlerName}={${sourceCode.getText(node.value)}}`
388
+ );
389
+ },
390
+ });
391
+ }
392
+ }
393
+ }
394
+ },
395
+ };
396
+ },
397
+ });