From d6406d8360c5d90fc777dfd1a720ba235c5a8a12 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Fri, 16 Feb 2024 11:27:52 -0800 Subject: [PATCH] Validate hook calls in object methods Adds some test cases for hook calls in object methods. Initially we didn't catch these because InferTypes doesn't actually visit ObjectMethod bodies. Once we fix that we correctly reject these examples. --- .../src/TypeInference/InferTypes.ts | 6 ++- .../src/Validation/ValidateHooksUsage.ts | 1 + ...ion-expression-object-expression.expect.md | 39 +++++++++++++++++++ ...d-function-expression-object-expression.js | 18 +++++++++ ...lid-hook-in-nested-object-method.expect.md | 35 +++++++++++++++++ ...or.invalid-hook-in-nested-object-method.js | 14 +++++++ 6 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-function-expression-object-expression.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-function-expression-object-expression.js create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-object-method.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-object-method.js diff --git a/compiler/packages/babel-plugin-react-forget/src/TypeInference/InferTypes.ts b/compiler/packages/babel-plugin-react-forget/src/TypeInference/InferTypes.ts index a9a0e8d662..2fe2afeb69 100644 --- a/compiler/packages/babel-plugin-react-forget/src/TypeInference/InferTypes.ts +++ b/compiler/packages/babel-plugin-react-forget/src/TypeInference/InferTypes.ts @@ -69,7 +69,10 @@ function apply(func: HIRFunction, unifier: Unifier): void { const { lvalue, value } = instr; lvalue.identifier.type = unifier.get(lvalue.identifier.type); - if (value.kind === "FunctionExpression") { + if ( + value.kind === "FunctionExpression" || + value.kind === "ObjectMethod" + ) { apply(value.loweredFunc.func, unifier); } } @@ -300,6 +303,7 @@ function* generateInstructionTypes( } case "ObjectMethod": { + yield* generate(value.loweredFunc.func); yield equation(left, { kind: "ObjectMethod" }); break; } diff --git a/compiler/packages/babel-plugin-react-forget/src/Validation/ValidateHooksUsage.ts b/compiler/packages/babel-plugin-react-forget/src/Validation/ValidateHooksUsage.ts index 63da00e38f..dd42e09457 100644 --- a/compiler/packages/babel-plugin-react-forget/src/Validation/ValidateHooksUsage.ts +++ b/compiler/packages/babel-plugin-react-forget/src/Validation/ValidateHooksUsage.ts @@ -403,6 +403,7 @@ function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void { for (const [, block] of fn.body.blocks) { for (const instr of block.instructions) { switch (instr.value.kind) { + case "ObjectMethod": case "FunctionExpression": { visitFunctionExpression(errors, instr.value.loweredFunc.func); break; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-function-expression-object-expression.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-function-expression-object-expression.expect.md new file mode 100644 index 0000000000..8ee48893f7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-function-expression-object-expression.expect.md @@ -0,0 +1,39 @@ + +## Input + +```javascript +// @compilationMode(infer) +function Component() { + const f = () => { + const x = { + outer() { + const g = () => { + const y = { + inner() { + return useFoo(); + }, + }; + return y; + }; + }, + }; + return x; + }; +} + +``` + + +## Error + +``` + 7 | const y = { + 8 | inner() { +> 9 | return useFoo(); + | ^^^^^^ [ReactForget] InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (9:9) + 10 | }, + 11 | }; + 12 | return y; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-function-expression-object-expression.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-function-expression-object-expression.js new file mode 100644 index 0000000000..aead5b5e22 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-function-expression-object-expression.js @@ -0,0 +1,18 @@ +// @compilationMode(infer) +function Component() { + const f = () => { + const x = { + outer() { + const g = () => { + const y = { + inner() { + return useFoo(); + }, + }; + return y; + }; + }, + }; + return x; + }; +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-object-method.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-object-method.expect.md new file mode 100644 index 0000000000..b5d2deb664 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-object-method.expect.md @@ -0,0 +1,35 @@ + +## Input + +```javascript +// @compilationMode(infer) +function Component() { + const x = { + outer() { + const y = { + inner() { + return useFoo(); + }, + }; + return y; + }, + }; + return x; +} + +``` + + +## Error + +``` + 5 | const y = { + 6 | inner() { +> 7 | return useFoo(); + | ^^^^^^ [ReactForget] InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (7:7) + 8 | }, + 9 | }; + 10 | return y; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-object-method.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-object-method.js new file mode 100644 index 0000000000..5ba5a74c0d --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-object-method.js @@ -0,0 +1,14 @@ +// @compilationMode(infer) +function Component() { + const x = { + outer() { + const y = { + inner() { + return useFoo(); + }, + }; + return y; + }, + }; + return x; +}