diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts index 80ce909f35aa..8016e43597c7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -1324,6 +1324,13 @@ function getFunctionName( if (parent.isVariableDeclarator() && parent.get('init').node === path.node) { // const useHook = () => {}; id = parent.get('id'); + } else if ( + parent.isCallExpression() && + parent.parentPath.isVariableDeclarator() && + parent.parentPath.get('init').node === parent.node + ) { + // const MyComponent = wrap(() => {}); + id = parent.parentPath.get('id'); } else if ( parent.isAssignmentExpression() && parent.get('right').node === path.node && diff --git a/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts b/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts index a0d0f6bdbc8e..7c562572d935 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts +++ b/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts @@ -192,6 +192,22 @@ const tests: CompilerTestCases = { }, ], }, + { + name: '[Heuristic] Compiles HOF-wrapped PascalCase component - detects prop mutation', + filename: 'component.tsx', + code: normalizeIndent` + const wrap = (value) => value; + const MyComponent = wrap(({a}) => { + a.key = 'value'; + return
; + }); + `, + errors: [ + { + message: /Modifying component props/, + }, + ], + }, ], }; @@ -199,3 +215,71 @@ const eslintTester = new ESLintTesterV8({ parser: require.resolve('@typescript-eslint/parser-v5'), }); eslintTester.run('react-compiler', allRules['immutability'].rule, tests); + +// Tests for set-state-in-effect rule with HOF-wrapped components +// Reproduction test for https://github.com/facebook/react/issues/35910 +const setStateInEffectTests: CompilerTestCases = { + valid: [ + { + name: 'Direct component with no setState in effect', + filename: 'test.tsx', + code: normalizeIndent` + import { useEffect, useState } from 'react'; + function DirectComponent() { + const [value, setValue] = useState(0); + useEffect(() => { + console.log(value); + }, []); + return
{value}
; + } + `, + }, + ], + invalid: [ + { + name: 'Direct component with setState in effect', + filename: 'test.tsx', + code: normalizeIndent` + import { useEffect, useState } from 'react'; + function DirectComponent() { + const [value, setValue] = useState(0); + useEffect(() => { + setValue(1); + }, []); + return
{value}
; + } + `, + errors: [ + { + message: /Calling setState synchronously within an effect/, + }, + ], + }, + { + name: 'Anonymous component callback passed to HOF has setState in effect', + filename: 'test.tsx', + code: normalizeIndent` + import { useEffect, useState } from 'react'; + const wrap = (value) => value; + const WrappedComponent = wrap(() => { + const [value, setValue] = useState(0); + useEffect(() => { + setValue(1); + }, []); + return
{value}
; + }); + `, + errors: [ + { + message: /Calling setState synchronously within an effect/, + }, + ], + }, + ], +}; + +eslintTester.run( + 'react-compiler-set-state-in-effect', + allRules['set-state-in-effect'].rule, + setStateInEffectTests, +); diff --git a/packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts b/packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts index 9aaddb07e656..8dcaf70365c5 100644 --- a/packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts +++ b/packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts @@ -97,6 +97,7 @@ function checkTopLevelNode(node: ESTree.Node): boolean { } // Handle: const MyComponent = () => {} or const useHook = function() {} + // Also handles: const MyComponent = wrap(() => {}) if (node.type === 'VariableDeclaration') { for (const decl of (node as ESTree.VariableDeclaration).declarations) { if (decl.id.type === 'Identifier') { @@ -104,7 +105,8 @@ function checkTopLevelNode(node: ESTree.Node): boolean { if ( init != null && (init.type === 'ArrowFunctionExpression' || - init.type === 'FunctionExpression') + init.type === 'FunctionExpression' || + init.type === 'CallExpression') ) { const name = decl.id.name; if (COMPONENT_NAME_PATTERN.test(name) || HOOK_NAME_PATTERN.test(name)) {