@samitouri / QOS-React-1 / commits / 0c0bedd377

New approach to hook validation

New approach to hooks validation per recent discussion. The idea is to avoid false positives while still preventing serious violations. See the comments in the file for more details about the approach. It uses a somewhat similar idea to InferReferenceEffects in that we track a "Kind" for each IdentifierId, and various instructions propagate or derive a result Kind from the operands. Kinds form a lattice and can be joined, allowing us to be more precise about known vs potential hooks, and known vs potential _sources_ of hooks.

Joe Savona committed Dec 4, 2023 at 08:15 UTC 0c0bedd3775251a1ada059df05e0494797d9e9b9
18 files changed +662 -158
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts
-7
@@ -74,7 +74,6 @@ import {
74 validateMemoizedEffectDependencies,
75 validateNoRefAccessInRender,
76 validateNoSetStateInRender,
77 - validateUnconditionalHooks,
77 validateUseMemo,
78 } from "../Validation";
79
@@ -151,12 +150,6 @@ function* runWithEnvironment(
150
151 if (env.config.validateHooksUsage) {
152 validateHooksUsage(hir);
154 - const conditionalHooksResult = validateUnconditionalHooks(hir).unwrap();
155 - yield log({
156 - kind: "debug",
157 - name: "ValidateUnconditionalHooks",
158 - value: conditionalHooksResult.debug(),
159 - });
153 }
154
155 analyseFunctions(hir);
compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts
+1 -1
@@ -467,7 +467,7 @@ export class Environment {
467 }
468
469 // From https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#LL18C1-L23C2
470 -function isHookName(name: string): boolean {
470 +export function isHookName(name: string): boolean {
471 /*
472 * if (__EXPERIMENTAL__) {
473 * return name === 'use' || /^use[A-Z0-9]/.test(name);
compiler/packages/babel-plugin-react-forget/src/Validation/ValidateHooksUsage.ts
+327 -23
@@ -10,19 +10,121 @@ import {
10 CompilerErrorDetail,
11 ErrorSeverity,
12 } from "../CompilerError";
13 -import { HIRFunction, Place, getHookKind } from "../HIR/HIR";
13 +import { computePostDominatorTree } from "../HIR";
14 +import { isHookName } from "../HIR/Environment";
15 import {
15 - eachInstructionValueOperand,
16 + BlockId,
17 + HIRFunction,
18 + IdentifierId,
19 + Place,
20 + getHookKind,
21 +} from "../HIR/HIR";
22 +import {
23 + eachInstructionLValue,
24 + eachInstructionOperand,
25 eachTerminalOperand,
26 } from "../HIR/visitors";
27 +import { assertExhaustive } from "../Utils/utils";
28 +
29 +/**
30 + * Represents the possible kinds of value which may be stored at a given Place during
31 + * abstract interpretation. The kinds form a lattice, with earlier items taking
32 + * precedence over later items (see joinKinds()).
33 + */
34 +enum Kind {
35 + // A potential/known hook which was already used in an invalid way
36 + Error = "Error",
37 +
38 + /*
39 + * A known hook. Sources include:
40 + * - LoadGlobal instructions whose type was inferred as a hook
41 + * - PropertyLoad, ComputedLoad, and Destructuring instructions
42 + * where the object is a KnownHook
43 + * - PropertyLoad, ComputedLoad, and Destructuring instructions
44 + * where the object is a Global and the property name is hook-like
45 + */
46 + KnownHook = "KnownHook",
47 +
48 + /*
49 + * A potential hook. Sources include:
50 + * - LValues (other than LoadGlobal) where the name is hook-like
51 + * - PropertyLoad, ComputedLoad, and Destructuring instructions
52 + * where the object is a potential hook or the property name
53 + * is hook-like
54 + */
55 + PotentialHook = "PotentialHook",
56 +
57 + // LoadGlobal values whose type was not inferred as a hook
58 + Global = "Global",
59 +
60 + // All other values, ie local variables
61 + Local = "Local",
62 +}
63 +
64 +function joinKinds(a: Kind, b: Kind): Kind {
65 + if (a === Kind.Error || b === Kind.Error) {
66 + return Kind.Error;
67 + } else if (a === Kind.KnownHook || b === Kind.KnownHook) {
68 + return Kind.KnownHook;
69 + } else if (a === Kind.PotentialHook || b === Kind.PotentialHook) {
70 + return Kind.PotentialHook;
71 + } else if (a === Kind.Global || b === Kind.Global) {
72 + return Kind.Global;
73 + } else {
74 + return Kind.Local;
75 + }
76 +}
77
78 /*
79 * Validates that the function honors the [Rules of Hooks](https://react.dev/warnings/invalid-hook-call-warning)
80 * rule that hooks may only be called and not otherwise referenced as first-class values.
81 + *
82 + * Specifically this pass implements the following rules:
83 + * - Known hooks may only be called unconditionally, and cannot be used as first-class values.
84 + * See the note for Kind.KnownHook for sources of known hooks
85 + * - Potential hooks may be referenced as first-class values, with the exception that they
86 + * may not appear as the callee of a conditional call.
87 + * See the note for Kind.PotentialHook for sources of potential hooks
88 */
89 export function validateHooksUsage(fn: HIRFunction): void {
90 + // Construct the set of blocks that is always reachable from the entry block.
91 + const unconditionalBlocks = new Set<BlockId>();
92 + const dominators = computePostDominatorTree(fn, {
93 + /*
94 + * Hooks must only be in a consistent order for executions that return normally,
95 + * so we opt-in to viewing throw as a non-exit node.
96 + */
97 + includeThrowsAsExitNode: false,
98 + });
99 + const exit = dominators.exit;
100 + let current: BlockId | null = fn.body.entry;
101 + while (current !== null && current !== exit) {
102 + CompilerError.invariant(!unconditionalBlocks.has(current), {
103 + reason:
104 + "Internal error: non-terminating loop in ValidateUnconditionalHooks",
105 + loc: null,
106 + suggestions: null,
107 + });
108 + unconditionalBlocks.add(current);
109 + current = dominators.get(current);
110 + }
111 +
112 const errors = new CompilerError();
25 - const pushError = (place: Place): void => {
113 + function recordConditionalHookError(place: Place): void {
114 + setKind(place, Kind.Error);
115 + errors.pushErrorDetail(
116 + new CompilerErrorDetail({
117 + description: null,
118 + reason:
119 + "Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)",
120 + loc: place.loc,
121 + severity: ErrorSeverity.InvalidReact,
122 + suggestions: null,
123 + })
124 + );
125 + }
126 + function recordInvalidHookUsageError(place: Place): void {
127 + setKind(place, Kind.Error);
128 errors.pushErrorDetail(
129 new CompilerErrorDetail({
130 description: null,
@@ -33,40 +135,242 @@ export function validateHooksUsage(fn: HIRFunction): void {
135 suggestions: null,
136 })
137 );
36 - };
138 + }
139 +
140 + const valueKinds = new Map<IdentifierId, Kind>();
141 + function getKindForPlace(place: Place): Kind {
142 + const knownKind = valueKinds.get(place.identifier.id);
143 + if (place.identifier.name !== null && isHookName(place.identifier.name)) {
144 + return joinKinds(knownKind ?? Kind.Local, Kind.PotentialHook);
145 + } else {
146 + return knownKind ?? Kind.Local;
147 + }
148 + }
149 +
150 + function visitPlace(place: Place): void {
151 + const kind = valueKinds.get(place.identifier.id);
152 + if (kind === Kind.KnownHook) {
153 + recordInvalidHookUsageError(place);
154 + }
155 + }
156 +
157 + function setKind(place: Place, kind: Kind): void {
158 + valueKinds.set(place.identifier.id, kind);
159 + }
160 +
161 + for (const param of fn.params) {
162 + const place = param.kind === "Identifier" ? param : param.place;
163 + const kind = getKindForPlace(place);
164 + setKind(place, kind);
165 + }
166
167 for (const [, block] of fn.body.blocks) {
168 + for (const phi of block.phis) {
169 + let kind: Kind =
170 + phi.id.name !== null && isHookName(phi.id.name)
171 + ? Kind.PotentialHook
172 + : Kind.Local;
173 + for (const [, operand] of phi.operands) {
174 + const operandKind = valueKinds.get(operand.id);
175 + /*
176 + * NOTE: we currently skip operands whose value is unknown
177 + * (which can only occur for functions with loops), we may
178 + * cause us to miss invalid code in some cases. We should
179 + * expand this to a fixpoint iteration in a follow-up.
180 + */
181 + if (operandKind !== undefined) {
182 + kind = joinKinds(kind, operandKind);
183 + }
184 + }
185 + valueKinds.set(phi.id.id, kind);
186 + }
187 for (const instr of block.instructions) {
40 - if (instr.value.kind === "CallExpression") {
41 - for (const operand of eachInstructionValueOperand(instr.value)) {
42 - if (operand === instr.value.callee) {
43 - continue;
188 + switch (instr.value.kind) {
189 + case "LoadGlobal": {
190 + /*
191 + * Globals are the one source of known hooks: they are either
192 + * directly a hook, or infer a Global kind from which knownhooks
193 + * can be derived later via property access (PropertyLoad etc)
194 + */
195 + if (getHookKind(fn.env, instr.lvalue.identifier) != null) {
196 + setKind(instr.lvalue, Kind.KnownHook);
197 + } else {
198 + setKind(instr.lvalue, Kind.Global);
199 + }
200 + break;
201 + }
202 + case "LoadContext":
203 + case "LoadLocal": {
204 + visitPlace(instr.value.place);
205 + const kind = getKindForPlace(instr.value.place);
206 + setKind(instr.lvalue, kind);
207 + break;
208 + }
209 + case "StoreLocal":
210 + case "StoreContext": {
211 + visitPlace(instr.value.value);
212 + const kind = getKindForPlace(instr.value.value);
213 + setKind(instr.value.lvalue.place, kind);
214 + setKind(instr.lvalue, kind);
215 + break;
216 + }
217 + case "ComputedLoad": {
218 + visitPlace(instr.value.object);
219 + const kind = getKindForPlace(instr.value.object);
220 + setKind(instr.lvalue, joinKinds(getKindForPlace(instr.lvalue), kind));
221 + break;
222 + }
223 + case "PropertyLoad": {
224 + visitPlace(instr.value.object);
225 + const objectKind = getKindForPlace(instr.value.object);
226 + const isHookProperty = isHookName(instr.value.property);
227 + let kind: Kind;
228 + switch (objectKind) {
229 + case Kind.Error: {
230 + kind = Kind.Error;
231 + break;
232 + }
233 + case Kind.KnownHook: {
234 + /**
235 + * const useFoo;
236 + * function Component() {
237 + * let x = useFoo.useBar; // useFoo is KnownHook, any property from it inherits KnownHook
238 + * }
239 + */
240 + kind = Kind.KnownHook;
241 + break;
242 + }
243 + case Kind.PotentialHook: {
244 + /**
245 + * function Component(props) {
246 + * let useFoo;
247 + * let x = useFoo.useBar; // useFoo is PotentialHook, any property from it inherits PotentialHook
248 + * }
249 + */
250 + kind = Kind.PotentialHook;
251 + break;
252 + }
253 + case Kind.Global: {
254 + /**
255 + * function Component() {
256 + * let x = React.useState; // hook-named property of global is knownhook
257 + * let y = React.foo; // else inherit Global
258 + * }
259 + */
260 + kind = isHookProperty ? Kind.KnownHook : Kind.Global;
261 + break;
262 + }
263 + case Kind.Local: {
264 + /**
265 + * function Component() {
266 + * let o = createObject();
267 + * let x = o.useState; // hook-named property of local is potentialhook
268 + * let y = o.foo; // else inherit local
269 + * }
270 + */
271 + kind = isHookProperty ? Kind.PotentialHook : Kind.Local;
272 + break;
273 + }
274 + default: {
275 + assertExhaustive(objectKind, `Unexpected kind '${objectKind}'`);
276 + }
277 }
45 - if (getHookKind(fn.env, operand.identifier) != null) {
46 - pushError(operand);
278 + setKind(instr.lvalue, kind);
279 + break;
280 + }
281 + case "CallExpression": {
282 + const calleeKind = getKindForPlace(instr.value.callee);
283 + const isHookCallee =
284 + calleeKind === Kind.KnownHook || calleeKind === Kind.PotentialHook;
285 + if (isHookCallee && !unconditionalBlocks.has(block.id)) {
286 + recordConditionalHookError(instr.value.callee);
287 + }
288 + /**
289 + * We intentionally skip the callee because known/potential hooks
290 + * are always allowed to be called.
291 + */
292 + for (const operand of eachInstructionOperand(instr)) {
293 + if (operand === instr.value.callee) {
294 + continue;
295 + }
296 + visitPlace(operand);
297 }
298 + break;
299 }
49 - } else if (instr.value.kind === "MethodCall") {
50 - for (const operand of eachInstructionValueOperand(instr.value)) {
51 - if (operand === instr.value.property) {
52 - continue;
300 + case "MethodCall": {
301 + const calleeKind = getKindForPlace(instr.value.property);
302 + const isHookCallee =
303 + calleeKind === Kind.KnownHook || calleeKind === Kind.PotentialHook;
304 + if (isHookCallee && !unconditionalBlocks.has(block.id)) {
305 + recordConditionalHookError(instr.value.property);
306 + }
307 + /*
308 + * We intentionally skip the callee because known/potential hooks
309 + * are always allowed to be called as methods (`React.useState()`).
310 + */
311 + for (const operand of eachInstructionOperand(instr)) {
312 + if (operand === instr.value.property) {
313 + continue;
314 + }
315 + visitPlace(operand);
316 }
54 - if (getHookKind(fn.env, operand.identifier) != null) {
55 - pushError(operand);
317 + break;
318 + }
319 + case "Destructure": {
320 + visitPlace(instr.value.value);
321 + const objectKind = getKindForPlace(instr.value.value);
322 + for (const lvalue of eachInstructionLValue(instr)) {
323 + const isHookProperty =
324 + lvalue.identifier.name !== null &&
325 + isHookName(lvalue.identifier.name);
326 + let kind: Kind;
327 + switch (objectKind) {
328 + case Kind.Error: {
329 + kind = Kind.Error;
330 + break;
331 + }
332 + case Kind.KnownHook: {
333 + kind = Kind.KnownHook;
334 + break;
335 + }
336 + case Kind.PotentialHook: {
337 + kind = Kind.PotentialHook;
338 + break;
339 + }
340 + case Kind.Global: {
341 + kind = isHookProperty ? Kind.KnownHook : Kind.Global;
342 + break;
343 + }
344 + case Kind.Local: {
345 + kind = isHookProperty ? Kind.PotentialHook : Kind.Local;
346 + break;
347 + }
348 + default: {
349 + assertExhaustive(objectKind, `Unexpected kind '${objectKind}'`);
350 + }
351 + }
352 + setKind(lvalue, kind);
353 }
354 + break;
355 }
58 - } else {
59 - for (const operand of eachInstructionValueOperand(instr.value)) {
60 - if (getHookKind(fn.env, operand.identifier) != null) {
61 - pushError(operand);
356 + default: {
357 + /*
358 + * Else check usages of operands, but do *not* flow properties
359 + * from operands into the lvalues. For example, `let x = identity(y)`
360 + * does not infer `x` as a potential hook even if `y` is a potential hook.
361 + */
362 + for (const operand of eachInstructionOperand(instr)) {
363 + visitPlace(operand);
364 + }
365 + for (const lvalue of eachInstructionLValue(instr)) {
366 + const kind = getKindForPlace(lvalue);
367 + setKind(lvalue, kind);
368 }
369 }
370 }
371 }
372 for (const operand of eachTerminalOperand(block.terminal)) {
67 - if (getHookKind(fn.env, operand.identifier) != null) {
68 - pushError(operand);
69 - }
373 + visitPlace(operand);
374 }
375 }
376
compiler/packages/babel-plugin-react-forget/src/Validation/ValidateUnconditionalHooks.ts deleted
-124
@@ -1,124 +0,0 @@
1 -/*
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - */
7 -
8 -import {
9 - CompilerError,
10 - CompilerErrorDetail,
11 - ErrorSeverity,
12 -} from "../CompilerError";
13 -import { PostDominator, computePostDominatorTree } from "../HIR/Dominator";
14 -import { BlockId, HIRFunction, SourceLocation, getHookKind } from "../HIR/HIR";
15 -import { Err, Ok, Result } from "../Utils/Result";
16 -
17 -/*
18 - * Validates that the function honors the [Rules of Hooks](https://react.dev/warnings/invalid-hook-call-warning)
19 - * rule that hooks may not be called conditionally. More precisely, a component or hook must always call the
20 - * same set of hooks in the same order.
21 - *
22 - * The algorithm is based on [Dominators](https://en.wikipedia.org/wiki/Dominator_(graph_theory)). Hooks may
23 - * only be called in basic blocks that are unconditionally reachable from the entry node. In graph theory,
24 - * this corresponds to basic blocks which post dominate the entry block — that are on every path from the
25 - * entry block to the exit:
26 - *
27 - * ```
28 - * bb0 (entry)
29 - * / \
30 - * bb1 bb2
31 - * \ /
32 - * bb3
33 - * |
34 - * (exit)
35 - * ```
36 - *
37 - * Here, neither bb1 or bb2 post dominate the entry, which corresponds to the fact that control can
38 - * flow from the entry node to either of these nodes. However, bb3 does post dominate the entry node:
39 - * control flow will _always_ reach bb3 from the entry node. In this graph is is therefore safe to call
40 - * hooks only in bb0 and bb3, the post dominators of bb0.
41 - *
42 - * However if for example bb2 were to early return:
43 - *
44 - * ```
45 - * bb0 (entry)
46 - * / \
47 - * bb1 bb2
48 - * \ |
49 - * bb3 /
50 - * | /
51 - * (exit)
52 - * ```
53 - *
54 - * Now only the exit node would post dominate the entry node: there is no other node which is
55 - * guaranteed to be reachable. In this graph is is only safe to call hooks in bb0.
56 - */
57 -export function validateUnconditionalHooks(
58 - fn: HIRFunction
59 -): Result<PostDominator<BlockId>, CompilerError> {
60 - // Construct the set of blocks that is always reachable from the entry block.
61 - const unconditionalBlocks = new Set<BlockId>();
62 - const dominators = computePostDominatorTree(fn, {
63 - /*
64 - * Hooks must only be in a consistent order for executions that return normally,
65 - * so we opt-in to viewing throw as a non-exit node.
66 - */
67 - includeThrowsAsExitNode: false,
68 - });
69 - const exit = dominators.exit;
70 - let current: BlockId | null = fn.body.entry;
71 - while (current !== null && current !== exit) {
72 - CompilerError.invariant(!unconditionalBlocks.has(current), {
73 - reason:
74 - "Internal error: non-terminating loop in ValidateUnconditionalHooks",
75 - loc: null,
76 - suggestions: null,
77 - });
78 - unconditionalBlocks.add(current);
79 - current = dominators.get(current);
80 - }
81 -
82 - const errors = new CompilerError();
83 - function recordError(loc: SourceLocation): void {
84 - errors.pushErrorDetail(
85 - new CompilerErrorDetail({
86 - description: null,
87 - reason:
88 - "Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)",
89 - loc,
90 - severity: ErrorSeverity.InvalidReact,
91 - suggestions: null,
92 - })
93 - );
94 - }
95 -
96 - for (const [, block] of fn.body.blocks) {
97 - if (unconditionalBlocks.has(block.id)) {
98 - continue;
99 - }
100 - for (const instr of block.instructions) {
101 - if (
102 - instr.value.kind === "CallExpression" &&
103 - getHookKind(fn.env, instr.value.callee.identifier) != null
104 - ) {
105 - /*
106 - * TODO: the current ESLint rule has different error messages for code that is called conditionally, in a loop, etc.
107 - * An option would be to first record an Array<[BlockId, Place]> of problematic hooks, then compute the normal dominator graph
108 - * and walk upward to determine whether each error location was due to a loop, if, etc.
109 - */
110 - recordError(instr.loc);
111 - } else if (
112 - instr.value.kind === "MethodCall" &&
113 - getHookKind(fn.env, instr.value.property.identifier) != null
114 - ) {
115 - recordError(instr.loc);
116 - }
117 - }
118 - }
119 - if (errors.hasErrors()) {
120 - return Err(errors);
121 - } else {
122 - return Ok(dominators);
123 - }
124 -}
compiler/packages/babel-plugin-react-forget/src/Validation/index.ts
-1
@@ -10,5 +10,4 @@ export { validateHooksUsage } from "./ValidateHooksUsage";
10 export { validateMemoizedEffectDependencies } from "./ValidateMemoizedEffectDependencies";
11 export { validateNoRefAccessInRender } from "./ValidateNoRefAccesInRender";
12 export { validateNoSetStateInRender } from "./ValidateNoSetStateInRender";
13 -export { validateUnconditionalHooks } from "./ValidateUnconditionalHooks";
13 export { validateUseMemo } from "./ValidateUseMemo";
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.propertyload-hook.expect.md
-2
@@ -14,8 +14,6 @@ function Component() {
14
15 ```
16 [ReactForget] InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (2:2)
17 -
18 -[ReactForget] InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (3:3)
17 ```
18
19
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/allow-locals-named-like-hooks.expect.md new
+85
@@ -0,0 +1,85 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { makeObject_Primitives } from "shared-runtime";
6 +
7 +function Component(props) {
8 + let useFeature = makeObject_Primitives();
9 + let x;
10 + if (useFeature) {
11 + x = [useFeature + useFeature].push(-useFeature);
12 + }
13 + let y = useFeature;
14 + let z = useFeature.useProperty;
15 + return (
16 + <div onClick={useFeature}>
17 + {x}
18 + {y}
19 + {z}
20 + </div>
21 + );
22 +}
23 +
24 +export const FIXTURE_ENTRYPOINT = {
25 + fn: Component,
26 + params: [{}],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { unstable_useMemoCache as useMemoCache } from "react";
35 +import { makeObject_Primitives } from "shared-runtime";
36 +
37 +function Component(props) {
38 + const $ = useMemoCache(2);
39 + const useFeature = makeObject_Primitives();
40 + let x;
41 + if (useFeature) {
42 + let t0;
43 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
44 + t0 = [useFeature + useFeature].push(-useFeature);
45 + $[0] = t0;
46 + } else {
47 + t0 = $[0];
48 + }
49 + x = t0;
50 + }
51 +
52 + const y = useFeature;
53 + const z = useFeature.useProperty;
54 + let t1;
55 + if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
56 + t1 = (
57 + <div onClick={useFeature}>
58 + {x}
59 + {y}
60 + {z}
61 + </div>
62 + );
63 + $[1] = t1;
64 + } else {
65 + t1 = $[1];
66 + }
67 + return t1;
68 +}
69 +
70 +export const FIXTURE_ENTRYPOINT = {
71 + fn: Component,
72 + params: [{}],
73 +};
74 +
75 +```
76 +
77 +### Eval output
78 +(kind: exception) Objects are not valid as a React child (found: object with keys {a, b, c}). If you meant to render a collection of children, use an array instead.
79 +logs: ['The above error occurred in the <div> component:\n' +
80 + '\n' +
81 + ' at div\n' +
82 + ' at WrapperTestComponent (<project_root>/packages/sprout/dist/runner-evaluator.js:50:26)\n' +
83 + '\n' +
84 + 'Consider adding an error boundary to your tree to customize error handling behavior.\n' +
85 + 'Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.']
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/allow-locals-named-like-hooks.js new
+23
@@ -0,0 +1,23 @@
1 +import { makeObject_Primitives } from "shared-runtime";
2 +
3 +function Component(props) {
4 + let useFeature = makeObject_Primitives();
5 + let x;
6 + if (useFeature) {
7 + x = [useFeature + useFeature].push(-useFeature);
8 + }
9 + let y = useFeature;
10 + let z = useFeature.useProperty;
11 + return (
12 + <div onClick={useFeature}>
13 + {x}
14 + {y}
15 + {z}
16 + </div>
17 + );
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Component,
22 + params: [{}],
23 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/allow-props-named-like-hooks.expect.md new
+86
@@ -0,0 +1,86 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component({ useFeature }) {
6 + let x;
7 + if (useFeature) {
8 + x = [useFeature + useFeature].push(-useFeature);
9 + }
10 + let y = useFeature;
11 + let z = useFeature.useProperty;
12 + return (
13 + <div onClick={useFeature}>
14 + {x}
15 + {y}
16 + {z}
17 + </div>
18 + );
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: Component,
23 + params: [{}],
24 +};
25 +
26 +```
27 +
28 +## Code
29 +
30 +```javascript
31 +import { unstable_useMemoCache as useMemoCache } from "react";
32 +function Component(t28) {
33 + const $ = useMemoCache(8);
34 + const { useFeature } = t28;
35 + let x;
36 + if (useFeature) {
37 + const t0 = useFeature + useFeature;
38 + let t1;
39 + if ($[0] !== t0 || $[1] !== useFeature) {
40 + t1 = [t0].push(-useFeature);
41 + $[0] = t0;
42 + $[1] = useFeature;
43 + $[2] = t1;
44 + } else {
45 + t1 = $[2];
46 + }
47 + x = t1;
48 + }
49 +
50 + const y = useFeature;
51 + const z = useFeature.useProperty;
52 + let t2;
53 + if ($[3] !== useFeature || $[4] !== x || $[5] !== y || $[6] !== z) {
54 + t2 = (
55 + <div onClick={useFeature}>
56 + {x}
57 + {y}
58 + {z}
59 + </div>
60 + );
61 + $[3] = useFeature;
62 + $[4] = x;
63 + $[5] = y;
64 + $[6] = z;
65 + $[7] = t2;
66 + } else {
67 + t2 = $[7];
68 + }
69 + return t2;
70 +}
71 +
72 +export const FIXTURE_ENTRYPOINT = {
73 + fn: Component,
74 + params: [{}],
75 +};
76 +
77 +```
78 +
79 +### Eval output
80 +(kind: exception) Cannot read properties of undefined (reading 'useProperty')
81 +logs: ['The above error occurred in the <WrapperTestComponent> component:\n' +
82 + '\n' +
83 + ' at WrapperTestComponent (<project_root>/packages/sprout/dist/runner-evaluator.js:50:26)\n' +
84 + '\n' +
85 + 'Consider adding an error boundary to your tree to customize error handling behavior.\n' +
86 + 'Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.']
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/allow-props-named-like-hooks.js new
+20
@@ -0,0 +1,20 @@
1 +function Component({ useFeature }) {
2 + let x;
3 + if (useFeature) {
4 + x = [useFeature + useFeature].push(-useFeature);
5 + }
6 + let y = useFeature;
7 + let z = useFeature.useProperty;
8 + return (
9 + <div onClick={useFeature}>
10 + {x}
11 + {y}
12 + {z}
13 + </div>
14 + );
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{}],
20 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-conditionally-call-local-named-like-hook.expect.md new
+23
@@ -0,0 +1,23 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { makeObject_Primitives } from "shared-runtime";
6 +
7 +function Component(props) {
8 + const useFoo = makeObject_Primitives();
9 + if (props.cond) {
10 + useFoo();
11 + }
12 +}
13 +
14 +```
15 +
16 +
17 +## Error
18 +
19 +```
20 +[ReactForget] InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (6:6)
21 +```
22 +
23 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-conditionally-call-local-named-like-hook.js new
+8
@@ -0,0 +1,8 @@
1 +import { makeObject_Primitives } from "shared-runtime";
2 +
3 +function Component(props) {
4 + const useFoo = makeObject_Primitives();
5 + if (props.cond) {
6 + useFoo();
7 + }
8 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-conditionally-call-prop-named-like-hook.expect.md new
+20
@@ -0,0 +1,20 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component({ cond, useFoo }) {
6 + if (cond) {
7 + useFoo();
8 + }
9 +}
10 +
11 +```
12 +
13 +
14 +## Error
15 +
16 +```
17 +[ReactForget] InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (3:3)
18 +```
19 +
20 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-conditionally-call-prop-named-like-hook.js new
+5
@@ -0,0 +1,5 @@
1 +function Component({ cond, useFoo }) {
2 + if (cond) {
3 + useFoo();
4 + }
5 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-conditionally-methodcall-hooklike-property-of-local.expect.md new
+23
@@ -0,0 +1,23 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { makeObject_Primitives } from "shared-runtime";
6 +
7 +function Component(props) {
8 + const local = makeObject_Primitives();
9 + if (props.cond) {
10 + local.useFoo();
11 + }
12 +}
13 +
14 +```
15 +
16 +
17 +## Error
18 +
19 +```
20 +[ReactForget] InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (6:6)
21 +```
22 +
23 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-conditionally-methodcall-hooklike-property-of-local.js new
+8
@@ -0,0 +1,8 @@
1 +import { makeObject_Primitives } from "shared-runtime";
2 +
3 +function Component(props) {
4 + const local = makeObject_Primitives();
5 + if (props.cond) {
6 + local.useFoo();
7 + }
8 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-condtionally-call-hooklike-property-of-local.expect.md new
+24
@@ -0,0 +1,24 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { makeObject_Primitives } from "shared-runtime";
6 +
7 +function Component(props) {
8 + const local = makeObject_Primitives();
9 + if (props.cond) {
10 + const foo = local.useFoo;
11 + foo();
12 + }
13 +}
14 +
15 +```
16 +
17 +
18 +## Error
19 +
20 +```
21 +[ReactForget] InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (7:7)
22 +```
23 +
24 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-condtionally-call-hooklike-property-of-local.js new
+9
@@ -0,0 +1,9 @@
1 +import { makeObject_Primitives } from "shared-runtime";
2 +
3 +function Component(props) {
4 + const local = makeObject_Primitives();
5 + if (props.cond) {
6 + const foo = local.useFoo;
7 + foo();
8 + }
9 +}