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;
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user