Skip to content

Commit 02d7879

Browse files
authored
Merge pull request #125 from solidjs-community/fix/custom-hook-tracking
Tighten up checking for tracked scopes in custom hook arguments.
2 parents 7f340e1 + ce5606e commit 02d7879

File tree

5 files changed

+117
-20
lines changed

5 files changed

+117
-20
lines changed

docs/reactivity.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ const Component = (props) => {
218218
return <div>{value()}</div>;
219219
};
220220

221+
const Component = (props) => {
222+
const [value] = createSignal(props.value);
223+
};
224+
221225
const Component = (props) => {
222226
const derived = () => props.value;
223227
const oops = derived();
@@ -421,6 +425,18 @@ const result = indexArray(array, (item) => {
421425
const [signal] = createSignal();
422426
let el = <Component staticProp={signal()} />;
423427

428+
const [signal] = createSignal(0);
429+
useExample(signal());
430+
431+
const [signal] = createSignal(0);
432+
useExample([signal()]);
433+
434+
const [signal] = createSignal(0);
435+
useExample({ value: signal() });
436+
437+
const [signal] = createSignal(0);
438+
useExample((() => signal())());
439+
424440
```
425441

426442
### Valid Examples
@@ -613,6 +629,22 @@ function createFoo(v) {}
613629
const [bar, setBar] = createSignal();
614630
createFoo({ onBar: () => bar() });
615631

632+
function createFoo(v) {}
633+
const [bar, setBar] = createSignal();
634+
createFoo({
635+
onBar() {
636+
bar();
637+
},
638+
});
639+
640+
function createFoo(v) {}
641+
const [bar, setBar] = createSignal();
642+
createFoo(bar);
643+
644+
function createFoo(v) {}
645+
const [bar, setBar] = createSignal();
646+
createFoo([bar]);
647+
616648
const [bar, setBar] = createSignal();
617649
X.createFoo(() => bar());
618650

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
],
3939
"dependencies": {
4040
"@typescript-eslint/utils": "^6.4.0",
41+
"estraverse": "^5.3.0",
4142
"is-html": "^2.0.0",
4243
"kebab-case": "^1.0.2",
4344
"known-css-properties": "^0.24.0",
@@ -52,6 +53,7 @@
5253
"@rollup/plugin-node-resolve": "^14.1.0",
5354
"@tsconfig/node16": "^16.1.0",
5455
"@types/eslint": "^8.40.2",
56+
"@types/estraverse": "^5.1.7",
5557
"@types/fs-extra": "^9.0.13",
5658
"@types/is-html": "^2.0.0",
5759
"@types/jest": "^27.5.2",

pnpm-lock.yaml

Lines changed: 14 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/rules/reactivity.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import { TSESTree as T, TSESLint, ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
7+
import { traverse } from "estraverse";
78
import {
89
findParent,
910
findInScope,
@@ -17,6 +18,7 @@ import {
1718
ignoreTransparentWrappers,
1819
getFunctionName,
1920
isJSXElementOrFragment,
21+
trace,
2022
} from "../utils";
2123

2224
const { findVariable, getFunctionHeadLocation } = ASTUtils;
@@ -823,6 +825,27 @@ export default createRule<Options, MessageIds>({
823825
});
824826
}
825827
};
828+
// given some expression, mark any functions within it as tracking scopes, and do not traverse
829+
// those functions
830+
const permissivelyTrackNode = (node: T.Node) => {
831+
traverse(node as any, {
832+
enter(cn) {
833+
const childNode = cn as T.Node;
834+
const traced = trace(childNode, context.getScope());
835+
// when referencing a function or something that could be a derived signal, track it
836+
if (
837+
isFunctionNode(traced) ||
838+
(traced.type === "Identifier" &&
839+
traced.parent.type !== "MemberExpression" &&
840+
!(traced.parent.type === "CallExpression" && traced.parent.callee === traced))
841+
) {
842+
pushTrackedScope(childNode, "called-function");
843+
this.skip(); // poor-man's `findInScope`: don't enter child scopes
844+
}
845+
},
846+
});
847+
};
848+
826849
if (node.type === "JSXExpressionContainer") {
827850
if (
828851
node.parent?.type === "JSXAttribute" &&
@@ -1037,15 +1060,7 @@ export default createRule<Options, MessageIds>({
10371060
// Assume all identifier/function arguments are tracked scopes, and use "called-function"
10381061
// to allow async handlers (permissive). Assume non-resolvable args are reactive expressions.
10391062
for (const arg of node.arguments) {
1040-
if (isFunctionNode(arg)) {
1041-
pushTrackedScope(arg, "called-function");
1042-
} else if (
1043-
arg.type === "Identifier" ||
1044-
arg.type === "ObjectExpression" ||
1045-
arg.type === "ArrayExpression"
1046-
) {
1047-
pushTrackedScope(arg, "expression");
1048-
}
1063+
permissivelyTrackNode(arg);
10491064
}
10501065
}
10511066
} else if (node.callee.type === "MemberExpression") {
@@ -1064,15 +1079,7 @@ export default createRule<Options, MessageIds>({
10641079
) {
10651080
// Handle custom hook parameters for property access custom hooks
10661081
for (const arg of node.arguments) {
1067-
if (isFunctionNode(arg)) {
1068-
pushTrackedScope(arg, "called-function");
1069-
} else if (
1070-
arg.type === "Identifier" ||
1071-
arg.type === "ObjectExpression" ||
1072-
arg.type === "ArrayExpression"
1073-
) {
1074-
pushTrackedScope(arg, "expression");
1075-
}
1082+
permissivelyTrackNode(arg);
10761083
}
10771084
}
10781085
}

test/rules/reactivity.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,18 @@ export const cases = run("reactivity", rule, {
149149
`function createFoo(v) {}
150150
const [bar, setBar] = createSignal();
151151
createFoo({ onBar: () => bar() });`,
152+
`function createFoo(v) {}
153+
const [bar, setBar] = createSignal();
154+
createFoo({ onBar() { bar() } });`,
155+
`function createFoo(v) {}
156+
const [bar, setBar] = createSignal();
157+
createFoo(bar);`,
158+
`function createFoo(v) {}
159+
const [bar, setBar] = createSignal();
160+
createFoo([bar]);`,
161+
// `function createFoo(v) {}
162+
// const [bar, setBar] = createSignal();
163+
// createFoo((() => () => bar())());`,
152164
`const [bar, setBar] = createSignal();
153165
X.createFoo(() => bar());`,
154166
`const [bar, setBar] = createSignal();
@@ -390,6 +402,13 @@ export const cases = run("reactivity", rule, {
390402
},
391403
],
392404
},
405+
{
406+
code: `
407+
const Component = props => {
408+
const [value] = createSignal(props.value);
409+
}`,
410+
errors: [{ messageId: "untrackedReactive", type: T.MemberExpression }],
411+
},
393412
// mark `props` as props by name before we've determined if Component is a component in :exit
394413
{
395414
code: `
@@ -808,5 +827,30 @@ export const cases = run("reactivity", rule, {
808827
let el = <Component staticProp={signal()} />;`,
809828
errors: [{ messageId: "untrackedReactive" }],
810829
},
830+
// custom hooks
831+
{
832+
code: `
833+
const [signal] = createSignal(0);
834+
useExample(signal())`,
835+
errors: [{ messageId: "untrackedReactive" }],
836+
},
837+
{
838+
code: `
839+
const [signal] = createSignal(0);
840+
useExample([signal()])`,
841+
errors: [{ messageId: "untrackedReactive" }],
842+
},
843+
{
844+
code: `
845+
const [signal] = createSignal(0);
846+
useExample({ value: signal() })`,
847+
errors: [{ messageId: "untrackedReactive" }],
848+
},
849+
{
850+
code: `
851+
const [signal] = createSignal(0);
852+
useExample((() => signal())())`,
853+
errors: [{ messageId: "expectedFunctionGotExpression" }],
854+
},
811855
],
812856
});

0 commit comments

Comments
 (0)