Skip to content

Commit 1310464

Browse files
committed
Add no-react-deps rule
1 parent ffd7c44 commit 1310464

File tree

5 files changed

+190
-0
lines changed

5 files changed

+190
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ const [editedValue, setEditedValue] = createSignal(props.value);
131131
|| 🔧 | [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. |
132132
|| 🔧 | [solid/no-innerhtml](docs/no-innerhtml.md) | Disallow usage of the innerHTML attribute, which can often lead to security vulnerabilities. |
133133
| | | [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. |
134+
|| 🔧 | [solid/no-react-deps](docs/no-react-deps.md) | Disallow usage of dependency arrays in createEffect and createMemo. |
134135
|| 🔧 | [solid/no-react-specific-props](docs/no-react-specific-props.md) | Disallow usage of React-specific `className`/`htmlFor` props, which were deprecated in v1.4.0. |
135136
|| | [solid/no-unknown-namespaces](docs/no-unknown-namespaces.md) | Enforce using only Solid-specific namespaced attribute names (i.e. `'on:'` in `<div on:click={...} />`). |
136137
| | 🔧 | [solid/prefer-classlist](docs/prefer-classlist.md) | Enforce using the classlist prop over importing a classnames helper. The classlist prop accepts an object `{ [class: string]: boolean }` just like classnames. |

docs/no-react-deps.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<!-- AUTO-GENERATED-CONTENT:START (HEADER) -->
2+
# solid/no-react-deps
3+
Disallow usage of dependency arrays in createEffect and createMemo.
4+
This rule is **a warning** by default.
5+
6+
[View source](../src/rules/no-react-deps.ts) · [View tests](../test/rules/no-react-deps.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, and all of them can be auto-fixed.
20+
21+
```js
22+
createEffect(() => {
23+
console.log(signal());
24+
}, [signal()]);
25+
// after eslint --fix:
26+
createEffect(() => {
27+
console.log(signal());
28+
});
29+
30+
createEffect(() => {
31+
console.log(signal());
32+
}, [signal]);
33+
// after eslint --fix:
34+
createEffect(() => {
35+
console.log(signal());
36+
});
37+
38+
const value = createMemo(() => computeExpensiveValue(a(), b()), [a(), b()]);
39+
// after eslint --fix:
40+
const value = createMemo(() => computeExpensiveValue(a(), b()));
41+
42+
const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b]);
43+
// after eslint --fix:
44+
const value = createMemo(() => computeExpensiveValue(a(), b()));
45+
46+
const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b()]);
47+
// after eslint --fix:
48+
const value = createMemo(() => computeExpensiveValue(a(), b()));
49+
50+
```
51+
52+
### Valid Examples
53+
54+
These snippets don't cause lint errors.
55+
56+
```js
57+
createEffect(() => {
58+
console.log(signal());
59+
});
60+
61+
createEffect((prev) => {
62+
console.log(signal() + prev);
63+
}, 0);
64+
65+
const value = createMemo(() => computeExpensiveValue(a(), b()));
66+
67+
const sum = createMemo((prev) => input() + prev, 0);
68+
69+
const args = [
70+
() => {
71+
console.log(signal());
72+
},
73+
[signal()],
74+
];
75+
createEffect(...args);
76+
77+
```
78+
<!-- AUTO-GENERATED-CONTENT:END -->

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import jsxUsesVars from "./rules/jsx-uses-vars";
88
import noDestructure from "./rules/no-destructure";
99
import noInnerHTML from "./rules/no-innerhtml";
1010
import noProxyApis from "./rules/no-proxy-apis";
11+
import noReactDeps from "./rules/no-react-deps";
1112
import noReactSpecificProps from "./rules/no-react-specific-props";
1213
import noUnknownNamespaces from "./rules/no-unknown-namespaces";
1314
import preferClasslist from "./rules/prefer-classlist";
@@ -29,6 +30,7 @@ const allRules = {
2930
"no-destructure": noDestructure,
3031
"no-innerhtml": noInnerHTML,
3132
"no-proxy-apis": noProxyApis,
33+
"no-react-deps": noReactDeps,
3234
"no-react-specific-props": noReactSpecificProps,
3335
"no-unknown-namespaces": noUnknownNamespaces,
3436
"prefer-classlist": preferClasslist,
@@ -76,6 +78,7 @@ const plugin = {
7678
// these rules are mostly style suggestions
7779
"solid/imports": 1,
7880
"solid/style-prop": 1,
81+
"solid/no-react-deps": 1,
7982
"solid/no-react-specific-props": 1,
8083
"solid/self-closing-comp": 1,
8184
// handled by Solid compiler, opt-in style suggestion
@@ -109,6 +112,7 @@ const plugin = {
109112
// these rules are mostly style suggestions
110113
"solid/imports": 1,
111114
"solid/style-prop": 1,
115+
"solid/no-react-deps": 1,
112116
"solid/no-react-specific-props": 1,
113117
"solid/self-closing-comp": 1,
114118
// namespaces taken care of by TS

src/rules/no-react-deps.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { TSESLint } from "@typescript-eslint/utils";
2+
import { isFunctionNode, trace, trackImports } from "../utils";
3+
4+
const rule: TSESLint.RuleModule<"noUselessDep", []> = {
5+
meta: {
6+
type: "problem",
7+
docs: {
8+
recommended: "warn",
9+
description: "Disallow usage of dependency arrays in createEffect and createMemo.",
10+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/docs/no-react-deps.md",
11+
},
12+
fixable: "code",
13+
schema: [],
14+
messages: {
15+
noUselessDep:
16+
"In Solid, {{ name }} doesn't need a dependency array because it automatically tracks its dependencies. If you really need to override the list of dependencies, use `on`.",
17+
},
18+
},
19+
create(context) {
20+
/** Tracks imports from 'solid-js', handling aliases. */
21+
const { matchImport, handleImportDeclaration } = trackImports();
22+
23+
return {
24+
ImportDeclaration: handleImportDeclaration,
25+
CallExpression(node) {
26+
if (
27+
node.callee.type === "Identifier" &&
28+
matchImport(["createEffect", "createMemo"], node.callee.name) &&
29+
node.arguments.length === 2 &&
30+
node.arguments.every((arg) => arg.type !== "SpreadElement")
31+
) {
32+
// grab both arguments, tracing any variables to their actual values if possible
33+
const [arg0, arg1] = node.arguments.map((arg) => trace(arg, context.getScope()));
34+
35+
if (isFunctionNode(arg0) && arg0.params.length === 0 && arg1.type === "ArrayExpression") {
36+
// A second argument that looks like a dependency array was passed to
37+
// createEffect/createMemo, and the inline function doesn't accept a parameter, so it
38+
// can't just be an initial value.
39+
context.report({
40+
node: node.arguments[1], // if this is a variable, highlight the usage, not the initialization
41+
messageId: "noUselessDep",
42+
data: {
43+
name: node.callee.name,
44+
},
45+
// remove dep array if it's given inline, otherwise don't fix
46+
fix: arg1 === node.arguments[1] ? (fixer) => fixer.remove(arg1) : undefined,
47+
});
48+
}
49+
}
50+
},
51+
};
52+
},
53+
};
54+
55+
export default rule;

test/rules/no-react-deps.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { run } from "../ruleTester";
2+
import rule from "../../src/rules/no-react-deps";
3+
4+
export const cases = run("no-react-deps", rule, {
5+
valid: [
6+
`createEffect(() => {
7+
console.log(signal());
8+
});`,
9+
`createEffect((prev) => {
10+
console.log(signal() + prev);
11+
}, 0);`,
12+
`const value = createMemo(() => computeExpensiveValue(a(), b()));`,
13+
`const sum = createMemo((prev) => input() + prev, 0);`,
14+
`const args = [() => { console.log(signal()); }, [signal()]];
15+
createEffect(...args);`,
16+
],
17+
invalid: [
18+
{
19+
code: `createEffect(() => {
20+
console.log(signal());
21+
}, [signal()]);`,
22+
errors: [{ messageId: "noUselessDep", data: { name: "createEffect" } }],
23+
output: `createEffect(() => {
24+
console.log(signal());
25+
}, );`,
26+
},
27+
{
28+
code: `createEffect(() => {
29+
console.log(signal());
30+
}, [signal]);`,
31+
errors: [{ messageId: "noUselessDep", data: { name: "createEffect" } }],
32+
output: `createEffect(() => {
33+
console.log(signal());
34+
}, );`,
35+
},
36+
{
37+
code: `const value = createMemo(() => computeExpensiveValue(a(), b()), [a(), b()]);`,
38+
errors: [{ messageId: "noUselessDep", data: { name: "createMemo" } }],
39+
output: `const value = createMemo(() => computeExpensiveValue(a(), b()), );`,
40+
},
41+
{
42+
code: `const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b]);`,
43+
errors: [{ messageId: "noUselessDep", data: { name: "createMemo" } }],
44+
output: `const value = createMemo(() => computeExpensiveValue(a(), b()), );`,
45+
},
46+
{
47+
code: `const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b()]);`,
48+
errors: [{ messageId: "noUselessDep", data: { name: "createMemo" } }],
49+
output: `const value = createMemo(() => computeExpensiveValue(a(), b()), );`,
50+
},
51+
],
52+
});

0 commit comments

Comments
 (0)