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:
BoshenandCameron Clark authored and GitHub committed 2026-06-15 08:53:18 +01:00
1 parent d6d1189b58
commit 1490a0a069
11 files changed
+935 -22

No files matched your search

Generated
+1
View File
@@ -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.
+1
View File
@@ -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"] }
+5
View File
@@ -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
View File
@@ -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()),
+1
View File
@@ -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
+82 -18
View File
@@ -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)
}
}
}
+5 -3
View File
@@ -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() }`.
+32
View File
@@ -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": {