@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.
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/dist/chunk-4UZNJ5MS.mjs +32 -0
- package/dist/chunk-4UZNJ5MS.mjs.map +1 -0
- package/dist/chunk-EBU4C5NV.mjs +77 -0
- package/dist/chunk-EBU4C5NV.mjs.map +1 -0
- package/dist/chunk-LE6SW2M3.mjs +6231 -0
- package/dist/chunk-LE6SW2M3.mjs.map +1 -0
- package/dist/chunk-MHDQJMLE.mjs +39 -0
- package/dist/chunk-MHDQJMLE.mjs.map +1 -0
- package/dist/configs/recommended.d.mts +109 -0
- package/dist/configs/recommended.d.ts +109 -0
- package/dist/configs/recommended.js +5877 -0
- package/dist/configs/recommended.js.map +1 -0
- package/dist/configs/recommended.mjs +5 -0
- package/dist/configs/recommended.mjs.map +1 -0
- package/dist/configs/typescript.d.mts +103 -0
- package/dist/configs/typescript.d.ts +103 -0
- package/dist/configs/typescript.js +6246 -0
- package/dist/configs/typescript.js.map +1 -0
- package/dist/configs/typescript.mjs +6 -0
- package/dist/configs/typescript.mjs.map +1 -0
- package/dist/configs/v2-strict.d.mts +130 -0
- package/dist/configs/v2-strict.d.ts +130 -0
- package/dist/configs/v2-strict.js +6317 -0
- package/dist/configs/v2-strict.js.map +1 -0
- package/dist/configs/v2-strict.mjs +7 -0
- package/dist/configs/v2-strict.mjs.map +1 -0
- package/dist/configs/v2.d.mts +127 -0
- package/dist/configs/v2.d.ts +127 -0
- package/dist/configs/v2.js +6289 -0
- package/dist/configs/v2.js.map +1 -0
- package/dist/configs/v2.mjs +6 -0
- package/dist/configs/v2.mjs.map +1 -0
- package/dist/index.d.mts +740 -0
- package/dist/index.d.ts +740 -0
- package/dist/index.js +6365 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +42 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +109 -0
- package/src/compat.ts +53 -0
- package/src/configs/recommended.ts +48 -0
- package/src/configs/typescript.ts +21 -0
- package/src/configs/v2-strict.ts +28 -0
- package/src/configs/v2.ts +68 -0
- package/src/deps.d.ts +1 -0
- package/src/index.ts +29 -0
- package/src/plugin.ts +92 -0
- package/src/rules/components-return-once.ts +209 -0
- package/src/rules/event-handlers.ts +397 -0
- package/src/rules/imports.ts +338 -0
- package/src/rules/jsx-no-duplicate-props.ts +110 -0
- package/src/rules/jsx-no-script-url.ts +61 -0
- package/src/rules/jsx-no-undef.ts +230 -0
- package/src/rules/jsx-uses-vars.ts +66 -0
- package/src/rules/no-accessor-as-prop.ts +147 -0
- package/src/rules/no-array-handlers.ts +57 -0
- package/src/rules/no-boolean-enumerated-attribute.ts +209 -0
- package/src/rules/no-browser-globals-in-server-function.ts +108 -0
- package/src/rules/no-destructure.ts +245 -0
- package/src/rules/no-innerhtml.ts +143 -0
- package/src/rules/no-invalid-server-capture.ts +104 -0
- package/src/rules/no-module-scope-reactive-primitive.ts +69 -0
- package/src/rules/no-proxy-apis.ts +96 -0
- package/src/rules/no-react-deps.ts +68 -0
- package/src/rules/no-react-specific-props.ts +60 -0
- package/src/rules/no-reactive-read-in-effect-callback.ts +570 -0
- package/src/rules/no-restated-default-options.ts +96 -0
- package/src/rules/no-single-arg-create-effect.ts +56 -0
- package/src/rules/no-store-mutation-outside-setter.ts +137 -0
- package/src/rules/no-unknown-namespaces.ts +140 -0
- package/src/rules/no-unused-signal.ts +119 -0
- package/src/rules/no-write-in-pure-computation.ts +231 -0
- package/src/rules/prefer-classlist.ts +93 -0
- package/src/rules/prefer-for.ts +93 -0
- package/src/rules/prefer-onSettled-for-side-effects.ts +101 -0
- package/src/rules/prefer-show.ts +102 -0
- package/src/rules/prefer-structured-class.ts +117 -0
- package/src/rules/reactivity.ts +1730 -0
- package/src/rules/removed-api.ts +212 -0
- package/src/rules/require-async-server-function.ts +122 -0
- package/src/rules/self-closing-comp.ts +150 -0
- package/src/rules/style-prop.ts +140 -0
- package/src/rules/valid-use-server.ts +192 -0
- package/src/rules/validate-jsx-nesting.ts +1 -0
- package/src/utils/reactive-imports.ts +382 -0
- package/src/utils/reactive-types.ts +272 -0
- package/src/utils/reactive-values.ts +162 -0
- package/src/utils.ts +442 -0
|
@@ -0,0 +1,230 @@
|
|
|
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 { isDOMElementName, formatList, appendImports, insertImports, isSolidV2 } from "../utils";
|
|
11
|
+
import { getScope, getSourceCode } from "../compat";
|
|
12
|
+
|
|
13
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
14
|
+
|
|
15
|
+
// Currently all of the control flow components are from 'solid-js'.
|
|
16
|
+
const AUTO_COMPONENTS = ["Show", "For", "Index", "Switch", "Match"];
|
|
17
|
+
// Solid 2.0 control flow components; `Index`, `ErrorBoundary`, `Suspense`, and
|
|
18
|
+
// `SuspenseList` no longer exist (see solid/removed-api).
|
|
19
|
+
const AUTO_COMPONENTS_V2 = [
|
|
20
|
+
"For",
|
|
21
|
+
"Repeat",
|
|
22
|
+
"Show",
|
|
23
|
+
"Switch",
|
|
24
|
+
"Match",
|
|
25
|
+
"Errored",
|
|
26
|
+
"Loading",
|
|
27
|
+
"Reveal",
|
|
28
|
+
];
|
|
29
|
+
const SOURCE_MODULE = "solid-js";
|
|
30
|
+
|
|
31
|
+
/*
|
|
32
|
+
* This rule is adapted from eslint-plugin-react's jsx-no-undef rule under
|
|
33
|
+
* the MIT license. Thank you for your work!
|
|
34
|
+
*/
|
|
35
|
+
type MessageIds = "undefined" | "customDirectiveUndefined" | "autoImport";
|
|
36
|
+
type Options = [
|
|
37
|
+
{
|
|
38
|
+
allowGlobals?: boolean;
|
|
39
|
+
autoImport?: boolean;
|
|
40
|
+
typescriptEnabled?: boolean;
|
|
41
|
+
}?
|
|
42
|
+
];
|
|
43
|
+
export default createRule<Options, MessageIds>({
|
|
44
|
+
meta: {
|
|
45
|
+
type: "problem",
|
|
46
|
+
docs: {
|
|
47
|
+
description: "Disallow references to undefined variables in JSX. Handles custom directives.",
|
|
48
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-no-undef.md",
|
|
49
|
+
},
|
|
50
|
+
fixable: "code",
|
|
51
|
+
schema: [
|
|
52
|
+
{
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
allowGlobals: {
|
|
56
|
+
type: "boolean",
|
|
57
|
+
description:
|
|
58
|
+
"When true, the rule will consider the global scope when checking for defined components.",
|
|
59
|
+
default: false,
|
|
60
|
+
},
|
|
61
|
+
autoImport: {
|
|
62
|
+
type: "boolean",
|
|
63
|
+
description:
|
|
64
|
+
'Automatically import certain components from `"solid-js"` if they are undefined.',
|
|
65
|
+
default: true,
|
|
66
|
+
},
|
|
67
|
+
typescriptEnabled: {
|
|
68
|
+
type: "boolean",
|
|
69
|
+
description: "Adjusts behavior not to conflict with TypeScript's type checking.",
|
|
70
|
+
default: false,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
messages: {
|
|
77
|
+
undefined: "'{{identifier}}' is not defined.",
|
|
78
|
+
customDirectiveUndefined: "Custom directive '{{identifier}}' is not defined.",
|
|
79
|
+
autoImport: "{{imports}} should be imported from '{{source}}'.",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
defaultOptions: [],
|
|
83
|
+
create(context) {
|
|
84
|
+
const allowGlobals = context.options[0]?.allowGlobals ?? false;
|
|
85
|
+
const autoImport = context.options[0]?.autoImport !== false;
|
|
86
|
+
const isTypeScriptEnabled = context.options[0]?.typescriptEnabled ?? false;
|
|
87
|
+
const autoComponents = isSolidV2(context) ? AUTO_COMPONENTS_V2 : AUTO_COMPONENTS;
|
|
88
|
+
|
|
89
|
+
const missingComponentsSet = new Set<string>();
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Compare an identifier with the variables declared in the scope
|
|
93
|
+
* @param {ASTNode} node - Identifier or JSXIdentifier node
|
|
94
|
+
* @returns {void}
|
|
95
|
+
*/
|
|
96
|
+
function checkIdentifierInJSX(
|
|
97
|
+
node: T.Identifier | T.JSXIdentifier,
|
|
98
|
+
{
|
|
99
|
+
isComponent,
|
|
100
|
+
isCustomDirective,
|
|
101
|
+
}: { isComponent?: boolean; isCustomDirective?: boolean } = {}
|
|
102
|
+
) {
|
|
103
|
+
let scope = getScope(context, node);
|
|
104
|
+
const sourceCode = getSourceCode(context);
|
|
105
|
+
const sourceType = sourceCode.ast.sourceType;
|
|
106
|
+
const scopeUpperBound = !allowGlobals && sourceType === "module" ? "module" : "global";
|
|
107
|
+
const variables = [...scope.variables];
|
|
108
|
+
|
|
109
|
+
// Ignore 'this' keyword (also maked as JSXIdentifier when used in JSX)
|
|
110
|
+
if (node.name === "this") {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
while (scope.type !== scopeUpperBound && scope.type !== "global" && scope.upper) {
|
|
115
|
+
scope = scope.upper;
|
|
116
|
+
variables.push(...scope.variables);
|
|
117
|
+
}
|
|
118
|
+
if (scope.childScopes.length) {
|
|
119
|
+
variables.push(...scope.childScopes[0].variables);
|
|
120
|
+
// Temporary fix for babel-eslint
|
|
121
|
+
if (scope.childScopes[0].childScopes.length) {
|
|
122
|
+
variables.push(...scope.childScopes[0].childScopes[0].variables);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (variables.find((variable) => variable.name === node.name)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
isComponent &&
|
|
132
|
+
autoImport &&
|
|
133
|
+
autoComponents.includes(node.name) &&
|
|
134
|
+
!missingComponentsSet.has(node.name)
|
|
135
|
+
) {
|
|
136
|
+
// track which names are undefined
|
|
137
|
+
missingComponentsSet.add(node.name);
|
|
138
|
+
} else if (isCustomDirective) {
|
|
139
|
+
context.report({
|
|
140
|
+
node,
|
|
141
|
+
messageId: "customDirectiveUndefined",
|
|
142
|
+
data: {
|
|
143
|
+
identifier: node.name,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
} else if (!isTypeScriptEnabled) {
|
|
147
|
+
context.report({
|
|
148
|
+
node,
|
|
149
|
+
messageId: "undefined",
|
|
150
|
+
data: {
|
|
151
|
+
identifier: node.name,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
JSXOpeningElement(node) {
|
|
159
|
+
let n: T.Node | undefined;
|
|
160
|
+
switch (node.name.type) {
|
|
161
|
+
case "JSXIdentifier":
|
|
162
|
+
if (!isDOMElementName(node.name.name)) {
|
|
163
|
+
checkIdentifierInJSX(node.name, { isComponent: true });
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
case "JSXMemberExpression":
|
|
167
|
+
n = node.name;
|
|
168
|
+
do {
|
|
169
|
+
n = (n as any).object;
|
|
170
|
+
} while (n && n.type !== "JSXIdentifier");
|
|
171
|
+
if (n) {
|
|
172
|
+
checkIdentifierInJSX(n);
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
default:
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
"JSXAttribute > JSXNamespacedName": (node: T.JSXNamespacedName) => {
|
|
180
|
+
// <Element use:X /> applies the `X` custom directive to the element, where `X` must be an identifier in scope.
|
|
181
|
+
if (
|
|
182
|
+
node.namespace?.type === "JSXIdentifier" &&
|
|
183
|
+
node.namespace.name === "use" &&
|
|
184
|
+
node.name?.type === "JSXIdentifier"
|
|
185
|
+
) {
|
|
186
|
+
checkIdentifierInJSX(node.name, { isCustomDirective: true });
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
"Program:exit": (programNode: T.Program) => {
|
|
190
|
+
// add in any auto import components used in the program
|
|
191
|
+
const missingComponents = Array.from(missingComponentsSet.values());
|
|
192
|
+
if (autoImport && missingComponents.length) {
|
|
193
|
+
const importNode = programNode.body.find(
|
|
194
|
+
(n) =>
|
|
195
|
+
n.type === "ImportDeclaration" &&
|
|
196
|
+
n.importKind !== "type" &&
|
|
197
|
+
n.source.type === "Literal" &&
|
|
198
|
+
n.source.value === SOURCE_MODULE
|
|
199
|
+
) as T.ImportDeclaration | undefined;
|
|
200
|
+
if (importNode) {
|
|
201
|
+
context.report({
|
|
202
|
+
node: importNode,
|
|
203
|
+
messageId: "autoImport",
|
|
204
|
+
data: {
|
|
205
|
+
imports: formatList(missingComponents), // "Show, For, and Switch"
|
|
206
|
+
source: SOURCE_MODULE,
|
|
207
|
+
},
|
|
208
|
+
fix: (fixer) => {
|
|
209
|
+
return appendImports(fixer, getSourceCode(context), importNode, missingComponents);
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
} else {
|
|
213
|
+
context.report({
|
|
214
|
+
node: programNode,
|
|
215
|
+
messageId: "autoImport",
|
|
216
|
+
data: {
|
|
217
|
+
imports: formatList(missingComponents),
|
|
218
|
+
source: SOURCE_MODULE,
|
|
219
|
+
},
|
|
220
|
+
fix: (fixer) => {
|
|
221
|
+
// insert `import { missing, identifiers } from "solid-js"` at top of module
|
|
222
|
+
return insertImports(fixer, getSourceCode(context), "solid-js", missingComponents);
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
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 { markVariableAsUsed } from "../compat";
|
|
11
|
+
|
|
12
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* This rule is lifted almost verbatim from eslint-plugin-react's
|
|
16
|
+
* jsx-uses-vars rule under the MIT license. Thank you for your work!
|
|
17
|
+
* Solid's custom directives are also handled.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export default createRule({
|
|
21
|
+
meta: {
|
|
22
|
+
type: "problem",
|
|
23
|
+
docs: {
|
|
24
|
+
// eslint-disable-next-line eslint-plugin/require-meta-docs-description
|
|
25
|
+
description: "Prevent variables used in JSX from being marked as unused.",
|
|
26
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-uses-vars.md",
|
|
27
|
+
},
|
|
28
|
+
schema: [],
|
|
29
|
+
// eslint-disable-next-line eslint-plugin/prefer-message-ids
|
|
30
|
+
messages: {},
|
|
31
|
+
},
|
|
32
|
+
defaultOptions: [],
|
|
33
|
+
create(context) {
|
|
34
|
+
return {
|
|
35
|
+
JSXOpeningElement(node) {
|
|
36
|
+
let parent: T.JSXTagNameExpression;
|
|
37
|
+
switch (node.name.type) {
|
|
38
|
+
case "JSXNamespacedName": // <Foo:Bar>
|
|
39
|
+
return;
|
|
40
|
+
case "JSXIdentifier": // <Foo>
|
|
41
|
+
markVariableAsUsed(context, node.name.name, node.name);
|
|
42
|
+
break;
|
|
43
|
+
case "JSXMemberExpression": // <Foo...Bar>
|
|
44
|
+
parent = node.name.object;
|
|
45
|
+
while (parent?.type === "JSXMemberExpression") {
|
|
46
|
+
parent = parent.object;
|
|
47
|
+
}
|
|
48
|
+
if (parent.type === "JSXIdentifier") {
|
|
49
|
+
markVariableAsUsed(context, parent.name, parent);
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"JSXAttribute > JSXNamespacedName": (node: T.JSXNamespacedName) => {
|
|
55
|
+
// <Element use:X /> applies the `X` custom directive to the element, where `X` must be an identifier in scope.
|
|
56
|
+
if (
|
|
57
|
+
node.namespace?.type === "JSXIdentifier" &&
|
|
58
|
+
node.namespace.name === "use" &&
|
|
59
|
+
node.name?.type === "JSXIdentifier"
|
|
60
|
+
) {
|
|
61
|
+
markVariableAsUsed(context, node.name.name, node.name);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
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 { findVariable } from "../compat";
|
|
11
|
+
import { isDOMElementName, isFunctionNode, getSolidSourceRegex, trackImports } from "../utils";
|
|
12
|
+
|
|
13
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
14
|
+
|
|
15
|
+
type MessageIds = "accessorAsProp" | "functionAsProp";
|
|
16
|
+
type Options = [];
|
|
17
|
+
|
|
18
|
+
// Attributes on DOM elements that legitimately accept a function value.
|
|
19
|
+
const FUNCTION_ATTRIBUTES = new Set(["ref", "children"]);
|
|
20
|
+
|
|
21
|
+
// camelCase `onX` names are event handlers; lowercase `onx` names are literal
|
|
22
|
+
// attributes in Solid 2.0 but are solid/event-handlers' territory — skip both
|
|
23
|
+
// here so a single node never gets two reports.
|
|
24
|
+
const isOnPrefixed = (name: string) => name.length > 2 && name.startsWith("on");
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* Passing an uncalled signal accessor as a DOM element attribute
|
|
28
|
+
* (`<div title={count} />`) silently sets the attribute to a stringified
|
|
29
|
+
* function. TypeScript flags this with a cryptic assignability error that
|
|
30
|
+
* many pipelines never run (Vite builds don't typecheck); this rule is the
|
|
31
|
+
* enforcement and translation layer. Unlike solid/reactivity's signal
|
|
32
|
+
* tracing, any expression that resolves to a function fires here.
|
|
33
|
+
*/
|
|
34
|
+
export default createRule<Options, MessageIds>({
|
|
35
|
+
meta: {
|
|
36
|
+
type: "problem",
|
|
37
|
+
docs: {
|
|
38
|
+
description:
|
|
39
|
+
"Disallow passing uncalled signal accessors or other functions as value-typed DOM element attributes.",
|
|
40
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-accessor-as-prop.md",
|
|
41
|
+
},
|
|
42
|
+
hasSuggestions: true,
|
|
43
|
+
schema: [],
|
|
44
|
+
messages: {
|
|
45
|
+
accessorAsProp:
|
|
46
|
+
"'{{attribute}}' expects a value, but '{{name}}' is a signal accessor — call it: '{{name}}()'.",
|
|
47
|
+
functionAsProp:
|
|
48
|
+
"'{{attribute}}' expects a value, but this is a function; it would render as a stringified function. If it derives a value, call it.",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
defaultOptions: [],
|
|
52
|
+
create(context) {
|
|
53
|
+
const { matchImport, handleImportDeclaration } = trackImports(getSolidSourceRegex(context));
|
|
54
|
+
|
|
55
|
+
/** Is `id` a signal accessor or plain function we can resolve statically? */
|
|
56
|
+
const resolveFunctionKind = (id: T.Identifier): "accessor" | "function" | null => {
|
|
57
|
+
const variable = findVariable(context, id);
|
|
58
|
+
const def = variable?.defs[0];
|
|
59
|
+
if (!def) return null;
|
|
60
|
+
|
|
61
|
+
if (def.type === "FunctionName") return "function";
|
|
62
|
+
if (def.type !== "Variable" || !def.node.init) return null;
|
|
63
|
+
|
|
64
|
+
const { id: declId, init } = def.node;
|
|
65
|
+
// const count = () => ...;
|
|
66
|
+
if (declId.type === "Identifier" && isFunctionNode(init)) return "function";
|
|
67
|
+
if (init.type !== "CallExpression" || init.callee.type !== "Identifier") return null;
|
|
68
|
+
|
|
69
|
+
// const memo = createMemo(...)
|
|
70
|
+
if (
|
|
71
|
+
declId.type === "Identifier" &&
|
|
72
|
+
matchImport(["createMemo", "children"], init.callee.name)
|
|
73
|
+
) {
|
|
74
|
+
return "accessor";
|
|
75
|
+
}
|
|
76
|
+
// const [count] = createSignal(...); const [value] = createOptimistic(...)
|
|
77
|
+
if (
|
|
78
|
+
declId.type === "ArrayPattern" &&
|
|
79
|
+
declId.elements[0]?.type === "Identifier" &&
|
|
80
|
+
declId.elements[0].name === id.name &&
|
|
81
|
+
matchImport(["createSignal", "createOptimistic"], init.callee.name)
|
|
82
|
+
) {
|
|
83
|
+
return "accessor";
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
ImportDeclaration: handleImportDeclaration,
|
|
90
|
+
JSXAttribute(node) {
|
|
91
|
+
// Only plain attributes on native DOM elements; namespaced attributes
|
|
92
|
+
// and custom elements can legitimately carry unusual values.
|
|
93
|
+
if (node.name.type !== "JSXIdentifier") return;
|
|
94
|
+
const attribute = node.name.name;
|
|
95
|
+
if (FUNCTION_ATTRIBUTES.has(attribute) || isOnPrefixed(attribute)) return;
|
|
96
|
+
|
|
97
|
+
const element = node.parent as T.JSXOpeningElement;
|
|
98
|
+
if (
|
|
99
|
+
element.name.type !== "JSXIdentifier" ||
|
|
100
|
+
!isDOMElementName(element.name.name) ||
|
|
101
|
+
element.name.name.includes("-")
|
|
102
|
+
) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (node.value?.type !== "JSXExpressionContainer") return;
|
|
107
|
+
const expression = node.value.expression;
|
|
108
|
+
|
|
109
|
+
if (isFunctionNode(expression)) {
|
|
110
|
+
context.report({ node: expression, messageId: "functionAsProp", data: { attribute } });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (expression.type === "Identifier") {
|
|
115
|
+
const kind = resolveFunctionKind(expression);
|
|
116
|
+
if (kind === "accessor") {
|
|
117
|
+
context.report({
|
|
118
|
+
node: expression,
|
|
119
|
+
messageId: "accessorAsProp",
|
|
120
|
+
data: { attribute, name: expression.name },
|
|
121
|
+
suggest: [
|
|
122
|
+
{
|
|
123
|
+
messageId: "accessorAsProp",
|
|
124
|
+
data: { attribute, name: expression.name },
|
|
125
|
+
fix: (fixer) => fixer.insertTextAfter(expression, "()"),
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
} else if (kind === "function") {
|
|
130
|
+
context.report({
|
|
131
|
+
node: expression,
|
|
132
|
+
messageId: "functionAsProp",
|
|
133
|
+
data: { attribute },
|
|
134
|
+
suggest: [
|
|
135
|
+
{
|
|
136
|
+
messageId: "functionAsProp",
|
|
137
|
+
data: { attribute },
|
|
138
|
+
fix: (fixer) => fixer.insertTextAfter(expression, "()"),
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
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 { isDOMElementName, trace } from "../utils";
|
|
11
|
+
|
|
12
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
13
|
+
|
|
14
|
+
export default createRule({
|
|
15
|
+
meta: {
|
|
16
|
+
type: "problem",
|
|
17
|
+
docs: {
|
|
18
|
+
description: "Disallow usage of type-unsafe event handlers.",
|
|
19
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-array-handlers.md",
|
|
20
|
+
},
|
|
21
|
+
schema: [],
|
|
22
|
+
messages: {
|
|
23
|
+
noArrayHandlers: "Passing an array as an event handler is potentially type-unsafe.",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultOptions: [],
|
|
27
|
+
create(context) {
|
|
28
|
+
return {
|
|
29
|
+
JSXAttribute(node) {
|
|
30
|
+
const openingElement = node.parent as T.JSXOpeningElement;
|
|
31
|
+
if (
|
|
32
|
+
openingElement.name.type !== "JSXIdentifier" ||
|
|
33
|
+
!isDOMElementName(openingElement.name.name)
|
|
34
|
+
) {
|
|
35
|
+
return; // bail if this is not a DOM/SVG element or web component
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isNamespacedHandler =
|
|
39
|
+
node.name.type === "JSXNamespacedName" && node.name.namespace.name === "on";
|
|
40
|
+
const isNormalEventHandler =
|
|
41
|
+
node.name.type === "JSXIdentifier" && /^on[a-zA-Z]/.test(node.name.name);
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
(isNamespacedHandler || isNormalEventHandler) &&
|
|
45
|
+
node.value?.type === "JSXExpressionContainer" &&
|
|
46
|
+
trace(node.value.expression, context).type === "ArrayExpression"
|
|
47
|
+
) {
|
|
48
|
+
// Warn if passed an array
|
|
49
|
+
context.report({
|
|
50
|
+
node,
|
|
51
|
+
messageId: "noArrayHandlers",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
});
|