Skip to content

Commit a1a7dd7

Browse files
committed
Tighten up checking for tracked scopes in custom hook arguments.
1 parent 7f340e1 commit a1a7dd7

File tree

5 files changed

+105
-20
lines changed

5 files changed

+105
-20
lines changed

docs/reactivity.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,18 @@ const result = indexArray(array, (item) => {
421421
const [signal] = createSignal();
422422
let el = <Component staticProp={signal()} />;
423423

424+
const [signal] = createSignal(0);
425+
useExample(signal());
426+
427+
const [signal] = createSignal(0);
428+
useExample([signal()]);
429+
430+
const [signal] = createSignal(0);
431+
useExample({ value: signal() });
432+
433+
const [signal] = createSignal(0);
434+
useExample((() => signal())());
435+
424436
```
425437

426438
### Valid Examples
@@ -613,6 +625,22 @@ function createFoo(v) {}
613625
const [bar, setBar] = createSignal();
614626
createFoo({ onBar: () => bar() });
615627

628+
function createFoo(v) {}
629+
const [bar, setBar] = createSignal();
630+
createFoo({
631+
onBar() {
632+
bar();
633+
},
634+
});
635+
636+
function createFoo(v) {}
637+
const [bar, setBar] = createSignal();
638+
createFoo(bar);
639+
640+
function createFoo(v) {}
641+
const [bar, setBar] = createSignal();
642+
createFoo([bar]);
643+
616644
const [bar, setBar] = createSignal();
617645
X.createFoo(() => bar());
618646

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: 24 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,26 @@ 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 === "CallExpression" && traced.parent.callee === traced))
840+
) {
841+
pushTrackedScope(childNode, "called-function");
842+
this.skip(); // poor-man's `findInScope`: don't enter child scopes
843+
}
844+
},
845+
});
846+
};
847+
826848
if (node.type === "JSXExpressionContainer") {
827849
if (
828850
node.parent?.type === "JSXAttribute" &&
@@ -1037,15 +1059,7 @@ export default createRule<Options, MessageIds>({
10371059
// Assume all identifier/function arguments are tracked scopes, and use "called-function"
10381060
// to allow async handlers (permissive). Assume non-resolvable args are reactive expressions.
10391061
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-
}
1062+
permissivelyTrackNode(arg);
10491063
}
10501064
}
10511065
} else if (node.callee.type === "MemberExpression") {
@@ -1064,15 +1078,7 @@ export default createRule<Options, MessageIds>({
10641078
) {
10651079
// Handle custom hook parameters for property access custom hooks
10661080
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-
}
1081+
permissivelyTrackNode(arg);
10761082
}
10771083
}
10781084
}

test/rules/reactivity.test.ts

Lines changed: 37 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();
@@ -808,5 +820,30 @@ export const cases = run("reactivity", rule, {
808820
let el = <Component staticProp={signal()} />;`,
809821
errors: [{ messageId: "untrackedReactive" }],
810822
},
823+
// custom hooks
824+
{
825+
code: `
826+
const [signal] = createSignal(0);
827+
useExample(signal())`,
828+
errors: [{ messageId: "untrackedReactive" }],
829+
},
830+
{
831+
code: `
832+
const [signal] = createSignal(0);
833+
useExample([signal()])`,
834+
errors: [{ messageId: "untrackedReactive" }],
835+
},
836+
{
837+
code: `
838+
const [signal] = createSignal(0);
839+
useExample({ value: signal() })`,
840+
errors: [{ messageId: "untrackedReactive" }],
841+
},
842+
{
843+
code: `
844+
const [signal] = createSignal(0);
845+
useExample((() => signal())())`,
846+
errors: [{ messageId: "expectedFunctionGotExpression" }],
847+
},
811848
],
812849
});

0 commit comments

Comments
 (0)