mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(linter/react): implement react-compiler rule (#23202)
Adds a nursery `react/react-compiler` rule that runs the React Compiler (`oxc_react_compiler`) in lint-only mode and reports Rules of React violations: conditional hook calls, setState during render, ref access during render, mutation errors, etc. ### Rule - `run_once` rule calling `oxc_react_compiler::lint()` on the program; the compiler's own prefilter skips files with no React-like functions. - Lints with the same compiler options as `eslint-plugin-react-compiler` (`COMPILER_OPTIONS` in its `RunReactCompiler.ts`): `lint` output mode plus the validations the plugin force-enables (impure functions, capitalized calls, JSX in try, static components, setState in effects, etc.). - Reports only error-severity violations by default. Compiler bail-outs (skipped optimizations, e.g. unsupported syntax) are opt-in via `reportAllBailouts`. - Flow suppressions are handled like the eslint plugin: diagnostics whose primary location sits directly below a `$FlowFixMe[react-rule-hook]` / `$FlowFixMe[react-rule-unsafe-ref]` comment are dropped, while other diagnostics in the same function still surface. ### Tests All test cases from [`eslint-plugin-react-compiler/__tests__`](https://github.com/facebook/react/tree/main/compiler/packages/eslint-plugin-react-compiler/__tests__) (MIT) are ported, covering hooks order, capitalized calls, impure calls in render, JSX in try/catch, ref access in render, useMemo diagnostics, useState mutation (TS), `'use memo'`/`'use no forget'` directives, and Flow suppressions. The only case not ported is "Basic example with component syntax", which uses Flow's experimental `component` declaration syntax that oxc does not parse. `RustBackendComparison-test.ts` replicates the same cases for backend comparison, so it adds no new ones. Every diagnostic message and location in the snapshot matches the upstream assertions. ### Diagnostics `oxc_react_compiler` diagnostics previously had no spans. They now carry labeled spans built from the compiler's source locations (byte offsets flow through from oxc spans): `detail.loc` → sub-detail locations with their messages (e.g. "Found setState() in render") → enclosing function as fallback. Long descriptions moved from the message into `help`. This makes code frames and `oxlint-disable` directives work. Example output: ``` ⚠ react(react-compiler): RenderSetState: Cannot call setState during render ╭─[App.tsx:3:17] 2 │ const [state, setState] = useState(0); 3 │ setState(1); · ────┬─── · ╰── Found setState() in render 4 │ return <div>{state}</div>; ╰──── help: Calling setState during render may trigger an infinite loop. ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Cameron Clark <cameron.clark@hey.com>
This commit is contained in:
1 parent
d6d1189b58
commit
1490a0a069
11 files changed
+935
-22
No files matched your search
Generated
+1
@@ -2301,6 +2301,7 @@ dependencies = [
|
||||
"oxc_index",
|
||||
"oxc_macros",
|
||||
"oxc_parser",
|
||||
"oxc_react_compiler",
|
||||
"oxc_regular_expression",
|
||||
"oxc_resolver",
|
||||
"oxc_semantic",
|
||||
|
||||
@@ -1299,6 +1299,7 @@ export interface DummyRuleMap {
|
||||
"react/only-export-components"?: RuleNoConfig | [AllowWarnDeny, OnlyExportComponentsConfig];
|
||||
"react/prefer-es6-class"?: RuleNoConfig | [AllowWarnDeny, AlwaysNever];
|
||||
"react/prefer-function-component"?: RuleNoConfig | [AllowWarnDeny, PreferFunctionComponent];
|
||||
"react/react-compiler"?: RuleNoConfig | [AllowWarnDeny, ReactCompilerConfig];
|
||||
"react/react-in-jsx-scope"?: RuleNoConfig;
|
||||
"react/require-render-return"?: RuleNoConfig;
|
||||
"react/rules-of-hooks"?: RuleNoConfig;
|
||||
@@ -4557,6 +4558,15 @@ export interface PreferFunctionComponent {
|
||||
*/
|
||||
allowJsxUtilityClass?: boolean;
|
||||
}
|
||||
export interface ReactCompilerConfig {
|
||||
/**
|
||||
* Also report compiler bail-outs — places where React Compiler skipped a
|
||||
* component or hook (for example because of unsupported syntax) without
|
||||
* finding a rule violation. These do not indicate incorrect code, only
|
||||
* code that the compiler declined to optimize.
|
||||
*/
|
||||
reportAllBailouts?: boolean;
|
||||
}
|
||||
export interface SelfClosingComp {
|
||||
/**
|
||||
* Whether to enforce self-closing for custom components.
|
||||
|
||||
@@ -39,6 +39,7 @@ oxc_ecmascript = { workspace = true }
|
||||
oxc_index = { workspace = true }
|
||||
oxc_macros = { workspace = true, features = ["ruledocs"] }
|
||||
oxc_parser = { workspace = true }
|
||||
oxc_react_compiler = { workspace = true }
|
||||
oxc_regular_expression = { workspace = true }
|
||||
oxc_resolver = { workspace = true }
|
||||
oxc_semantic = { workspace = true, features = ["cfg", "linter"] }
|
||||
|
||||
@@ -2840,6 +2840,11 @@ impl RuleRunner for crate::rules::react::prefer_function_component::PreferFuncti
|
||||
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
|
||||
}
|
||||
|
||||
impl RuleRunner for crate::rules::react::react_compiler::ReactCompiler {
|
||||
const NODE_TYPES: Option<&AstTypesBitset> = None;
|
||||
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::RunOnce;
|
||||
}
|
||||
|
||||
impl RuleRunner for crate::rules::react::react_in_jsx_scope::ReactInJsxScope {
|
||||
const NODE_TYPES: Option<&AstTypesBitset> =
|
||||
Some(&AstTypesBitset::from_types(&[AstType::JSXFragment, AstType::JSXOpeningElement]));
|
||||
|
||||
+30
-1
@@ -470,6 +470,7 @@ pub use crate::rules::react::no_will_update_set_state::NoWillUpdateSetState as R
|
||||
pub use crate::rules::react::only_export_components::OnlyExportComponents as ReactOnlyExportComponents;
|
||||
pub use crate::rules::react::prefer_es6_class::PreferEs6Class as ReactPreferEs6Class;
|
||||
pub use crate::rules::react::prefer_function_component::PreferFunctionComponent as ReactPreferFunctionComponent;
|
||||
pub use crate::rules::react::react_compiler::ReactCompiler as ReactReactCompiler;
|
||||
pub use crate::rules::react::react_in_jsx_scope::ReactInJsxScope as ReactReactInJsxScope;
|
||||
pub use crate::rules::react::require_render_return::RequireRenderReturn as ReactRequireRenderReturn;
|
||||
pub use crate::rules::react::rules_of_hooks::RulesOfHooks as ReactRulesOfHooks;
|
||||
@@ -1295,6 +1296,7 @@ pub enum RuleEnum {
|
||||
ReactOnlyExportComponents(ReactOnlyExportComponents),
|
||||
ReactPreferEs6Class(ReactPreferEs6Class),
|
||||
ReactPreferFunctionComponent(ReactPreferFunctionComponent),
|
||||
ReactReactCompiler(ReactReactCompiler),
|
||||
ReactReactInJsxScope(ReactReactInJsxScope),
|
||||
ReactRequireRenderReturn(ReactRequireRenderReturn),
|
||||
ReactRulesOfHooks(ReactRulesOfHooks),
|
||||
@@ -2183,7 +2185,8 @@ const REACT_NO_WILL_UPDATE_SET_STATE_ID: usize = REACT_NO_UNSTABLE_NESTED_COMPON
|
||||
const REACT_ONLY_EXPORT_COMPONENTS_ID: usize = REACT_NO_WILL_UPDATE_SET_STATE_ID + 1usize;
|
||||
const REACT_PREFER_ES_6_CLASS_ID: usize = REACT_ONLY_EXPORT_COMPONENTS_ID + 1usize;
|
||||
const REACT_PREFER_FUNCTION_COMPONENT_ID: usize = REACT_PREFER_ES_6_CLASS_ID + 1usize;
|
||||
const REACT_REACT_IN_JSX_SCOPE_ID: usize = REACT_PREFER_FUNCTION_COMPONENT_ID + 1usize;
|
||||
const REACT_REACT_COMPILER_ID: usize = REACT_PREFER_FUNCTION_COMPONENT_ID + 1usize;
|
||||
const REACT_REACT_IN_JSX_SCOPE_ID: usize = REACT_REACT_COMPILER_ID + 1usize;
|
||||
const REACT_REQUIRE_RENDER_RETURN_ID: usize = REACT_REACT_IN_JSX_SCOPE_ID + 1usize;
|
||||
const REACT_RULES_OF_HOOKS_ID: usize = REACT_REQUIRE_RENDER_RETURN_ID + 1usize;
|
||||
const REACT_SELF_CLOSING_COMP_ID: usize = REACT_RULES_OF_HOOKS_ID + 1usize;
|
||||
@@ -3150,6 +3153,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(_) => REACT_ONLY_EXPORT_COMPONENTS_ID,
|
||||
Self::ReactPreferEs6Class(_) => REACT_PREFER_ES_6_CLASS_ID,
|
||||
Self::ReactPreferFunctionComponent(_) => REACT_PREFER_FUNCTION_COMPONENT_ID,
|
||||
Self::ReactReactCompiler(_) => REACT_REACT_COMPILER_ID,
|
||||
Self::ReactReactInJsxScope(_) => REACT_REACT_IN_JSX_SCOPE_ID,
|
||||
Self::ReactRequireRenderReturn(_) => REACT_REQUIRE_RENDER_RETURN_ID,
|
||||
Self::ReactRulesOfHooks(_) => REACT_RULES_OF_HOOKS_ID,
|
||||
@@ -4109,6 +4113,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(_) => ReactOnlyExportComponents::NAME,
|
||||
Self::ReactPreferEs6Class(_) => ReactPreferEs6Class::NAME,
|
||||
Self::ReactPreferFunctionComponent(_) => ReactPreferFunctionComponent::NAME,
|
||||
Self::ReactReactCompiler(_) => ReactReactCompiler::NAME,
|
||||
Self::ReactReactInJsxScope(_) => ReactReactInJsxScope::NAME,
|
||||
Self::ReactRequireRenderReturn(_) => ReactRequireRenderReturn::NAME,
|
||||
Self::ReactRulesOfHooks(_) => ReactRulesOfHooks::NAME,
|
||||
@@ -5084,6 +5089,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(_) => ReactOnlyExportComponents::CATEGORY,
|
||||
Self::ReactPreferEs6Class(_) => ReactPreferEs6Class::CATEGORY,
|
||||
Self::ReactPreferFunctionComponent(_) => ReactPreferFunctionComponent::CATEGORY,
|
||||
Self::ReactReactCompiler(_) => ReactReactCompiler::CATEGORY,
|
||||
Self::ReactReactInJsxScope(_) => ReactReactInJsxScope::CATEGORY,
|
||||
Self::ReactRequireRenderReturn(_) => ReactRequireRenderReturn::CATEGORY,
|
||||
Self::ReactRulesOfHooks(_) => ReactRulesOfHooks::CATEGORY,
|
||||
@@ -6066,6 +6072,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(_) => ReactOnlyExportComponents::FIX,
|
||||
Self::ReactPreferEs6Class(_) => ReactPreferEs6Class::FIX,
|
||||
Self::ReactPreferFunctionComponent(_) => ReactPreferFunctionComponent::FIX,
|
||||
Self::ReactReactCompiler(_) => ReactReactCompiler::FIX,
|
||||
Self::ReactReactInJsxScope(_) => ReactReactInJsxScope::FIX,
|
||||
Self::ReactRequireRenderReturn(_) => ReactRequireRenderReturn::FIX,
|
||||
Self::ReactRulesOfHooks(_) => ReactRulesOfHooks::FIX,
|
||||
@@ -7128,6 +7135,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(_) => ReactOnlyExportComponents::documentation(),
|
||||
Self::ReactPreferEs6Class(_) => ReactPreferEs6Class::documentation(),
|
||||
Self::ReactPreferFunctionComponent(_) => ReactPreferFunctionComponent::documentation(),
|
||||
Self::ReactReactCompiler(_) => ReactReactCompiler::documentation(),
|
||||
Self::ReactReactInJsxScope(_) => ReactReactInJsxScope::documentation(),
|
||||
Self::ReactRequireRenderReturn(_) => ReactRequireRenderReturn::documentation(),
|
||||
Self::ReactRulesOfHooks(_) => ReactRulesOfHooks::documentation(),
|
||||
@@ -8969,6 +8977,8 @@ impl RuleEnum {
|
||||
ReactPreferFunctionComponent::config_schema(generator)
|
||||
.or_else(|| ReactPreferFunctionComponent::schema(generator))
|
||||
}
|
||||
Self::ReactReactCompiler(_) => ReactReactCompiler::config_schema(generator)
|
||||
.or_else(|| ReactReactCompiler::schema(generator)),
|
||||
Self::ReactReactInJsxScope(_) => ReactReactInJsxScope::config_schema(generator)
|
||||
.or_else(|| ReactReactInJsxScope::schema(generator)),
|
||||
Self::ReactRequireRenderReturn(_) => ReactRequireRenderReturn::config_schema(generator)
|
||||
@@ -10550,6 +10560,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(_) => "react",
|
||||
Self::ReactPreferEs6Class(_) => "react",
|
||||
Self::ReactPreferFunctionComponent(_) => "react",
|
||||
Self::ReactReactCompiler(_) => "react",
|
||||
Self::ReactReactInJsxScope(_) => "react",
|
||||
Self::ReactRequireRenderReturn(_) => "react",
|
||||
Self::ReactRulesOfHooks(_) => "react",
|
||||
@@ -12364,6 +12375,9 @@ impl RuleEnum {
|
||||
Self::ReactPreferFunctionComponent(_) => Ok(Self::ReactPreferFunctionComponent(
|
||||
ReactPreferFunctionComponent::from_configuration(value)?,
|
||||
)),
|
||||
Self::ReactReactCompiler(_) => {
|
||||
Ok(Self::ReactReactCompiler(ReactReactCompiler::from_configuration(value)?))
|
||||
}
|
||||
Self::ReactReactInJsxScope(_) => {
|
||||
Ok(Self::ReactReactInJsxScope(ReactReactInJsxScope::from_configuration(value)?))
|
||||
}
|
||||
@@ -14073,6 +14087,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(rule) => rule.to_configuration(),
|
||||
Self::ReactPreferEs6Class(rule) => rule.to_configuration(),
|
||||
Self::ReactPreferFunctionComponent(rule) => rule.to_configuration(),
|
||||
Self::ReactReactCompiler(rule) => rule.to_configuration(),
|
||||
Self::ReactReactInJsxScope(rule) => rule.to_configuration(),
|
||||
Self::ReactRequireRenderReturn(rule) => rule.to_configuration(),
|
||||
Self::ReactRulesOfHooks(rule) => rule.to_configuration(),
|
||||
@@ -14918,6 +14933,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(rule) => rule.run(node, ctx),
|
||||
Self::ReactPreferEs6Class(rule) => rule.run(node, ctx),
|
||||
Self::ReactPreferFunctionComponent(rule) => rule.run(node, ctx),
|
||||
Self::ReactReactCompiler(rule) => rule.run(node, ctx),
|
||||
Self::ReactReactInJsxScope(rule) => rule.run(node, ctx),
|
||||
Self::ReactRequireRenderReturn(rule) => rule.run(node, ctx),
|
||||
Self::ReactRulesOfHooks(rule) => rule.run(node, ctx),
|
||||
@@ -15756,6 +15772,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(rule) => rule.run(node, ctx),
|
||||
Self::ReactPreferEs6Class(rule) => rule.run(node, ctx),
|
||||
Self::ReactPreferFunctionComponent(rule) => rule.run(node, ctx),
|
||||
Self::ReactReactCompiler(rule) => rule.run(node, ctx),
|
||||
Self::ReactReactInJsxScope(rule) => rule.run(node, ctx),
|
||||
Self::ReactRequireRenderReturn(rule) => rule.run(node, ctx),
|
||||
Self::ReactRulesOfHooks(rule) => rule.run(node, ctx),
|
||||
@@ -16601,6 +16618,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(rule) => rule.run_once(ctx),
|
||||
Self::ReactPreferEs6Class(rule) => rule.run_once(ctx),
|
||||
Self::ReactPreferFunctionComponent(rule) => rule.run_once(ctx),
|
||||
Self::ReactReactCompiler(rule) => rule.run_once(ctx),
|
||||
Self::ReactReactInJsxScope(rule) => rule.run_once(ctx),
|
||||
Self::ReactRequireRenderReturn(rule) => rule.run_once(ctx),
|
||||
Self::ReactRulesOfHooks(rule) => rule.run_once(ctx),
|
||||
@@ -17439,6 +17457,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(rule) => rule.run_once(ctx),
|
||||
Self::ReactPreferEs6Class(rule) => rule.run_once(ctx),
|
||||
Self::ReactPreferFunctionComponent(rule) => rule.run_once(ctx),
|
||||
Self::ReactReactCompiler(rule) => rule.run_once(ctx),
|
||||
Self::ReactReactInJsxScope(rule) => rule.run_once(ctx),
|
||||
Self::ReactRequireRenderReturn(rule) => rule.run_once(ctx),
|
||||
Self::ReactRulesOfHooks(rule) => rule.run_once(ctx),
|
||||
@@ -18415,6 +18434,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(rule) => rule.run_on_jest_node(jest_node, ctx),
|
||||
Self::ReactPreferEs6Class(rule) => rule.run_on_jest_node(jest_node, ctx),
|
||||
Self::ReactPreferFunctionComponent(rule) => rule.run_on_jest_node(jest_node, ctx),
|
||||
Self::ReactReactCompiler(rule) => rule.run_on_jest_node(jest_node, ctx),
|
||||
Self::ReactReactInJsxScope(rule) => rule.run_on_jest_node(jest_node, ctx),
|
||||
Self::ReactRequireRenderReturn(rule) => rule.run_on_jest_node(jest_node, ctx),
|
||||
Self::ReactRulesOfHooks(rule) => rule.run_on_jest_node(jest_node, ctx),
|
||||
@@ -19513,6 +19533,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(rule) => rule.run_on_jest_node(jest_node, ctx),
|
||||
Self::ReactPreferEs6Class(rule) => rule.run_on_jest_node(jest_node, ctx),
|
||||
Self::ReactPreferFunctionComponent(rule) => rule.run_on_jest_node(jest_node, ctx),
|
||||
Self::ReactReactCompiler(rule) => rule.run_on_jest_node(jest_node, ctx),
|
||||
Self::ReactReactInJsxScope(rule) => rule.run_on_jest_node(jest_node, ctx),
|
||||
Self::ReactRequireRenderReturn(rule) => rule.run_on_jest_node(jest_node, ctx),
|
||||
Self::ReactRulesOfHooks(rule) => rule.run_on_jest_node(jest_node, ctx),
|
||||
@@ -20481,6 +20502,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(rule) => rule.should_run(ctx),
|
||||
Self::ReactPreferEs6Class(rule) => rule.should_run(ctx),
|
||||
Self::ReactPreferFunctionComponent(rule) => rule.should_run(ctx),
|
||||
Self::ReactReactCompiler(rule) => rule.should_run(ctx),
|
||||
Self::ReactReactInJsxScope(rule) => rule.should_run(ctx),
|
||||
Self::ReactRequireRenderReturn(rule) => rule.should_run(ctx),
|
||||
Self::ReactRulesOfHooks(rule) => rule.should_run(ctx),
|
||||
@@ -21498,6 +21520,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(_) => ReactOnlyExportComponents::IS_TSGOLINT_RULE,
|
||||
Self::ReactPreferEs6Class(_) => ReactPreferEs6Class::IS_TSGOLINT_RULE,
|
||||
Self::ReactPreferFunctionComponent(_) => ReactPreferFunctionComponent::IS_TSGOLINT_RULE,
|
||||
Self::ReactReactCompiler(_) => ReactReactCompiler::IS_TSGOLINT_RULE,
|
||||
Self::ReactReactInJsxScope(_) => ReactReactInJsxScope::IS_TSGOLINT_RULE,
|
||||
Self::ReactRequireRenderReturn(_) => ReactRequireRenderReturn::IS_TSGOLINT_RULE,
|
||||
Self::ReactRulesOfHooks(_) => ReactRulesOfHooks::IS_TSGOLINT_RULE,
|
||||
@@ -22625,6 +22648,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(_) => ReactOnlyExportComponents::VERSION,
|
||||
Self::ReactPreferEs6Class(_) => ReactPreferEs6Class::VERSION,
|
||||
Self::ReactPreferFunctionComponent(_) => ReactPreferFunctionComponent::VERSION,
|
||||
Self::ReactReactCompiler(_) => ReactReactCompiler::VERSION,
|
||||
Self::ReactReactInJsxScope(_) => ReactReactInJsxScope::VERSION,
|
||||
Self::ReactRequireRenderReturn(_) => ReactRequireRenderReturn::VERSION,
|
||||
Self::ReactRulesOfHooks(_) => ReactRulesOfHooks::VERSION,
|
||||
@@ -23649,6 +23673,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(_) => ReactOnlyExportComponents::HAS_CONFIG,
|
||||
Self::ReactPreferEs6Class(_) => ReactPreferEs6Class::HAS_CONFIG,
|
||||
Self::ReactPreferFunctionComponent(_) => ReactPreferFunctionComponent::HAS_CONFIG,
|
||||
Self::ReactReactCompiler(_) => ReactReactCompiler::HAS_CONFIG,
|
||||
Self::ReactReactInJsxScope(_) => ReactReactInJsxScope::HAS_CONFIG,
|
||||
Self::ReactRequireRenderReturn(_) => ReactRequireRenderReturn::HAS_CONFIG,
|
||||
Self::ReactRulesOfHooks(_) => ReactRulesOfHooks::HAS_CONFIG,
|
||||
@@ -24654,6 +24679,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(_) => ReactOnlyExportComponents::INFO,
|
||||
Self::ReactPreferEs6Class(_) => ReactPreferEs6Class::INFO,
|
||||
Self::ReactPreferFunctionComponent(_) => ReactPreferFunctionComponent::INFO,
|
||||
Self::ReactReactCompiler(_) => ReactReactCompiler::INFO,
|
||||
Self::ReactReactInJsxScope(_) => ReactReactInJsxScope::INFO,
|
||||
Self::ReactRequireRenderReturn(_) => ReactRequireRenderReturn::INFO,
|
||||
Self::ReactRulesOfHooks(_) => ReactRulesOfHooks::INFO,
|
||||
@@ -25538,6 +25564,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(rule) => rule.types_info(),
|
||||
Self::ReactPreferEs6Class(rule) => rule.types_info(),
|
||||
Self::ReactPreferFunctionComponent(rule) => rule.types_info(),
|
||||
Self::ReactReactCompiler(rule) => rule.types_info(),
|
||||
Self::ReactReactInJsxScope(rule) => rule.types_info(),
|
||||
Self::ReactRequireRenderReturn(rule) => rule.types_info(),
|
||||
Self::ReactRulesOfHooks(rule) => rule.types_info(),
|
||||
@@ -26373,6 +26400,7 @@ impl RuleEnum {
|
||||
Self::ReactOnlyExportComponents(rule) => rule.run_info(),
|
||||
Self::ReactPreferEs6Class(rule) => rule.run_info(),
|
||||
Self::ReactPreferFunctionComponent(rule) => rule.run_info(),
|
||||
Self::ReactReactCompiler(rule) => rule.run_info(),
|
||||
Self::ReactReactInJsxScope(rule) => rule.run_info(),
|
||||
Self::ReactRequireRenderReturn(rule) => rule.run_info(),
|
||||
Self::ReactRulesOfHooks(rule) => rule.run_info(),
|
||||
@@ -27300,6 +27328,7 @@ pub static RULES: std::sync::LazyLock<Vec<RuleEnum>> = std::sync::LazyLock::new(
|
||||
RuleEnum::ReactOnlyExportComponents(ReactOnlyExportComponents::default()),
|
||||
RuleEnum::ReactPreferEs6Class(ReactPreferEs6Class::default()),
|
||||
RuleEnum::ReactPreferFunctionComponent(ReactPreferFunctionComponent::default()),
|
||||
RuleEnum::ReactReactCompiler(ReactReactCompiler::default()),
|
||||
RuleEnum::ReactReactInJsxScope(ReactReactInJsxScope::default()),
|
||||
RuleEnum::ReactRequireRenderReturn(ReactRequireRenderReturn::default()),
|
||||
RuleEnum::ReactRulesOfHooks(ReactRulesOfHooks::default()),
|
||||
|
||||
@@ -463,6 +463,7 @@ pub(crate) mod react {
|
||||
pub mod only_export_components;
|
||||
pub mod prefer_es6_class;
|
||||
pub mod prefer_function_component;
|
||||
pub mod react_compiler;
|
||||
pub mod react_in_jsx_scope;
|
||||
pub mod require_render_return;
|
||||
pub mod rules_of_hooks;
|
||||
|
||||
@@ -0,0 +1,527 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use oxc_diagnostics::Severity;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_react_compiler::{EnvironmentConfig, PluginOptions, default_plugin_options};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
context::LintContext,
|
||||
rule::{DefaultRuleConfig, Rule},
|
||||
};
|
||||
|
||||
/// The compiler options `eslint-plugin-react-compiler` lints with — `lint`
|
||||
/// output mode plus validations that are off by default in the compiler.
|
||||
/// Mirrors `COMPILER_OPTIONS` in the plugin's `src/shared/RunReactCompiler.ts`.
|
||||
fn react_compiler_options(filename: Option<String>) -> PluginOptions {
|
||||
PluginOptions {
|
||||
filename,
|
||||
output_mode: Some("lint".to_string()),
|
||||
// Don't emit errors on Flow suppressions — Flow already gave a signal.
|
||||
// Suppressed lines are filtered in `run_once` instead (like the eslint
|
||||
// plugin), so other diagnostics in the same function still surface.
|
||||
flow_suppressions: false,
|
||||
environment: EnvironmentConfig {
|
||||
validate_ref_access_during_render: true,
|
||||
validate_no_set_state_in_render: true,
|
||||
validate_no_set_state_in_effects: true,
|
||||
validate_no_jsx_in_try_statements: true,
|
||||
validate_no_impure_functions_in_render: true,
|
||||
validate_static_components: true,
|
||||
validate_no_freezing_known_mutable_functions: true,
|
||||
validate_no_void_use_memo: true,
|
||||
validate_no_capitalized_calls: Some(vec![]),
|
||||
validate_hooks_usage: true,
|
||||
validate_no_derived_computations_in_effects: true,
|
||||
..EnvironmentConfig::default()
|
||||
},
|
||||
..default_plugin_options()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Deserialize)]
|
||||
pub struct ReactCompiler(Box<ReactCompilerConfig>);
|
||||
|
||||
impl std::ops::Deref for ReactCompiler {
|
||||
type Target = ReactCompilerConfig;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
|
||||
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ReactCompilerConfig {
|
||||
/// Also report compiler bail-outs — places where React Compiler skipped a
|
||||
/// component or hook (for example because of unsupported syntax) without
|
||||
/// finding a rule violation. These do not indicate incorrect code, only
|
||||
/// code that the compiler declined to optimize.
|
||||
report_all_bailouts: bool,
|
||||
}
|
||||
|
||||
declare_oxc_lint!(
|
||||
/// ### What it does
|
||||
///
|
||||
/// Runs the React Compiler's analysis in lint-only mode and reports code
|
||||
/// that violates the Rules of React — for example calling hooks
|
||||
/// conditionally, calling setState during render, accessing refs during
|
||||
/// render, or mutating props and state.
|
||||
///
|
||||
/// This rule surfaces the same diagnostics as `eslint-plugin-react-compiler`.
|
||||
///
|
||||
/// ::: warning
|
||||
/// This rule is experimental, and will change to fit in better with Oxlint.
|
||||
/// :::
|
||||
///
|
||||
/// ### Why is this bad?
|
||||
///
|
||||
/// Code that breaks the Rules of React can behave unpredictably at runtime
|
||||
/// (stale UI, infinite re-render loops, lost state) and prevents React
|
||||
/// Compiler from optimizing the component. Following these rules keeps
|
||||
/// components correct and allows them to be automatically memoized.
|
||||
///
|
||||
/// ### Examples
|
||||
///
|
||||
/// Examples of **incorrect** code for this rule:
|
||||
/// ```jsx
|
||||
/// function Component(props) {
|
||||
/// if (props.cond) {
|
||||
/// useState(0); // hooks may not be called conditionally
|
||||
/// }
|
||||
/// return <div>{props.text}</div>;
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ```jsx
|
||||
/// function Component() {
|
||||
/// const ref = useRef(null);
|
||||
/// return <div>{ref.current}</div>; // refs may not be read during render
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Examples of **correct** code for this rule:
|
||||
/// ```jsx
|
||||
/// function Component(props) {
|
||||
/// const [state, setState] = useState(0);
|
||||
/// return <button onClick={() => setState(state + 1)}>{props.text}</button>;
|
||||
/// }
|
||||
/// ```
|
||||
ReactCompiler,
|
||||
react,
|
||||
nursery,
|
||||
config = ReactCompilerConfig,
|
||||
version = "1.70.0",
|
||||
);
|
||||
|
||||
impl Rule for ReactCompiler {
|
||||
fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
|
||||
serde_json::from_value::<DefaultRuleConfig<Self>>(value).map(DefaultRuleConfig::into_inner)
|
||||
}
|
||||
|
||||
fn run_once(&self, ctx: &LintContext) {
|
||||
let program = ctx.nodes().program();
|
||||
let options = react_compiler_options(ctx.file_path().to_str().map(ToString::to_string));
|
||||
|
||||
let result = oxc_react_compiler::lint(program, options);
|
||||
|
||||
let diagnostics = result.diagnostics.into_vec();
|
||||
let diagnostics = if self.report_all_bailouts {
|
||||
diagnostics
|
||||
} else {
|
||||
diagnostics
|
||||
.into_iter()
|
||||
.filter(|diagnostic| diagnostic.severity == Severity::Error)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let suppressed_lines = flow_suppression_lines(ctx);
|
||||
for mut diagnostic in diagnostics {
|
||||
// If Flow already caught this error, we don't need to report it again.
|
||||
if is_flow_suppressed(&diagnostic, &suppressed_lines, ctx.source_text()) {
|
||||
continue;
|
||||
}
|
||||
// The rule code already identifies the source, so the prefix is noise here.
|
||||
if let Some(message) = diagnostic.message.strip_prefix("[ReactCompiler] ") {
|
||||
diagnostic.message = Cow::Owned(message.to_string());
|
||||
}
|
||||
ctx.diagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Flow suppression codes that silence a React Compiler diagnostic on the next
|
||||
/// line, matching the eslint plugin's `hasFlowSuppression` check.
|
||||
const FLOW_SUPPRESSIONS: [&str; 2] =
|
||||
["$FlowFixMe[react-rule-hook]", "$FlowFixMe[react-rule-unsafe-ref]"];
|
||||
|
||||
/// Lines (0-based) on which a recognized Flow suppression comment ends.
|
||||
fn flow_suppression_lines(ctx: &LintContext) -> Vec<u32> {
|
||||
let source_text = ctx.source_text();
|
||||
ctx.semantic()
|
||||
.comments()
|
||||
.iter()
|
||||
.filter(|comment| {
|
||||
let text = comment.span.source_text(source_text);
|
||||
FLOW_SUPPRESSIONS.iter().any(|suppression| text.contains(suppression))
|
||||
})
|
||||
.map(|comment| line_number(source_text, comment.span.end))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A diagnostic is suppressed when a Flow suppression comment sits on the line
|
||||
/// directly above its primary location.
|
||||
fn is_flow_suppressed(
|
||||
diagnostic: &oxc_diagnostics::OxcDiagnostic,
|
||||
suppressed_lines: &[u32],
|
||||
source_text: &str,
|
||||
) -> bool {
|
||||
if suppressed_lines.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let Some(label) = diagnostic.labels.first() else {
|
||||
return false;
|
||||
};
|
||||
let line = line_number(source_text, label.offset());
|
||||
line.checked_sub(1).is_some_and(|previous| suppressed_lines.contains(&previous))
|
||||
}
|
||||
|
||||
fn line_number(source_text: &str, offset: u32) -> u32 {
|
||||
let end = (offset as usize).min(source_text.len());
|
||||
u32::try_from(memchr::memchr_iter(b'\n', &source_text.as_bytes()[..end]).count())
|
||||
.unwrap_or(u32::MAX)
|
||||
}
|
||||
|
||||
// Test cases ported from `eslint-plugin-react-compiler` (MIT licensed):
|
||||
// <https://github.com/facebook/react/tree/main/compiler/packages/eslint-plugin-react-compiler/__tests__>
|
||||
// `RustBackendComparison-test.ts` only replicates the cases below to compare the
|
||||
// TS and Rust backends, so it adds no new cases.
|
||||
//
|
||||
// The one case that cannot be ported is "Basic example with component syntax"
|
||||
// (PluginTest-test.ts), which uses Flow's experimental `component` declaration
|
||||
// syntax that oxc does not parse.
|
||||
#[test]
|
||||
fn test() {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
// ---- PluginTest-test.ts ----
|
||||
// [Invariant] Defined after use
|
||||
// (OK because invariants are only meant for the compiler team's consumption)
|
||||
(
|
||||
"
|
||||
function Component(props) {
|
||||
let y = function () {
|
||||
m(x);
|
||||
};
|
||||
|
||||
let x = { a };
|
||||
m(x);
|
||||
return y;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// Classes don't throw
|
||||
(
|
||||
"
|
||||
class Foo {
|
||||
#bar() {}
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// ---- InvalidHooksRule-test.ts ----
|
||||
// Basic example
|
||||
(
|
||||
"
|
||||
function Component() {
|
||||
useHook();
|
||||
return <div>Hello world</div>;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// Violation with Flow suppression
|
||||
(
|
||||
"
|
||||
// Valid since error already suppressed with flow.
|
||||
function useHook() {
|
||||
if (cond) {
|
||||
// $FlowFixMe[react-rule-hook]
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// ---- ReactCompilerRuleTypescript-test.ts ----
|
||||
// Basic example
|
||||
(
|
||||
"
|
||||
function Button(props) {
|
||||
return null;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// Repro for hooks as normal values
|
||||
(
|
||||
"
|
||||
function Button(props) {
|
||||
const scrollview = React.useRef<ScrollView>(null);
|
||||
return <Button thing={scrollview} />;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// ---- RustBackend-test.ts ----
|
||||
// Basic component compiles without errors
|
||||
(
|
||||
"
|
||||
function Component(props) {
|
||||
return <div>{props.text}</div>;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// Component with hooks compiles without errors
|
||||
(
|
||||
"
|
||||
import {useState} from 'react';
|
||||
function Component(props) {
|
||||
const [state, setState] = useState(0);
|
||||
return <div onClick={() => setState(state + 1)}>{state}</div>;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// ---- oxlint-specific ----
|
||||
// A bail-out (local named `fbt`) is not a violation, only a skipped
|
||||
// optimization; it is not reported by default.
|
||||
(
|
||||
"function Component() {
|
||||
const fbt = 'span';
|
||||
return <fbt desc='label'>Hello</fbt>;
|
||||
}",
|
||||
None,
|
||||
),
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// ---- PluginTest-test.ts ----
|
||||
// Multiple diagnostic kinds from the same function are surfaced
|
||||
(
|
||||
"
|
||||
import Child from './Child';
|
||||
function Component() {
|
||||
const result = cond ?? useConditionalHook();
|
||||
return <>
|
||||
{Child(result)}
|
||||
</>;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// Multiple diagnostics within the same file are surfaced
|
||||
(
|
||||
"
|
||||
function useConditional1() {
|
||||
'use memo';
|
||||
return cond ?? useConditionalHook();
|
||||
}
|
||||
function useConditional2(props) {
|
||||
'use memo';
|
||||
return props.cond && useConditionalHook();
|
||||
}",
|
||||
None,
|
||||
),
|
||||
// 'use no forget' does not disable eslint rule
|
||||
(
|
||||
"
|
||||
let count = 0;
|
||||
function Component() {
|
||||
'use no forget';
|
||||
return cond ?? useConditionalHook();
|
||||
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// Multiple non-fatal useMemo diagnostics are surfaced
|
||||
(
|
||||
"
|
||||
import {useMemo, useState} from 'react';
|
||||
|
||||
function Component({item, cond}) {
|
||||
const [prevItem, setPrevItem] = useState(item);
|
||||
const [state, setState] = useState(0);
|
||||
|
||||
useMemo(() => {
|
||||
if (cond) {
|
||||
setPrevItem(item);
|
||||
setState(0);
|
||||
}
|
||||
}, [cond, item, init]);
|
||||
|
||||
return <Child x={state} />;
|
||||
}",
|
||||
None,
|
||||
),
|
||||
// ---- InvalidHooksRule-test.ts ----
|
||||
// Simple violation
|
||||
(
|
||||
"
|
||||
function useConditional() {
|
||||
if (cond) {
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// Multiple diagnostics within the same function are surfaced
|
||||
(
|
||||
"
|
||||
function useConditional() {
|
||||
cond ?? useConditionalHook();
|
||||
props.cond && useConditionalHook();
|
||||
return <div>Hello world</div>;
|
||||
}",
|
||||
None,
|
||||
),
|
||||
// ---- ImpureFunctionCallsRule-test.ts ----
|
||||
// Known impure function calls are caught
|
||||
(
|
||||
"
|
||||
function Component() {
|
||||
const date = Date.now();
|
||||
const now = performance.now();
|
||||
const rand = Math.random();
|
||||
return <Foo date={date} now={now} rand={rand} />;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// ---- NoAmbiguousJsxRule-test.ts ----
|
||||
// JSX in try blocks are warned against
|
||||
(
|
||||
"
|
||||
function Component(props) {
|
||||
let el;
|
||||
try {
|
||||
el = <Child />;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return el;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// ---- NoCapitalizedCallsRule-test.ts ----
|
||||
// Simple violation
|
||||
(
|
||||
"
|
||||
import Child from './Child';
|
||||
function Component() {
|
||||
return <>
|
||||
{Child()}
|
||||
</>;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// Method call violation
|
||||
(
|
||||
"
|
||||
import myModule from './MyModule';
|
||||
function Component() {
|
||||
return <>
|
||||
{myModule.Child()}
|
||||
</>;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// Multiple diagnostics within the same function are surfaced
|
||||
(
|
||||
"
|
||||
import Child1 from './Child1';
|
||||
import MyModule from './MyModule';
|
||||
function Component() {
|
||||
return <>
|
||||
{Child1()}
|
||||
{MyModule.Child2()}
|
||||
</>;
|
||||
}",
|
||||
None,
|
||||
),
|
||||
// ---- NoRefAccessInRender-tests.ts ----
|
||||
// validate against simple ref access in render
|
||||
(
|
||||
"
|
||||
function Component(props) {
|
||||
const ref = useRef(null);
|
||||
const value = ref.current;
|
||||
return value;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// ---- ReactCompilerRuleTypescript-test.ts ----
|
||||
// Mutating useState value
|
||||
(
|
||||
"
|
||||
import { useState } from 'react';
|
||||
function Component(props) {
|
||||
// typescript syntax that hermes-parser doesn't understand yet
|
||||
const x: `foo${1}` = 'foo1';
|
||||
const [state, setState] = useState({a: 0});
|
||||
state.a = 1;
|
||||
return <div>{props.foo}</div>;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// ---- RustBackend-test.ts ----
|
||||
// Conditional hook call detected by Rust backend
|
||||
(
|
||||
"
|
||||
function Component() {
|
||||
const result = cond ?? useConditionalHook();
|
||||
return <div>{result}</div>;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// Multiple diagnostics detected by Rust backend
|
||||
(
|
||||
"
|
||||
function useConditional1() {
|
||||
'use memo';
|
||||
return cond ?? useConditionalHook();
|
||||
}
|
||||
function useConditional2(props) {
|
||||
'use memo';
|
||||
return props.cond && useConditionalHook();
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
// ---- oxlint-specific ----
|
||||
// Bail-outs are reported when `reportAllBailouts` is enabled.
|
||||
(
|
||||
"function Component() {
|
||||
const fbt = 'span';
|
||||
return <fbt desc='label'>Hello</fbt>;
|
||||
}",
|
||||
Some(json!([{ "reportAllBailouts": true }])),
|
||||
),
|
||||
];
|
||||
|
||||
Tester::new(ReactCompiler::NAME, ReactCompiler::PLUGIN, pass, fail).test_and_snapshot();
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
---
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ react(react-compiler): Hooks: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
╭─[react_compiler.tsx:4:26]
|
||||
3 │ function Component() {
|
||||
4 │ const result = cond ?? useConditionalHook();
|
||||
· ──────────────────
|
||||
5 │ return <>
|
||||
╰────
|
||||
|
||||
⚠ react(react-compiler): CapitalizedCalls: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
|
||||
╭─[react_compiler.tsx:6:6]
|
||||
5 │ return <>
|
||||
6 │ {Child(result)}
|
||||
· ─────────────
|
||||
7 │ </>;
|
||||
╰────
|
||||
help: Child may be a component
|
||||
|
||||
⚠ react(react-compiler): Hooks: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
╭─[react_compiler.tsx:4:18]
|
||||
3 │ 'use memo';
|
||||
4 │ return cond ?? useConditionalHook();
|
||||
· ──────────────────
|
||||
5 │ }
|
||||
╰────
|
||||
|
||||
⚠ react(react-compiler): Hooks: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
╭─[react_compiler.tsx:8:24]
|
||||
7 │ 'use memo';
|
||||
8 │ return props.cond && useConditionalHook();
|
||||
· ──────────────────
|
||||
9 │ }
|
||||
╰────
|
||||
|
||||
⚠ react(react-compiler): Hooks: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
╭─[react_compiler.tsx:5:18]
|
||||
4 │ 'use no forget';
|
||||
5 │ return cond ?? useConditionalHook();
|
||||
· ──────────────────
|
||||
6 │
|
||||
╰────
|
||||
|
||||
⚠ react(react-compiler): VoidUseMemo: useMemo() callbacks must return a value
|
||||
╭─[react_compiler.tsx:8:11]
|
||||
7 │
|
||||
8 │ ╭─▶ useMemo(() => {
|
||||
9 │ │ if (cond) {
|
||||
10 │ │ setPrevItem(item);
|
||||
11 │ │ setState(0);
|
||||
12 │ │ }
|
||||
13 │ ├─▶ }, [cond, item, init]);
|
||||
· ╰──── useMemo() callbacks must return a value
|
||||
14 │
|
||||
╰────
|
||||
help: This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects
|
||||
|
||||
⚠ react(react-compiler): RenderSetState: Calling setState from useMemo may trigger an infinite loop
|
||||
╭─[react_compiler.tsx:10:7]
|
||||
9 │ if (cond) {
|
||||
10 │ setPrevItem(item);
|
||||
· ─────┬─────
|
||||
· ╰── Found setState() within useMemo()
|
||||
11 │ setState(0);
|
||||
╰────
|
||||
help: Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)
|
||||
|
||||
⚠ react(react-compiler): RenderSetState: Calling setState from useMemo may trigger an infinite loop
|
||||
╭─[react_compiler.tsx:11:7]
|
||||
10 │ setPrevItem(item);
|
||||
11 │ setState(0);
|
||||
· ────┬───
|
||||
· ╰── Found setState() within useMemo()
|
||||
12 │ }
|
||||
╰────
|
||||
help: Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)
|
||||
|
||||
⚠ react(react-compiler): MemoDependencies: Found extra memoization dependencies
|
||||
╭─[react_compiler.tsx:13:19]
|
||||
12 │ }
|
||||
13 │ }, [cond, item, init]);
|
||||
· ──┬─
|
||||
· ╰── Unnecessary dependency `init`. Values declared outside of a component/hook should not be listed as dependencies as the component will not re-render if they change
|
||||
14 │
|
||||
╰────
|
||||
help: Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often
|
||||
|
||||
⚠ react(react-compiler): Hooks: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
╭─[react_compiler.tsx:4:5]
|
||||
3 │ if (cond) {
|
||||
4 │ useConditionalHook();
|
||||
· ──────────────────
|
||||
5 │ }
|
||||
╰────
|
||||
|
||||
⚠ react(react-compiler): Hooks: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
╭─[react_compiler.tsx:3:11]
|
||||
2 │ function useConditional() {
|
||||
3 │ cond ?? useConditionalHook();
|
||||
· ──────────────────
|
||||
4 │ props.cond && useConditionalHook();
|
||||
╰────
|
||||
|
||||
⚠ react(react-compiler): Hooks: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
╭─[react_compiler.tsx:4:17]
|
||||
3 │ cond ?? useConditionalHook();
|
||||
4 │ props.cond && useConditionalHook();
|
||||
· ──────────────────
|
||||
5 │ return <div>Hello world</div>;
|
||||
╰────
|
||||
|
||||
⚠ react(react-compiler): Purity: Cannot call impure function during render
|
||||
╭─[react_compiler.tsx:3:16]
|
||||
2 │ function Component() {
|
||||
3 │ const date = Date.now();
|
||||
· ─────┬────
|
||||
· ╰── Cannot call impure function
|
||||
4 │ const now = performance.now();
|
||||
╰────
|
||||
help: `Date.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)
|
||||
|
||||
⚠ react(react-compiler): Purity: Cannot call impure function during render
|
||||
╭─[react_compiler.tsx:4:15]
|
||||
3 │ const date = Date.now();
|
||||
4 │ const now = performance.now();
|
||||
· ────────┬────────
|
||||
· ╰── Cannot call impure function
|
||||
5 │ const rand = Math.random();
|
||||
╰────
|
||||
help: `performance.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)
|
||||
|
||||
⚠ react(react-compiler): Purity: Cannot call impure function during render
|
||||
╭─[react_compiler.tsx:5:16]
|
||||
4 │ const now = performance.now();
|
||||
5 │ const rand = Math.random();
|
||||
· ──────┬──────
|
||||
· ╰── Cannot call impure function
|
||||
6 │ return <Foo date={date} now={now} rand={rand} />;
|
||||
╰────
|
||||
help: `Math.random` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)
|
||||
|
||||
⚠ react(react-compiler): ErrorBoundaries: Avoid constructing JSX within try/catch
|
||||
╭─[react_compiler.tsx:5:10]
|
||||
4 │ try {
|
||||
5 │ el = <Child />;
|
||||
· ────┬────
|
||||
· ╰── Avoid constructing JSX within try/catch
|
||||
6 │ } catch {
|
||||
╰────
|
||||
help: React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)
|
||||
|
||||
⚠ react(react-compiler): CapitalizedCalls: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
|
||||
╭─[react_compiler.tsx:5:6]
|
||||
4 │ return <>
|
||||
5 │ {Child()}
|
||||
· ───────
|
||||
6 │ </>;
|
||||
╰────
|
||||
help: Child may be a component
|
||||
|
||||
⚠ react(react-compiler): CapitalizedCalls: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
|
||||
╭─[react_compiler.tsx:5:6]
|
||||
4 │ return <>
|
||||
5 │ {myModule.Child()}
|
||||
· ────────────────
|
||||
6 │ </>;
|
||||
╰────
|
||||
help: Child may be a component
|
||||
|
||||
⚠ react(react-compiler): CapitalizedCalls: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
|
||||
╭─[react_compiler.tsx:6:6]
|
||||
5 │ return <>
|
||||
6 │ {Child1()}
|
||||
· ────────
|
||||
7 │ {MyModule.Child2()}
|
||||
╰────
|
||||
help: Child1 may be a component
|
||||
|
||||
⚠ react(react-compiler): CapitalizedCalls: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
|
||||
╭─[react_compiler.tsx:7:6]
|
||||
6 │ {Child1()}
|
||||
7 │ {MyModule.Child2()}
|
||||
· ─────────────────
|
||||
8 │ </>;
|
||||
╰────
|
||||
help: Child2 may be a component
|
||||
|
||||
⚠ react(react-compiler): Refs: Cannot access refs during render
|
||||
╭─[react_compiler.tsx:4:17]
|
||||
3 │ const ref = useRef(null);
|
||||
4 │ const value = ref.current;
|
||||
· ─────┬─────
|
||||
· ╰── Cannot access ref value during render
|
||||
5 │ return value;
|
||||
╰────
|
||||
help: React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
|
||||
|
||||
⚠ react(react-compiler): Immutability: This value cannot be modified
|
||||
╭─[react_compiler.tsx:7:11]
|
||||
6 │ const [state, setState] = useState({a: 0});
|
||||
7 │ state.a = 1;
|
||||
· ──┬──
|
||||
· ╰── value cannot be modified
|
||||
8 │ return <div>{props.foo}</div>;
|
||||
╰────
|
||||
help: Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead
|
||||
|
||||
⚠ react(react-compiler): Hooks: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
╭─[react_compiler.tsx:3:26]
|
||||
2 │ function Component() {
|
||||
3 │ const result = cond ?? useConditionalHook();
|
||||
· ──────────────────
|
||||
4 │ return <div>{result}</div>;
|
||||
╰────
|
||||
|
||||
⚠ react(react-compiler): Hooks: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
╭─[react_compiler.tsx:4:18]
|
||||
3 │ 'use memo';
|
||||
4 │ return cond ?? useConditionalHook();
|
||||
· ──────────────────
|
||||
5 │ }
|
||||
╰────
|
||||
|
||||
⚠ react(react-compiler): Hooks: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
|
||||
╭─[react_compiler.tsx:8:24]
|
||||
7 │ 'use memo';
|
||||
8 │ return props.cond && useConditionalHook();
|
||||
· ──────────────────
|
||||
9 │ }
|
||||
╰────
|
||||
|
||||
⚠ react(react-compiler): Todo: Support local variables named `fbt`
|
||||
╭─[react_compiler.tsx:2:23]
|
||||
1 │ function Component() {
|
||||
2 │ const fbt = 'span';
|
||||
· ───
|
||||
3 │ return <fbt desc='label'>Hello</fbt>;
|
||||
╰────
|
||||
help: Local variables named `fbt` may conflict with the fbt plugin and are not yet supported
|
||||
@@ -3,9 +3,11 @@
|
||||
// This source code is licensed under the MIT license found in the
|
||||
// LICENSE file in the root directory of this source tree.
|
||||
|
||||
use oxc_diagnostics::{Diagnostics, OxcDiagnostic};
|
||||
use oxc_diagnostics::{Diagnostics, LabeledSpan, OxcDiagnostic};
|
||||
use oxc_span::Span;
|
||||
|
||||
use react_compiler::entrypoint::compile_result::{
|
||||
CompileResult, CompilerErrorDetailInfo, LoggerEvent,
|
||||
CompileResult, CompilerErrorDetailInfo, CompilerErrorInfo, LoggerEvent, LoggerSourceLocation,
|
||||
};
|
||||
|
||||
/// Convert a `CompileResult` into OXC diagnostics. Each diagnostic carries its
|
||||
@@ -34,9 +36,42 @@ pub fn compile_result_to_diagnostics(result: &CompileResult) -> Diagnostics {
|
||||
diagnostics
|
||||
}
|
||||
|
||||
fn error_info_to_diagnostic(
|
||||
error: &react_compiler::entrypoint::compile_result::CompilerErrorInfo,
|
||||
) -> OxcDiagnostic {
|
||||
/// Byte-offset span for a compiler source location. Locations that flow through
|
||||
/// the oxc frontend carry span offsets in `Position::index`; synthetic locations
|
||||
/// without offsets yield `None`.
|
||||
fn loc_to_span(loc: &LoggerSourceLocation) -> Option<Span> {
|
||||
Some(Span::new(loc.start.index?, loc.end.index?))
|
||||
}
|
||||
|
||||
/// Labels for an error detail: its own `loc` when present (`ErrorDetail`-style),
|
||||
/// otherwise the locations of its sub-details (`Diagnostic`-style), otherwise
|
||||
/// the enclosing function.
|
||||
fn detail_labels(
|
||||
detail: &CompilerErrorDetailInfo,
|
||||
fn_loc: Option<&LoggerSourceLocation>,
|
||||
) -> Vec<LabeledSpan> {
|
||||
if let Some(span) = detail.loc.as_ref().and_then(loc_to_span) {
|
||||
return vec![LabeledSpan::underline(span)];
|
||||
}
|
||||
if let Some(items) = &detail.details {
|
||||
let labels: Vec<LabeledSpan> = items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let span = item.loc.as_ref().and_then(loc_to_span)?;
|
||||
Some(match &item.message {
|
||||
Some(message) => span.label(message.clone()),
|
||||
None => LabeledSpan::underline(span),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if !labels.is_empty() {
|
||||
return labels;
|
||||
}
|
||||
}
|
||||
fn_loc.and_then(loc_to_span).map(|span| vec![LabeledSpan::underline(span)]).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn error_info_to_diagnostic(error: &CompilerErrorInfo) -> OxcDiagnostic {
|
||||
let message = format!("[ReactCompiler] {}", error.reason);
|
||||
let mut diag = OxcDiagnostic::error(message);
|
||||
|
||||
@@ -44,38 +79,67 @@ fn error_info_to_diagnostic(
|
||||
diag = diag.with_help(description.clone());
|
||||
}
|
||||
|
||||
let labels: Vec<LabeledSpan> =
|
||||
error.details.iter().flat_map(|detail| detail_labels(detail, None)).collect();
|
||||
if !labels.is_empty() {
|
||||
diag = diag.with_labels(labels);
|
||||
}
|
||||
|
||||
diag
|
||||
}
|
||||
|
||||
/// Map a detail to an [`OxcDiagnostic`] at the compiler's own *display* severity
|
||||
/// (`Error`/`Warning`/`Hint`; `Off` is suppressed). Fatality is separate, decided
|
||||
/// by `panicThreshold` ([`CompileResult::Error`]).
|
||||
fn error_detail_to_diagnostic(detail: &CompilerErrorDetailInfo) -> Option<OxcDiagnostic> {
|
||||
let message = if let Some(description) = &detail.description {
|
||||
format!("[ReactCompiler] {}: {}. {}", detail.category, detail.reason, description)
|
||||
} else {
|
||||
format!("[ReactCompiler] {}: {}", detail.category, detail.reason)
|
||||
};
|
||||
fn error_detail_to_diagnostic(
|
||||
detail: &CompilerErrorDetailInfo,
|
||||
fn_loc: Option<&LoggerSourceLocation>,
|
||||
) -> Option<OxcDiagnostic> {
|
||||
let message = format!("[ReactCompiler] {}: {}", detail.category, detail.reason);
|
||||
|
||||
let diagnostic = match detail.severity.as_str() {
|
||||
let mut diagnostic = match detail.severity.as_str() {
|
||||
"Off" => return None,
|
||||
"Error" => OxcDiagnostic::error(message),
|
||||
// `Warning`, `Hint`, and any unknown future value surface as warnings.
|
||||
_ => OxcDiagnostic::warn(message),
|
||||
};
|
||||
|
||||
if let Some(description) = &detail.description {
|
||||
diagnostic = diagnostic.with_help(description.clone());
|
||||
}
|
||||
|
||||
let labels = detail_labels(detail, fn_loc);
|
||||
if !labels.is_empty() {
|
||||
diagnostic = diagnostic.with_labels(labels);
|
||||
}
|
||||
|
||||
Some(diagnostic)
|
||||
}
|
||||
|
||||
fn event_to_diagnostic(event: &LoggerEvent) -> Option<OxcDiagnostic> {
|
||||
match event {
|
||||
LoggerEvent::CompileSuccess { .. } | LoggerEvent::CompileSkip { .. } => None,
|
||||
LoggerEvent::CompileError { detail, .. }
|
||||
| LoggerEvent::CompileErrorWithLoc { detail, .. } => error_detail_to_diagnostic(detail),
|
||||
LoggerEvent::CompileUnexpectedThrow { data, .. } => {
|
||||
Some(OxcDiagnostic::error(format!("[ReactCompiler] Unexpected error: {}", data)))
|
||||
LoggerEvent::CompileError { detail, fn_loc } => {
|
||||
error_detail_to_diagnostic(detail, fn_loc.as_ref())
|
||||
}
|
||||
LoggerEvent::PipelineError { data, .. } => {
|
||||
Some(OxcDiagnostic::error(format!("[ReactCompiler] Pipeline error: {}", data)))
|
||||
LoggerEvent::CompileErrorWithLoc { detail, fn_loc } => {
|
||||
error_detail_to_diagnostic(detail, Some(fn_loc))
|
||||
}
|
||||
LoggerEvent::CompileUnexpectedThrow { data, fn_loc } => {
|
||||
let mut diagnostic =
|
||||
OxcDiagnostic::error(format!("[ReactCompiler] Unexpected error: {}", data));
|
||||
if let Some(span) = fn_loc.as_ref().and_then(loc_to_span) {
|
||||
diagnostic = diagnostic.with_label(LabeledSpan::underline(span));
|
||||
}
|
||||
Some(diagnostic)
|
||||
}
|
||||
LoggerEvent::PipelineError { data, fn_loc } => {
|
||||
let mut diagnostic =
|
||||
OxcDiagnostic::error(format!("[ReactCompiler] Pipeline error: {}", data));
|
||||
if let Some(span) = fn_loc.as_ref().and_then(loc_to_span) {
|
||||
diagnostic = diagnostic.with_label(LabeledSpan::underline(span));
|
||||
}
|
||||
Some(diagnostic)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,14 @@ use convert_scope::convert_scope_info;
|
||||
use diagnostics::compile_result_to_diagnostics;
|
||||
use prefilter::{has_react_like_functions, has_resource_management_declarations};
|
||||
use react_compiler::entrypoint::compile_result::LoggerEvent;
|
||||
use react_compiler_hir::environment_config::EnvironmentConfig;
|
||||
use rustc_hash::FxHashSet;
|
||||
// Re-exported so integrations needn't depend on the upstream `react_compiler` crate.
|
||||
|
||||
// Re-exported so integrations needn't depend on the upstream `react_compiler` crates.
|
||||
pub use react_compiler::entrypoint::plugin_options::{
|
||||
CompilerTarget, DynamicGatingConfig, GatingConfig, PluginOptions,
|
||||
};
|
||||
pub use react_compiler_hir::environment_config::EnvironmentConfig;
|
||||
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
/// [`PluginOptions`] with the compiler's standard defaults (it has no `Default`).
|
||||
/// Override fields with struct-update syntax: `PluginOptions { ..default_plugin_options() }`.
|
||||
|
||||
@@ -6716,6 +6716,26 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"react/react-compiler": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RuleNoConfig"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"$ref": "#/definitions/AllowWarnDeny"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/ReactCompilerConfig"
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"react/react-in-jsx-scope": {
|
||||
"$ref": "#/definitions/RuleNoConfig"
|
||||
},
|
||||
@@ -16291,6 +16311,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ReactCompilerConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reportAllBailouts": {
|
||||
"description": "Also report compiler bail-outs — places where React Compiler skipped a\ncomponent or hook (for example because of unsupported syntax) without\nfinding a rule violation. These do not indicate incorrect code, only\ncode that the compiler declined to optimize.",
|
||||
"default": false,
|
||||
"type": "boolean",
|
||||
"markdownDescription": "Also report compiler bail-outs — places where React Compiler skipped a\ncomponent or hook (for example because of unsupported syntax) without\nfinding a rule violation. These do not indicate incorrect code, only\ncode that the compiler declined to optimize."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ReactPerfConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Reference in new issue
Block a user