mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(linter): only run React Compiler rules on files with top-level components or hooks
Closes #27604 Closes #19957
This commit is contained in:
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>;
|
||||
});
|
||||
+9
@@ -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;
|
||||
}
|
||||
+13
@@ -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;
|
||||
|
||||
// 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) {
|
||||
someGlobal = true;
|
||||
return <div>{props.text}</div>;
|
||||
});
|
||||
|
||||
export function App() {
|
||||
return <Component text="hello" />;
|
||||
}
|
||||
@@ -4,12 +4,12 @@
|
||||
# stdout
|
||||
```
|
||||
x react(globals): Cannot reassign variables declared outside of the component/hook
|
||||
,-[files/memo.jsx:5:3]
|
||||
4 | export const Component = React.memo(function (props) {
|
||||
5 | someGlobal = true;
|
||||
,-[files/memo.jsx:6:3]
|
||||
5 | export const Component = React.memo(function (props) {
|
||||
6 | someGlobal = true;
|
||||
: ^^^^^|^^^^
|
||||
: `-- `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
|
||||
rendering, use useState instead. Otherwise, update it in an effect
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use lazy_regex::Regex;
|
||||
use oxc_ast::ast::{
|
||||
BindingPattern, Declaration, ExportDefaultDeclarationKind, Expression, FunctionType, Program,
|
||||
Statement,
|
||||
};
|
||||
use oxc_diagnostics::OxcCode;
|
||||
use oxc_react_compiler::{
|
||||
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
|
||||
/// `node_modules` source filter and skip files with multiple `<script>` sections
|
||||
/// (vue/astro/svelte). The latter is required for correctness, not just speed —
|
||||
/// the cache on [`ContextHost`] is per file, so it would replay the first script
|
||||
/// section's findings for every later section.
|
||||
/// `node_modules` source filter and [`may_contain_react_code`] gate, and skip files
|
||||
/// with multiple `<script>` sections (vue/astro/svelte). The latter is required for
|
||||
/// correctness, not just speed — the cache on [`ContextHost`] is per file, so it
|
||||
/// would replay the first script section's findings for every later section.
|
||||
pub fn should_run_react_compiler(ctx: &ContextHost) -> bool {
|
||||
!ctx.file_path().to_string_lossy().contains("node_modules")
|
||||
&& !ctx
|
||||
.file_extension()
|
||||
.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
|
||||
|
||||
@@ -375,22 +375,22 @@ promise/prefer-catch | 24802
|
||||
promise/spec-only | 95297
|
||||
promise/valid-params | 62606
|
||||
react/button-has-type | 7019
|
||||
react/capitalized-calls | 7
|
||||
react/capitalized-calls | 4
|
||||
react/checked-requires-onchange-or-readonly | 63058
|
||||
react/display-name | 7
|
||||
react/error-boundaries | 7
|
||||
react/error-boundaries | 4
|
||||
react/exhaustive-deps | 62606
|
||||
react/exhaustive-effect-dependencies | 7
|
||||
react/exhaustive-effect-dependencies | 4
|
||||
react/forbid-component-props | 452
|
||||
react/forward-ref-uses-ref | 16812
|
||||
react/function-component-definition | 16812
|
||||
react/globals | 7
|
||||
react/globals | 4
|
||||
react/hook-use-state | 62606
|
||||
react/hooks | 7
|
||||
react/hooks | 4
|
||||
react/iframe-missing-sandbox | 63058
|
||||
react/immutability | 7
|
||||
react/incompatible-library | 7
|
||||
react/invariant | 7
|
||||
react/immutability | 4
|
||||
react/incompatible-library | 4
|
||||
react/invariant | 4
|
||||
react/jsx-boolean-value | 452
|
||||
react/jsx-curly-brace-presence | 459
|
||||
react/jsx-filename-extension | 7
|
||||
@@ -409,13 +409,13 @@ react/jsx-no-useless-fragment | 459
|
||||
react/jsx-pascal-case | 452
|
||||
react/jsx-props-no-spread-multi | 452
|
||||
react/jsx-props-no-spreading | 6
|
||||
react/memo-dependencies | 7
|
||||
react/memo-dependencies | 4
|
||||
react/no-array-index-key | 63058
|
||||
react/no-children-prop | 14929
|
||||
react/no-clone-element | 62606
|
||||
react/no-danger | 7019
|
||||
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-update-set-state | 62606
|
||||
react/no-direct-mutation-state | 1398
|
||||
@@ -438,25 +438,25 @@ react/no-will-update-set-state | 6567
|
||||
react/only-export-components | 3
|
||||
react/prefer-es6-class | 6745
|
||||
react/prefer-function-component | 178
|
||||
react/preserve-manual-memoization | 7
|
||||
react/purity | 7
|
||||
react/preserve-manual-memoization | 4
|
||||
react/purity | 4
|
||||
react/react-in-jsx-scope | 459
|
||||
react/refs | 7
|
||||
react/refs | 4
|
||||
react/require-render-return | 2604
|
||||
react/rule-suppression | 7
|
||||
react/rule-suppression | 4
|
||||
react/rules-of-hooks | 62606
|
||||
react/self-closing-comp | 452
|
||||
react/set-state-in-effect | 7
|
||||
react/set-state-in-render | 7
|
||||
react/set-state-in-effect | 4
|
||||
react/set-state-in-render | 4
|
||||
react/state-in-constructor | 13763
|
||||
react/static-components | 7
|
||||
react/static-components | 4
|
||||
react/style-prop-object | 7019
|
||||
react/syntax | 7
|
||||
react/todo | 7
|
||||
react/unsupported-syntax | 7
|
||||
react/use-memo | 7
|
||||
react/syntax | 4
|
||||
react/todo | 4
|
||||
react/unsupported-syntax | 4
|
||||
react/use-memo | 4
|
||||
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-new-array-as-prop | 804
|
||||
react_perf/jsx-no-new-function-as-prop | 804
|
||||
|
||||
Reference in new issue
Block a user