Skip to content

Commit 31e9cae

Browse files
authored
Merge pull request #45 from solidjs-community/feature/no-react-deps
Add no-react-deps rule
2 parents ffd7c44 + 0d0f44e commit 31e9cae

File tree

5 files changed

+234
-0
lines changed

5 files changed

+234
-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: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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 some 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 deps = [signal];
39+
createEffect(() => {
40+
console.log(signal());
41+
}, deps);
42+
43+
const value = createMemo(() => computeExpensiveValue(a(), b()), [a(), b()]);
44+
// after eslint --fix:
45+
const value = createMemo(() => computeExpensiveValue(a(), b()));
46+
47+
const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b]);
48+
// after eslint --fix:
49+
const value = createMemo(() => computeExpensiveValue(a(), b()));
50+
51+
const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b()]);
52+
// after eslint --fix:
53+
const value = createMemo(() => computeExpensiveValue(a(), b()));
54+
55+
const deps = [a, b];
56+
const value = createMemo(() => computeExpensiveValue(a(), b()), deps);
57+
58+
const deps = [a, b];
59+
const memoFn = () => computeExpensiveValue(a(), b());
60+
const value = createMemo(memoFn, deps);
61+
62+
```
63+
64+
### Valid Examples
65+
66+
These snippets don't cause lint errors.
67+
68+
```js
69+
createEffect(() => {
70+
console.log(signal());
71+
});
72+
73+
createEffect((prev) => {
74+
console.log(signal());
75+
return prev + 1;
76+
}, 0);
77+
78+
createEffect((prev) => {
79+
console.log(signal());
80+
return (prev || 0) + 1;
81+
});
82+
83+
const value = createMemo(() => computeExpensiveValue(a(), b()));
84+
85+
const sum = createMemo((prev) => input() + prev, 0);
86+
87+
const args = [
88+
() => {
89+
console.log(signal());
90+
},
91+
[signal()],
92+
];
93+
createEffect(...args);
94+
95+
```
96+
<!-- 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 accept 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: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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());
11+
return prev + 1;
12+
}, 0);`,
13+
`createEffect((prev) => {
14+
console.log(signal());
15+
return (prev || 0) + 1;
16+
});`,
17+
`const value = createMemo(() => computeExpensiveValue(a(), b()));`,
18+
`const sum = createMemo((prev) => input() + prev, 0);`,
19+
`const args = [() => { console.log(signal()); }, [signal()]];
20+
createEffect(...args);`,
21+
],
22+
invalid: [
23+
{
24+
code: `createEffect(() => {
25+
console.log(signal());
26+
}, [signal()]);`,
27+
errors: [{ messageId: "noUselessDep", data: { name: "createEffect" } }],
28+
output: `createEffect(() => {
29+
console.log(signal());
30+
}, );`,
31+
},
32+
{
33+
code: `createEffect(() => {
34+
console.log(signal());
35+
}, [signal]);`,
36+
errors: [{ messageId: "noUselessDep", data: { name: "createEffect" } }],
37+
output: `createEffect(() => {
38+
console.log(signal());
39+
}, );`,
40+
},
41+
{
42+
code: `const deps = [signal];
43+
createEffect(() => {
44+
console.log(signal());
45+
}, deps);`,
46+
errors: [{ messageId: "noUselessDep", data: { name: "createEffect" } }],
47+
// no `output`
48+
},
49+
{
50+
code: `const value = createMemo(() => computeExpensiveValue(a(), b()), [a(), b()]);`,
51+
errors: [{ messageId: "noUselessDep", data: { name: "createMemo" } }],
52+
output: `const value = createMemo(() => computeExpensiveValue(a(), b()), );`,
53+
},
54+
{
55+
code: `const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b]);`,
56+
errors: [{ messageId: "noUselessDep", data: { name: "createMemo" } }],
57+
output: `const value = createMemo(() => computeExpensiveValue(a(), b()), );`,
58+
},
59+
{
60+
code: `const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b()]);`,
61+
errors: [{ messageId: "noUselessDep", data: { name: "createMemo" } }],
62+
output: `const value = createMemo(() => computeExpensiveValue(a(), b()), );`,
63+
},
64+
{
65+
code: `const deps = [a, b];
66+
const value = createMemo(() => computeExpensiveValue(a(), b()), deps);`,
67+
errors: [{ messageId: "noUselessDep", data: { name: "createMemo" } }],
68+
// no `output`
69+
},
70+
{
71+
code: `const deps = [a, b];
72+
const memoFn = () => computeExpensiveValue(a(), b());
73+
const value = createMemo(memoFn, deps);`,
74+
errors: [{ messageId: "noUselessDep", data: { name: "createMemo" } }],
75+
// no `output`
76+
},
77+
],
78+
});

0 commit comments

Comments
 (0)