fix(linter): only run React Compiler rules on files with top-level components or hooks

Closes #27604
Closes #19957
This commit is contained in:
Boshen committed 2026-10-29 15:21:32 +08:00
1 parent 4c9d5aa496
commit c67167ef70
10 files changed
+174 -31

No files matched your search

@@ -0,0 +1,5 @@
{
"categories": { "correctness": "off" },
"plugins": ["react"],
"rules": { "react/globals": "error", "react/immutability": "error" }
}
@@ -0,0 +1,10 @@
import { useState } from "react";
let count = 0;
// A top-level hook declared with an arrow function is linted.
export const useCounter = () => {
const [step] = useState(1);
count = count + step;
return count;
};
@@ -0,0 +1,8 @@
let someGlobal = false;
// Only a `React.memo` call at the top level, so the file is skipped like in
// `eslint-plugin-react-hooks`.
export const Component = React.memo(function (props) {
someGlobal = true;
return <div>{props.text}</div>;
});
@@ -0,0 +1,9 @@
import { useState } from "react";
let result: number | null = null;
// A top-level component is linted.
export function Probe() {
result = useState(1)[0];
return null;
}
@@ -0,0 +1,13 @@
import { useState } from "react";
// No top-level component or hook, so the file is skipped like in
// `eslint-plugin-react-hooks`.
export function mountProbe(): number | null {
let result: number | null = null;
function Probe() {
result = useState(1)[0];
return null;
}
void Probe;
return result;
}
@@ -0,0 +1,36 @@
# Exit code
1
# stdout
```
x react(globals): Cannot reassign variables declared outside of the component/hook
,-[files/arrow-hook.js:8:3]
7 | const [step] = useState(1);
8 | count = count + step;
: ^^|^^
: `-- `count` cannot be reassigned
9 | return count;
`----
help: Variable `count` is declared outside of the component/hook. Reassigning this value during render is a side effect which can cause unpredictable behavior. If this variable is used in
rendering, use useState instead. Otherwise, update it in an effect
note: React Compiler skipped optimizing this component or hook. Additional guidance: https://react.dev/reference/eslint-plugin-react-hooks/lints/globals
x react(globals): Cannot reassign variables declared outside of the component/hook
,-[files/module-level-probe.test.tsx:7:3]
6 | export function Probe() {
7 | result = useState(1)[0];
: ^^^|^^
: `-- `result` cannot be reassigned
8 | return null;
`----
help: Variable `result` is declared outside of the component/hook. Reassigning this value during render is a side effect which can cause unpredictable behavior. If this variable is used in
rendering, use useState instead. Otherwise, update it in an effect
note: React Compiler skipped optimizing this component or hook. Additional guidance: https://react.dev/reference/eslint-plugin-react-hooks/lints/globals
Found 0 warnings and 2 errors.
Finished in Xms on 4 files with 2 rules using X threads.
```
# stderr
```
```
@@ -1,7 +1,12 @@
let someGlobal = false; let someGlobal = false;
// An anonymous component wrapped in `React.memo` is linted. // An anonymous component wrapped in `React.memo` is linted when the file also
// has a top-level component.
export const Component = React.memo(function (props) { export const Component = React.memo(function (props) {
someGlobal = true; someGlobal = true;
return <div>{props.text}</div>; return <div>{props.text}</div>;
}); });
export function App() {
return <Component text="hello" />;
}
@@ -4,12 +4,12 @@
# stdout # stdout
``` ```
x react(globals): Cannot reassign variables declared outside of the component/hook x react(globals): Cannot reassign variables declared outside of the component/hook
,-[files/memo.jsx:5:3] ,-[files/memo.jsx:6:3]
4 | export const Component = React.memo(function (props) { 5 | export const Component = React.memo(function (props) {
5 | someGlobal = true; 6 | someGlobal = true;
: ^^^^^|^^^^ : ^^^^^|^^^^
: `-- `someGlobal` cannot be reassigned : `-- `someGlobal` cannot be reassigned
6 | return <div>{props.text}</div>; 7 | return <div>{props.text}</div>;
`---- `----
help: Variable `someGlobal` is declared outside of the component/hook. Reassigning this value during render is a side effect which can cause unpredictable behavior. If this variable is used in help: Variable `someGlobal` is declared outside of the component/hook. Reassigning this value during render is a side effect which can cause unpredictable behavior. If this variable is used in
rendering, use useState instead. Otherwise, update it in an effect rendering, use useState instead. Otherwise, update it in an effect
+61 -4
View File
@@ -1,6 +1,10 @@
use std::borrow::Cow; use std::borrow::Cow;
use lazy_regex::Regex; use lazy_regex::Regex;
use oxc_ast::ast::{
BindingPattern, Declaration, ExportDefaultDeclarationKind, Expression, FunctionType, Program,
Statement,
};
use oxc_diagnostics::OxcCode; use oxc_diagnostics::OxcCode;
use oxc_react_compiler::{ use oxc_react_compiler::{
CompilerOutputMode, EnvironmentConfig, ErrorCategory, ExhaustiveEffectDepsMode, LintDiagnostic, CompilerOutputMode, EnvironmentConfig, ErrorCategory, ExhaustiveEffectDepsMode, LintDiagnostic,
@@ -114,15 +118,68 @@ pub fn build_react_compiler_results(host: &ContextHost) -> ReactCompilerResults
} }
/// Shared `should_run` for the React Compiler family: match the upstream /// Shared `should_run` for the React Compiler family: match the upstream
/// `node_modules` source filter and skip files with multiple `<script>` sections /// `node_modules` source filter and [`may_contain_react_code`] gate, and skip files
/// (vue/astro/svelte). The latter is required for correctness, not just speed — /// with multiple `<script>` sections (vue/astro/svelte). The latter is required for
/// the cache on [`ContextHost`] is per file, so it would replay the first script /// correctness, not just speed — the cache on [`ContextHost`] is per file, so it
/// section's findings for every later section. /// would replay the first script section's findings for every later section.
pub fn should_run_react_compiler(ctx: &ContextHost) -> bool { pub fn should_run_react_compiler(ctx: &ContextHost) -> bool {
!ctx.file_path().to_string_lossy().contains("node_modules") !ctx.file_path().to_string_lossy().contains("node_modules")
&& !ctx && !ctx
.file_extension() .file_extension()
.is_some_and(|ext| LINT_PARTIAL_LOADER_EXTENSIONS.iter().any(|e| e == &ext)) .is_some_and(|ext| LINT_PARTIAL_LOADER_EXTENSIONS.iter().any(|e| e == &ext))
&& may_contain_react_code(ctx.semantic().nodes().program())
}
/// Port of `mayContainReactCode` in `eslint-plugin-react-hooks`'s
/// `src/shared/RunReactCompiler.ts`: only compile files with a top-level function
/// named like a component or hook, or an anonymous default-exported function.
fn may_contain_react_code(program: &Program) -> bool {
program.body.iter().any(is_top_level_react_code)
}
fn is_top_level_react_code(statement: &Statement) -> bool {
match statement {
Statement::ExportDeclaration(export) => is_react_declaration(&export.declaration),
Statement::ExportDefaultDeclaration(export) => match &export.declaration {
ExportDefaultDeclarationKind::FunctionDeclaration(func) => {
func.r#type == FunctionType::FunctionDeclaration
&& func.id.as_ref().is_none_or(|id| is_component_or_hook_name(&id.name))
}
declaration => declaration.as_expression().is_some_and(is_function_expression),
},
_ => statement.as_declaration().is_some_and(is_react_declaration),
}
}
fn is_react_declaration(declaration: &Declaration) -> bool {
match declaration {
Declaration::FunctionDeclaration(func) => {
func.r#type == FunctionType::FunctionDeclaration
&& func.id.as_ref().is_some_and(|id| is_component_or_hook_name(&id.name))
}
Declaration::VariableDeclaration(declaration) => {
declaration.declarations.iter().any(|declarator| {
matches!(&declarator.id, BindingPattern::BindingIdentifier(id) if is_component_or_hook_name(&id.name))
&& declarator.init.as_ref().is_some_and(is_function_expression)
})
}
_ => false,
}
}
fn is_function_expression(expr: &Expression) -> bool {
matches!(
expr.without_parentheses(),
Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)
)
}
/// `/^[A-Z]/` or `/^use[A-Z0-9]/`.
fn is_component_or_hook_name(name: &str) -> bool {
let bytes = name.as_bytes();
bytes.first().is_some_and(u8::is_ascii_uppercase)
|| (bytes.starts_with(b"use")
&& bytes.get(3).is_some_and(|b| b.is_ascii_uppercase() || b.is_ascii_digit()))
} }
/// Shared `run_once` body for the React Compiler family of rules: report the /// Shared `run_once` body for the React Compiler family of rules: report the
+22 -22
View File
@@ -375,22 +375,22 @@ promise/prefer-catch | 24802
promise/spec-only | 95297 promise/spec-only | 95297
promise/valid-params | 62606 promise/valid-params | 62606
react/button-has-type | 7019 react/button-has-type | 7019
react/capitalized-calls | 7 react/capitalized-calls | 4
react/checked-requires-onchange-or-readonly | 63058 react/checked-requires-onchange-or-readonly | 63058
react/display-name | 7 react/display-name | 7
react/error-boundaries | 7 react/error-boundaries | 4
react/exhaustive-deps | 62606 react/exhaustive-deps | 62606
react/exhaustive-effect-dependencies | 7 react/exhaustive-effect-dependencies | 4
react/forbid-component-props | 452 react/forbid-component-props | 452
react/forward-ref-uses-ref | 16812 react/forward-ref-uses-ref | 16812
react/function-component-definition | 16812 react/function-component-definition | 16812
react/globals | 7 react/globals | 4
react/hook-use-state | 62606 react/hook-use-state | 62606
react/hooks | 7 react/hooks | 4
react/iframe-missing-sandbox | 63058 react/iframe-missing-sandbox | 63058
react/immutability | 7 react/immutability | 4
react/incompatible-library | 7 react/incompatible-library | 4
react/invariant | 7 react/invariant | 4
react/jsx-boolean-value | 452 react/jsx-boolean-value | 452
react/jsx-curly-brace-presence | 459 react/jsx-curly-brace-presence | 459
react/jsx-filename-extension | 7 react/jsx-filename-extension | 7
@@ -409,13 +409,13 @@ react/jsx-no-useless-fragment | 459
react/jsx-pascal-case | 452 react/jsx-pascal-case | 452
react/jsx-props-no-spread-multi | 452 react/jsx-props-no-spread-multi | 452
react/jsx-props-no-spreading | 6 react/jsx-props-no-spreading | 6
react/memo-dependencies | 7 react/memo-dependencies | 4
react/no-array-index-key | 63058 react/no-array-index-key | 63058
react/no-children-prop | 14929 react/no-children-prop | 14929
react/no-clone-element | 62606 react/no-clone-element | 62606
react/no-danger | 7019 react/no-danger | 7019
react/no-danger-with-children | 63058 react/no-danger-with-children | 63058
react/no-deriving-state-in-effects | 7 react/no-deriving-state-in-effects | 4
react/no-did-mount-set-state | 6567 react/no-did-mount-set-state | 6567
react/no-did-update-set-state | 62606 react/no-did-update-set-state | 62606
react/no-direct-mutation-state | 1398 react/no-direct-mutation-state | 1398
@@ -438,25 +438,25 @@ react/no-will-update-set-state | 6567
react/only-export-components | 3 react/only-export-components | 3
react/prefer-es6-class | 6745 react/prefer-es6-class | 6745
react/prefer-function-component | 178 react/prefer-function-component | 178
react/preserve-manual-memoization | 7 react/preserve-manual-memoization | 4
react/purity | 7 react/purity | 4
react/react-in-jsx-scope | 459 react/react-in-jsx-scope | 459
react/refs | 7 react/refs | 4
react/require-render-return | 2604 react/require-render-return | 2604
react/rule-suppression | 7 react/rule-suppression | 4
react/rules-of-hooks | 62606 react/rules-of-hooks | 62606
react/self-closing-comp | 452 react/self-closing-comp | 452
react/set-state-in-effect | 7 react/set-state-in-effect | 4
react/set-state-in-render | 7 react/set-state-in-render | 4
react/state-in-constructor | 13763 react/state-in-constructor | 13763
react/static-components | 7 react/static-components | 4
react/style-prop-object | 7019 react/style-prop-object | 7019
react/syntax | 7 react/syntax | 4
react/todo | 7 react/todo | 4
react/unsupported-syntax | 7 react/unsupported-syntax | 4
react/use-memo | 7 react/use-memo | 4
react/void-dom-elements-no-children | 7019 react/void-dom-elements-no-children | 7019
react/void-use-memo | 7 react/void-use-memo | 4
react_perf/jsx-no-jsx-as-prop | 804 react_perf/jsx-no-jsx-as-prop | 804
react_perf/jsx-no-new-array-as-prop | 804 react_perf/jsx-no-new-array-as-prop | 804
react_perf/jsx-no-new-function-as-prop | 804 react_perf/jsx-no-new-function-as-prop | 804