Skip to content

Commit c9c6462

Browse files
authored
Merge pull request #75 from HatulaPro/main
Add rule solid/no-array-handlers
2 parents 98fb997 + 7299a8a commit c9c6462

File tree

5 files changed

+224
-0
lines changed

5 files changed

+224
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ const [editedValue, setEditedValue] = createSignal(props.value);
128128
|| | [solid/jsx-no-script-url](docs/jsx-no-script-url.md) | Disallow javascript: URLs. |
129129
|| 🔧 | [solid/jsx-no-undef](docs/jsx-no-undef.md) | Disallow references to undefined variables in JSX. Handles custom directives. |
130130
|| | [solid/jsx-uses-vars](docs/jsx-uses-vars.md) | Prevent variables used in JSX from being marked as unused. |
131+
| | | [solid/no-array-handlers](docs/no-array-handlers.md) | Disallow usage of unsafe event handlers. |
131132
|| 🔧 | [solid/no-destructure](docs/no-destructure.md) | Disallow destructuring props. In Solid, props must be used with property accesses (`props.foo`) to preserve reactivity. This rule only tracks destructuring in the parameter list. |
132133
|| 🔧 | [solid/no-innerhtml](docs/no-innerhtml.md) | Disallow usage of the innerHTML attribute, which can often lead to security vulnerabilities. |
133134
| | | [solid/no-proxy-apis](docs/no-proxy-apis.md) | Disallow usage of APIs that use ES6 Proxies, only to target environments that don't support them. |

docs/no-array-handlers.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<!-- AUTO-GENERATED-CONTENT:START (HEADER) -->
2+
# solid/no-array-handlers
3+
Disallow usage of unsafe event handlers.
4+
This rule is **off** by default.
5+
6+
[View source](../src/rules/no-array-handlers.ts) · [View tests](../test/rules/no-array-handlers.test.ts)
7+
8+
<!-- AUTO-GENERATED-CONTENT:END -->
9+
10+
<!-- AUTO-GENERATED-CONTENT:START (OPTIONS) -->
11+
12+
<!-- AUTO-GENERATED-CONTENT:END -->
13+
14+
<!-- AUTO-GENERATED-CONTENT:START (CASES) -->
15+
## Tests
16+
17+
### Invalid Examples
18+
19+
These snippets cause lint errors.
20+
21+
```js
22+
let el = <button onClick={[(n) => console.log(n), "str"]} />;
23+
24+
let el = <button onClick={[(k: string) => k.toUpperCase(), "hello"]} />;
25+
26+
let el = <div onMouseOver={[1, 2, 3]} />;
27+
28+
let el = <div on:click={[handler, i()]} />;
29+
30+
let el = <button type="button" onclick={[handler, i() + 2]} class="btn" />;
31+
32+
let handler = [(x) => x * 2, 54];
33+
let el = <button style={{ background: "pink" }} onclick={handler} />;
34+
35+
const thing = (props) => (
36+
<div onclick={[props.callback, props.id]}>
37+
<button type="button" onclick={handler} class="btn" />
38+
</div>
39+
);
40+
41+
function Component() {
42+
const arr = [(n: number) => n * n, 2];
43+
return <div onClick={arr} />;
44+
}
45+
46+
```
47+
48+
### Valid Examples
49+
50+
These snippets don't cause lint errors.
51+
52+
```js
53+
let el = <button style={{ background: "red" }} onClick={() => 9001} />;
54+
55+
const handler = () => 1 + 1;
56+
let el = <button onClick={handler} />;
57+
58+
let el = <button onclick={() => 9001} />;
59+
60+
const handler = () => 1 + 1;
61+
let el = <button style={{ background: "pink" }} onclick={handler} />;
62+
63+
let el = <button attr:click={[(x) => x, 9001]} />;
64+
65+
let el = <button prop:onClick={[(x) => x, 9001]} />;
66+
67+
let el = <button on:Click={() => 1 + 1} />;
68+
69+
function Component(props) {
70+
return <div onClick={props.onClick} />;
71+
}
72+
73+
<button onClick={() => [handler, "abc"]} />;
74+
75+
<button onClick={() => [handler, { data: true }]} />;
76+
77+
function Component() {
78+
return <div onClick={[(n: number) => n * n, 2] as SafeArray<number>} />;
79+
}
80+
81+
```
82+
<!-- AUTO-GENERATED-CONTENT:END -->

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import preferShow from "./rules/prefer-show";
1717
import reactivity from "./rules/reactivity";
1818
import selfClosingComp from "./rules/self-closing-comp";
1919
import styleProp from "./rules/style-prop";
20+
import noArrayHandlers from "./rules/no-array-handlers";
2021
// import validateJsxNesting from "./rules/validate-jsx-nesting";
2122

2223
const allRules = {
@@ -39,6 +40,7 @@ const allRules = {
3940
reactivity,
4041
"self-closing-comp": selfClosingComp,
4142
"style-prop": styleProp,
43+
"no-array-handlers": noArrayHandlers,
4244
// "validate-jsx-nesting": validateJsxNesting
4345
};
4446

@@ -81,6 +83,7 @@ const plugin = {
8183
"solid/no-react-deps": 1,
8284
"solid/no-react-specific-props": 1,
8385
"solid/self-closing-comp": 1,
86+
"solid/no-array-handlers": 0,
8487
// handled by Solid compiler, opt-in style suggestion
8588
"solid/prefer-show": 0,
8689
// only necessary for resource-constrained environments
@@ -115,6 +118,7 @@ const plugin = {
115118
"solid/no-react-deps": 1,
116119
"solid/no-react-specific-props": 1,
117120
"solid/self-closing-comp": 1,
121+
"solid/no-array-handlers": 0,
118122
// namespaces taken care of by TS
119123
"solid/no-unknown-namespaces": 0,
120124
// handled by Solid compiler, opt-in style suggestion

src/rules/no-array-handlers.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { TSESTree as T, TSESLint } from "@typescript-eslint/utils";
2+
import { isDOMElementName, trace } from "../utils";
3+
4+
const rule: TSESLint.RuleModule<"noArrayHandlers", []> = {
5+
meta: {
6+
type: "problem",
7+
docs: {
8+
recommended: false,
9+
description: "Disallow usage of unsafe event handlers.",
10+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/docs/no-array-handlers.md",
11+
},
12+
schema: [],
13+
messages: { noArrayHandlers: "Passing an array as an event handler is potentially unsafe." },
14+
},
15+
create(context) {
16+
return {
17+
JSXAttribute(node) {
18+
const openingElement = node.parent as T.JSXOpeningElement;
19+
if (
20+
openingElement.name.type !== "JSXIdentifier" ||
21+
!isDOMElementName(openingElement.name.name)
22+
) {
23+
return; // bail if this is not a DOM/SVG element or web component
24+
}
25+
26+
if (node.name.type === "JSXNamespacedName") {
27+
if (
28+
node.name.namespace.name === "on" &&
29+
node.value?.type === "JSXExpressionContainer" &&
30+
node.value.expression.type === "ArrayExpression"
31+
) {
32+
// Handling events that start with "on:"
33+
context.report({
34+
node,
35+
messageId: "noArrayHandlers",
36+
});
37+
}
38+
return; // bali if it's not an event namespace
39+
}
40+
41+
// string name of the name node
42+
const { name } = node.name;
43+
44+
if (!/^on[a-zA-Z].*$/.test(name)) {
45+
return; // bail if Solid doesn't consider the prop name an event handler
46+
}
47+
48+
if (node.value?.type === "JSXExpressionContainer") {
49+
if (node.value.expression.type === "ArrayExpression") {
50+
// If passed an array
51+
context.report({
52+
node,
53+
messageId: "noArrayHandlers",
54+
});
55+
} else if (node.value.expression.type === "Identifier") {
56+
const traced = trace(node.value.expression, context.getScope());
57+
if (traced.type === "ArrayExpression") {
58+
context.report({
59+
node,
60+
messageId: "noArrayHandlers",
61+
});
62+
}
63+
}
64+
}
65+
},
66+
};
67+
},
68+
};
69+
70+
export default rule;

test/rules/no-array-handlers.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { tsOnlyTest, run } from "../ruleTester";
2+
import rule from "../../src/rules/no-array-handlers";
3+
4+
export const cases = run("no-array-handlers", rule, {
5+
valid: [
6+
`let el = <button style={{background: 'red'}} onClick={() => 9001} />`,
7+
`const handler = () => 1+1;
8+
let el = <button onClick={handler} />`,
9+
`let el = <button onclick={() => 9001} />`,
10+
`const handler = () => 1+1;
11+
let el = <button style={{background: 'pink'}} onclick={handler} />`,
12+
`let el = <button attr:click={[(x) => x, 9001]} />`,
13+
`let el = <button prop:onClick={[(x) => x, 9001]} />`,
14+
`let el = <button on:Click={() => 1+1} />`,
15+
`function Component(props) {
16+
return <div onClick={props.onClick} />;
17+
}`,
18+
`<button onClick={() => [handler, "abc"]} />`,
19+
`<button onClick={() => [handler, {data:true}]} /> `,
20+
{
21+
code: `function Component() {
22+
return <div onClick={[(n: number) => n*n, 2] as SafeArray<number>} />;
23+
}`,
24+
...tsOnlyTest,
25+
},
26+
],
27+
invalid: [
28+
{
29+
code: `let el = <button onClick={[(n) => console.log(n), 'str']} />`,
30+
errors: [{ messageId: "noArrayHandlers" }],
31+
},
32+
{
33+
code: `let el = <button onClick={[(k: string) => k.toUpperCase(), 'hello']} />`,
34+
errors: [{ messageId: "noArrayHandlers" }],
35+
...tsOnlyTest,
36+
},
37+
{
38+
code: `let el = <div onMouseOver={[1,2,3]} />`,
39+
errors: [{ messageId: "noArrayHandlers" }],
40+
},
41+
{
42+
code: `let el = <div on:click={[handler, i()]} />`,
43+
errors: [{ messageId: "noArrayHandlers" }],
44+
},
45+
{
46+
code: `let el = <button type="button" onclick={[handler, i() + 2]} class="btn" />`,
47+
errors: [{ messageId: "noArrayHandlers" }],
48+
},
49+
{
50+
code: `let handler = [(x) => x*2, 54];
51+
let el = <button style={{background: 'pink'}} onclick={handler} />`,
52+
errors: [{ messageId: "noArrayHandlers" }],
53+
},
54+
{
55+
code: `const thing = (props) => <div onclick={[props.callback, props.id]}><button type="button" onclick={handler} class="btn" /></div>`,
56+
errors: [{ messageId: "noArrayHandlers" }],
57+
},
58+
{
59+
code: `function Component() {
60+
const arr = [(n: number) => n*n, 2];
61+
return <div onClick={arr} />;
62+
}`,
63+
errors: [{ messageId: "noArrayHandlers" }],
64+
...tsOnlyTest,
65+
},
66+
],
67+
});

0 commit comments

Comments
 (0)