Repository: facebook/react
Pull request: [eslint-plugin-react-hooks] Skip compilation for non-React files

Add a fast heuristic to detect whether a file may contain React components or hooks before running the full compiler. This avoids the overhead of Babel AST parsing and compilation for utility files, config files, and other non-React code.

The heuristic uses ESLint's already-parsed AST to check for functions with React-like names at module scope:
- Capitalized functions: MyComponent, Button, App
- Hook pattern functions: useEffect, useState, useMyCustomHook

Files without matching function names are skipped and return an empty result, which is cached to avoid re-checking for subsequent rules.

Also adds test coverage for the heuristic edge cases.

Changed files:
filename: packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleFlow-test.ts
patch: @@ -0,0 +1,147 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import {RuleTester} from 'eslint';
+import {allRules} from '../src/shared/ReactCompiler';
+
+const ESLintTesterV8 = require('eslint-v8').RuleTester;
+
+/**
+ * A string template tag that removes padding from the left side of multi-line strings
+ * @param {Array} strings array of code strings (only one expected)
+ */
+function normalizeIndent(strings: TemplateStringsArray): string {
+  const codeLines = strings[0]?.split('\n') ?? [];
+  const leftPadding = codeLines[1]?.match(/\s+/)![0] ?? '';
+  return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
+}
+
+type CompilerTestCases = {
+  valid: RuleTester.ValidTestCase[];
+  invalid: RuleTester.InvalidTestCase[];
+};
+
+const tests: CompilerTestCases = {
+  valid: [
+    // ===========================================
+    // Tests for mayContainReactCode heuristic with Flow syntax
+    // Files that should be SKIPPED (no React-like function names)
+    // These contain code that WOULD trigger errors if compiled,
+    // but since the heuristic skips them, no errors are reported.
+    // ===========================================
+    {
+      name: '[Heuristic/Flow] Skips files with only lowercase utility functions',
+      filename: 'utils.js',
+      code: normalizeIndent`
+        function helper(obj) {
+          obj.key = 'value';
+          return obj;
+        }
+      `,
+    },
+    {
+      name: '[Heuristic/Flow] Skips lowercase arrow functions even with mutations',
+      filename: 'helpers.js',
+      code: normalizeIndent`
+        const processData = (input) => {
+          input.modified = true;
+          return input;
+        };
+      `,
+    },
+  ],
+  invalid: [
+    // ===========================================
+    // Tests for mayContainReactCode heuristic with Flow component/hook syntax
+    // These use Flow's component/hook declarations which should be detected
+    // ===========================================
+    {
+      name: '[Heuristic/Flow] Compiles Flow component declaration - detects prop mutation',
+      filename: 'component.js',
+      code: normalizeIndent`
+        component MyComponent(a: {key: string}) {
+          a.key = 'value';
+          return <div />;
+        }
+      `,
+      errors: [
+        {
+          message: /Modifying component props/,
+        },
+      ],
+    },
+    {
+      name: '[Heuristic/Flow] Compiles exported Flow component declaration - detects prop mutation',
+      filename: 'component.js',
+      code: normalizeIndent`
+        export component MyComponent(a: {key: string}) {
+          a.key = 'value';
+          return <div />;
+        }
+      `,
+      errors: [
+        {
+          message: /Modifying component props/,
+        },
+      ],
+    },
+    {
+      name: '[Heuristic/Flow] Compiles default exported Flow component declaration - detects prop mutation',
+      filename: 'component.js',
+      code: normalizeIndent`
+        export default component MyComponent(a: {key: string}) {
+          a.key = 'value';
+          return <div />;
+        }
+      `,
+      errors: [
+        {
+          message: /Modifying component props/,
+        },
+      ],
+    },
+    {
+      name: '[Heuristic/Flow] Compiles Flow hook declaration - detects argument mutation',
+      filename: 'hooks.js',
+      code: normalizeIndent`
+        hook useMyHook(a: {key: string}) {
+          a.key = 'value';
+          return a;
+        }
+      `,
+      errors: [
+        {
+          message: /Modifying component props or hook arguments/,
+        },
+      ],
+    },
+    {
+      name: '[Heuristic/Flow] Compiles exported Flow hook declaration - detects argument mutation',
+      filename: 'hooks.js',
+      code: normalizeIndent`
+        export hook useMyHook(a: {key: string}) {
+          a.key = 'value';
+          return a;
+        }
+      `,
+      errors: [
+        {
+          message: /Modifying component props or hook arguments/,
+        },
+      ],
+    },
+  ],
+};
+
+const eslintTester = new ESLintTesterV8({
+  parser: require.resolve('hermes-eslint'),
+  parserOptions: {
+    sourceType: 'module',
+    enableExperimentalComponentSyntax: true,
+  },
+});
+eslintTester.run('react-compiler', allRules['immutability'].rule, tests);

filename: packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts
patch: @@ -46,6 +46,35 @@ const tests: CompilerTestCases = {
         }
       `,
     },
+    // ===========================================
+    // Tests for mayContainReactCode heuristic
+    // Files that should be SKIPPED (no React-like function names)
+    // These contain code that WOULD trigger errors if compiled,
+    // but since the heuristic skips them, no errors are reported.
+    // ===========================================
+    {
+      name: '[Heuristic] Skips files with only lowercase utility functions',
+      filename: 'utils.ts',
+      // This mutates an argument, which would be flagged in a component/hook,
+      // but this file is skipped because there are no React-like function names
+      code: normalizeIndent`
+        function helper(obj) {
+          obj.key = 'value';
+          return obj;
+        }
+      `,
+    },
+    {
+      name: '[Heuristic] Skips lowercase arrow functions even with mutations',
+      filename: 'helpers.ts',
+      // Would be flagged if compiled, but skipped due to lowercase name
+      code: normalizeIndent`
+        const processData = (input) => {
+          input.modified = true;
+          return input;
+        };
+      `,
+    },
   ],
   invalid: [
     {
@@ -68,6 +97,101 @@ const tests: CompilerTestCases = {
         },
       ],
     },
+    // ===========================================
+    // Tests for mayContainReactCode heuristic
+    // Files that SHOULD be compiled (have React-like function names)
+    // These contain violations to prove compilation happens.
+    // ===========================================
+    {
+      name: '[Heuristic] Compiles PascalCase function declaration - detects prop mutation',
+      filename: 'component.tsx',
+      code: normalizeIndent`
+        function MyComponent({a}) {
+          a.key = 'value';
+          return <div />;
+        }
+      `,
+      errors: [
+        {
+          message: /Modifying component props/,
+        },
+      ],
+    },
+    {
+      name: '[Heuristic] Compiles PascalCase arrow function - detects prop mutation',
+      filename: 'component.tsx',
+      code: normalizeIndent`
+        const MyComponent = ({a}) => {
+          a.key = 'value';
+          return <div />;
+        };
+      `,
+      errors: [
+        {
+          message: /Modifying component props/,
+        },
+      ],
+    },
+    {
+      name: '[Heuristic] Compiles PascalCase function expression - detects prop mutation',
+      filename: 'component.tsx',
+      code: normalizeIndent`
+        const MyComponent = function({a}) {
+          a.key = 'value';
+          return <div />;
+        };
+      `,
+      errors: [
+        {
+          message: /Modifying component props/,
+        },
+      ],
+    },
+    {
+      name: '[Heuristic] Compiles exported function declaration - detects prop mutation',
+      filename: 'component.tsx',
+      code: normalizeIndent`
+        export function MyComponent({a}) {
+          a.key = 'value';
+          return <div />;
+        }
+      `,
+      errors: [
+        {
+          message: /Modifying component props/,
+        },
+      ],
+    },
+    {
+      name: '[Heuristic] Compiles exported arrow function - detects prop mutation',
+      filename: 'component.tsx',
+      code: normalizeIndent`
+        export const MyComponent = ({a}) => {
+          a.key = 'value';
+          return <div />;
+        };
+      `,
+      errors: [
+        {
+          message: /Modifying component props/,
+        },
+      ],
+    },
+    {
+      name: '[Heuristic] Compiles default exported function - detects prop mutation',
+      filename: 'component.tsx',
+      code: normalizeIndent`
+        export default function MyComponent({a}) {
+          a.key = 'value';
+          return <div />;
+        }
+      `,
+      errors: [
+        {
+          message: /Modifying component props/,
+        },
+      ],
+    },
   ],
 };
 

filename: packages/eslint-plugin-react-hooks/jest.config.js
patch: @@ -5,4 +5,8 @@ process.env.NODE_ENV = 'development';
 module.exports = {
   setupFiles: [require.resolve('../../scripts/jest/setupEnvironment.js')],
   moduleFileExtensions: ['ts', 'js', 'json'],
+  moduleNameMapper: {
+    '^babel-plugin-react-compiler$':
+      '<rootDir>/../../compiler/packages/babel-plugin-react-compiler/dist/index.js',
+  },
 };

filename: packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts
patch: @@ -17,10 +17,107 @@ import BabelPluginReactCompiler, {
   LoggerEvent,
 } from 'babel-plugin-react-compiler';
 import type {SourceCode} from 'eslint';
+import type * as ESTree from 'estree';
 import * as HermesParser from 'hermes-parser';
 import {isDeepStrictEqual} from 'util';
 import type {ParseResult} from '@babel/parser';
 
+// Pattern for component names: starts with uppercase letter
+const COMPONENT_NAME_PATTERN = /^[A-Z]/;
+// Pattern for hook names: starts with 'use' followed by uppercase letter or digit
+const HOOK_NAME_PATTERN = /^use[A-Z0-9]/;
+
+/**
+ * Quick heuristic using ESLint's already-parsed AST to detect if the file
+ * may contain React components or hooks based on function naming patterns.
+ * Only checks top-level declarations since components/hooks are declared at module scope.
+ * Returns true if compilation should proceed, false to skip.
+ */
+function mayContainReactCode(sourceCode: SourceCode): boolean {
+  const ast = sourceCode.ast;
+
+  // Only check top-level statements - components/hooks are declared at module scope
+  for (const node of ast.body) {
+    if (checkTopLevelNode(node)) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+function checkTopLevelNode(node: ESTree.Node): boolean {
+  // Handle Flow component/hook declarations (hermes-eslint produces these node types)
+  // @ts-expect-error not part of ESTree spec
+  if (node.type === 'ComponentDeclaration' || node.type === 'HookDeclaration') {
+    return true;
+  }
+
+  // Handle: export function MyComponent() {} or export const useHook = () => {}
+  if (node.type === 'ExportNamedDeclaration') {
+    const decl = (node as ESTree.ExportNamedDeclaration).declaration;
+    if (decl != null) {
+      return checkTopLevelNode(decl);
+    }
+    return false;
+  }
+
+  // Handle: export default function MyComponent() {} or export default () => {}
+  if (node.type === 'ExportDefaultDeclaration') {
+    const decl = (node as ESTree.ExportDefaultDeclaration).declaration;
+    // Anonymous default function export - compile conservatively
+    if (
+      decl.type === 'FunctionExpression' ||
+      decl.type === 'ArrowFunctionExpression' ||
+      (decl.type === 'FunctionDeclaration' &&
+        (decl as ESTree.FunctionDeclaration).id == null)
+    ) {
+      return true;
+    }
+    return checkTopLevelNode(decl as ESTree.Node);
+  }
+
+  // Handle: function MyComponent() {}
+  // Also handles Flow component/hook syntax transformed to FunctionDeclaration with flags
+  if (node.type === 'FunctionDeclaration') {
+    // Check for Hermes-added flags indicating Flow component/hook syntax
+    if (
+      '__componentDeclaration' in node ||
+      '__hookDeclaration' in node
+    ) {
+      return true;
+    }
+    const id = (node as ESTree.FunctionDeclaration).id;
+    if (id != null) {
+      const name = id.name;
+      if (COMPONENT_NAME_PATTERN.test(name) || HOOK_NAME_PATTERN.test(name)) {
+        return true;
+      }
+    }
+  }
+
+  // Handle: const MyComponent = () => {} or const useHook = function() {}
+  if (node.type === 'VariableDeclaration') {
+    for (const decl of (node as ESTree.VariableDeclaration).declarations) {
+      if (decl.id.type === 'Identifier') {
+        const init = decl.init;
+        if (
+          init != null &&
+          (init.type === 'ArrowFunctionExpression' ||
+            init.type === 'FunctionExpression')
+        ) {
+          const name = decl.id.name;
+          if (COMPONENT_NAME_PATTERN.test(name) || HOOK_NAME_PATTERN.test(name)) {
+            return true;
+          }
+        }
+      }
+    }
+  }
+
+  return false;
+}
+
 const COMPILER_OPTIONS: PluginOptions = {
   outputMode: 'lint',
   panicThreshold: 'none',
@@ -216,6 +313,24 @@ export default function runReactCompiler({
     return entry;
   }
 
+  // Quick heuristic: skip files that don't appear to contain React code.
+  // We still cache the empty result so subsequent rules don't re-run the check.
+  if (!mayContainReactCode(sourceCode)) {
+    const emptyResult: RunCacheEntry = {
+      sourceCode: sourceCode.text,
+      filename,
+      userOpts,
+      flowSuppressions: [],
+      events: [],
+    };
+    if (entry != null) {
+      Object.assign(entry, emptyResult);
+    } else {
+      cache.push(filename, emptyResult);
+    }
+    return {...emptyResult};
+  }
+
   const runEntry = runReactCompilerImpl({
     sourceCode,
     filename,