Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions crates/oxc_linter/src/config/settings/react.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@ pub struct ReactPluginSettings {
#[serde(default)]
#[serde(rename = "linkComponents")]
link_components: Vec<CustomComponent>,

/// React version to use for version-specific rules.
///
/// Example:
///
/// ```jsonc
/// {
/// "settings": {
/// "react": {
/// "version": "18.2.0"
/// }
/// }
/// }
/// ```
#[serde(default)]
pub version: Option<CompactStr>,
// TODO: More properties should be added
}

Expand Down
5 changes: 5 additions & 0 deletions crates/oxc_linter/src/generated/rule_runner_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2368,6 +2368,11 @@ impl RuleRunner for crate::rules::react::no_unknown_property::NoUnknownProperty
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
}

impl RuleRunner for crate::rules::react::no_unsafe::NoUnsafe {
const NODE_TYPES: Option<&AstTypesBitset> = None;
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
}

impl RuleRunner for crate::rules::react::only_export_components::OnlyExportComponents {
const NODE_TYPES: Option<&AstTypesBitset> = None;
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::RunOnce;
Expand Down
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ pub(crate) mod react {
pub mod no_string_refs;
pub mod no_unescaped_entities;
pub mod no_unknown_property;
pub mod no_unsafe;
pub mod only_export_components;
pub mod prefer_es6_class;
pub mod react_in_jsx_scope;
Expand Down Expand Up @@ -1055,6 +1056,7 @@ oxc_macros::declare_all_lint_rules! {
react::no_string_refs,
react::no_unescaped_entities,
react::no_unknown_property,
react::no_unsafe,
react::only_export_components,
react::prefer_es6_class,
react::react_in_jsx_scope,
Expand Down
329 changes: 329 additions & 0 deletions crates/oxc_linter/src/rules/react/no_unsafe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
use oxc_ast::AstKind;
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::{
AstNode,
context::{ContextHost, LintContext},
rule::{DefaultRuleConfig, Rule},
utils::{get_parent_component, is_es5_component},
};

fn no_unsafe_diagnostic(method_name: &str, span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn(format!("Unsafe lifecycle method `{method_name}` is not allowed"))
.with_help(format!(
"`{method_name}` is deprecated and may be removed in future React versions. Consider using alternative lifecycle methods or hooks."
))
.with_label(span)
Comment on lines +16 to +20
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original version of this rule includes specific method recommendations, we should try including those instead of just saying "alternative lifecycle methods or hooks":

https://github.com/jsx-eslint/eslint-plugin-react/blob/f2869fd6dc76ceb863c5e2aeea8bf4d392508775/lib/rules/no-unsafe.js#L58-L69

}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(rename_all = "camelCase")]
#[serde(rename_all = "camelCase", default)]
#[derive(Default)]
struct NoUnsafeConfig {
#[serde(default)]
check_aliases: bool,
}

#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub struct NoUnsafe(NoUnsafeConfig);

declare_oxc_lint!(
/// ### What it does
///
/// This rule identifies and restricts the use of unsafe React lifecycle methods.
///
/// ### Why is this bad?
///
/// Certain lifecycle methods (`componentWillMount`, `componentWillReceiveProps`, and `componentWillUpdate`)
/// are considered unsafe and have been deprecated since React 16.9. They are frequently misused and cause
/// problems in async rendering. Using their `UNSAFE_` prefixed versions or the deprecated names themselves
/// should be avoided.
Comment on lines +43 to +45
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states these methods were deprecated since React 16.9, but the implementation checks for React 16.3 (line 139). According to React's history, the UNSAFE_ prefix was introduced in React 16.3, and the old names were deprecated in 16.9 and removed in 17.0. The documentation should clarify that this rule flags UNSAFE_ prefixed methods starting from React 16.3+ (when they were introduced), and optionally flags the non-prefixed versions (aliases) when checkAliases is enabled.

Suggested change
/// are considered unsafe and have been deprecated since React 16.9. They are frequently misused and cause
/// problems in async rendering. Using their `UNSAFE_` prefixed versions or the deprecated names themselves
/// should be avoided.
/// are considered unsafe. The `UNSAFE_`-prefixed versions of these methods were introduced in React 16.3,
/// and this rule flags their usage starting from that version. The original (non-prefixed) versions were
/// deprecated in React 16.9 and removed in React 17.0. By default, only the `UNSAFE_`-prefixed methods are
/// flagged; optionally, the non-prefixed versions (aliases) are also flagged when `checkAliases` is enabled.
/// These methods are frequently misused and cause problems in async rendering. Their usage should be avoided.

Copilot uses AI. Check for mistakes.
///
/// ### Examples
///
/// Examples of **incorrect** code for this rule:
/// ```jsx
/// // By default, UNSAFE_ prefixed methods are flagged
/// class Foo extends React.Component {
/// UNSAFE_componentWillMount() {}
/// UNSAFE_componentWillReceiveProps() {}
/// UNSAFE_componentWillUpdate() {}
/// }
///
/// // With checkAliases: true, non-prefixed versions are also flagged
/// class Bar extends React.Component {
/// componentWillMount() {}
/// componentWillReceiveProps() {}
/// componentWillUpdate() {}
/// }
/// ```
///
/// Examples of **correct** code for this rule:
/// ```jsx
/// class Foo extends React.Component {
/// componentDidMount() {}
/// componentDidUpdate() {}
/// render() {}
/// }
/// ```
NoUnsafe,
react,
correctness,
config = NoUnsafeConfig,
);

impl Rule for NoUnsafe {
fn from_configuration(value: serde_json::Value) -> Self {
Self(
serde_json::from_value::<DefaultRuleConfig<NoUnsafeConfig>>(value)
.unwrap_or_default()
.into_inner(),
)
}

fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let react_version =
ctx.settings().react.version.as_ref().and_then(|v| parse_react_version(v.as_str()));

if let AstKind::MethodDefinition(method_def) = node.kind()
&& let Some(name) = method_def.key.static_name()
&& is_unsafe_method(name.as_ref(), self.0.check_aliases, react_version)
&& get_parent_component(node, ctx).is_some()
{
ctx.diagnostic(no_unsafe_diagnostic(name.as_ref(), method_def.key.span()));
}

if let AstKind::ObjectProperty(obj_prop) = node.kind()
&& let Some(name) = obj_prop.key.static_name()
&& is_unsafe_method(name.as_ref(), self.0.check_aliases, react_version)
{
for ancestor in ctx.nodes().ancestors(node.id()) {
if is_es5_component(ancestor) {
ctx.diagnostic(no_unsafe_diagnostic(name.as_ref(), obj_prop.key.span()));
break;
}
}
}
Comment on lines +90 to +111
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For performance, we have some code that does static analysis on the rules and detects what node types it looks at. It will be able to optimize this rule better if we organize it like this:

Suggested change
let react_version =
ctx.settings().react.version.as_ref().and_then(|v| parse_react_version(v.as_str()));
if let AstKind::MethodDefinition(method_def) = node.kind()
&& let Some(name) = method_def.key.static_name()
&& is_unsafe_method(name.as_ref(), self.0.check_aliases, react_version)
&& get_parent_component(node, ctx).is_some()
{
ctx.diagnostic(no_unsafe_diagnostic(name.as_ref(), method_def.key.span()));
}
if let AstKind::ObjectProperty(obj_prop) = node.kind()
&& let Some(name) = obj_prop.key.static_name()
&& is_unsafe_method(name.as_ref(), self.0.check_aliases, react_version)
{
for ancestor in ctx.nodes().ancestors(node.id()) {
if is_es5_component(ancestor) {
ctx.diagnostic(no_unsafe_diagnostic(name.as_ref(), obj_prop.key.span()));
break;
}
}
}
match node.kind() {
AstKind::MethodDefinition(method_def) => {
let react_version = ctx.settings().react.version.as_ref().and_then(|v| parse_react_version(v.as_str()));
if let Some(name) = method_def.key.static_name()
&& is_unsafe_method(name.as_ref(), self.0.check_aliases, react_version)
&& get_parent_component(node, ctx).is_some()
{
ctx.diagnostic(no_unsafe_diagnostic(name.as_ref(), method_def.key.span()));
}
}
AstKind::ObjectProperty(obj_prop) => { /* ... */ }
_ => {}
}

}

fn should_run(&self, ctx: &ContextHost) -> bool {
ctx.source_type().is_jsx()
}
}

/// Check if a method name is an unsafe lifecycle method
fn is_unsafe_method(
name: &str,
check_aliases: bool,
react_version: Option<(u32, u32, u32)>,
) -> bool {
// React 16.3 introduced the UNSAFE_ prefixed lifecycle methods
let check_unsafe_prefix =
react_version.is_none_or(|(major, minor, _)| major > 16 || (major == 16 && minor >= 3));

if check_unsafe_prefix
&& matches!(
name,
"UNSAFE_componentWillMount"
| "UNSAFE_componentWillReceiveProps"
| "UNSAFE_componentWillUpdate"
)
{
return true;
}

if check_aliases
&& matches!(
name,
"componentWillMount" | "componentWillReceiveProps" | "componentWillUpdate"
)
{
return true;
}

false
Comment on lines +129 to +149
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be condensed into a single match block I think:

Suggested change
if check_unsafe_prefix
&& matches!(
name,
"UNSAFE_componentWillMount"
| "UNSAFE_componentWillReceiveProps"
| "UNSAFE_componentWillUpdate"
)
{
return true;
}
if check_aliases
&& matches!(
name,
"componentWillMount" | "componentWillReceiveProps" | "componentWillUpdate"
)
{
return true;
}
false
match name {
"UNSAFE_componentWillMount"
| "UNSAFE_componentWillReceiveProps"
| "UNSAFE_componentWillUpdate" if check_unsafe_prefix => true,
"componentWillMount" | "componentWillReceiveProps" | "componentWillUpdate" if check_aliases => true,
_ => false
}

}

/// Parse React version string into (major, minor, patch) tuple
fn parse_react_version(version: &str) -> Option<(u32, u32, u32)> {
let parts: Vec<&str> = version.split('.').collect();
if parts.len() < 2 {
return None;
}

let major = parts[0].parse::<u32>().ok()?;
let minor = parts[1].parse::<u32>().ok()?;
let patch = parts.get(2).and_then(|p| p.parse::<u32>().ok()).unwrap_or(0);

Some((major, minor, patch))
}
Comment on lines +152 to +164
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This version written by @taearls is slightly better since it doesn't allocate:

/// Parse version string like "16.14.0" into (major, minor, patch)
fn parse_version(version: &str) -> Option<(u32, u32, u32)> {
    // Avoid Vec allocation by using split_once and split_once again
    let (major_str, rest) = version.split_once('.')?;
    let (minor_str, patch_str) = rest.split_once('.')?;

    let major = major_str.parse::<u32>().ok()?;
    let minor = minor_str.parse::<u32>().ok()?;
    let patch = patch_str.parse::<u32>().ok()?;

    Some((major, minor, patch))
}

Once of these PRs lands, we should move this into a shared util file.


#[test]
fn test() {
use crate::tester::Tester;

let pass = vec![
(
"
class Foo extends React.Component {
componentDidUpdate() {}
render() {}
}
",
None,
Some(serde_json::json!({ "settings": { "react": { "version": "16.4.0" } } })),
),
(
"
const Foo = createReactClass({
componentDidUpdate: function() {},
render: function() {}
});
",
None,
Some(serde_json::json!({ "settings": { "react": { "version": "16.4.0" } } })),
),
(
"
class Foo extends Bar {
componentWillMount() {}
componentWillReceiveProps() {}
componentWillUpdate() {}
}
",
None,
Some(serde_json::json!({ "settings": { "react": { "version": "16.4.0" } } })),
),
(
"
class Foo extends Bar {
UNSAFE_componentWillMount() {}
UNSAFE_componentWillReceiveProps() {}
UNSAFE_componentWillUpdate() {}
}
",
None,
Some(serde_json::json!({ "settings": { "react": { "version": "16.4.0" } } })),
),
(
"
const Foo = bar({
componentWillMount: function() {},
componentWillReceiveProps: function() {},
componentWillUpdate: function() {},
});
",
None,
Some(serde_json::json!({ "settings": { "react": { "version": "16.4.0" } } })),
),
(
"
const Foo = bar({
UNSAFE_componentWillMount: function() {},
UNSAFE_componentWillReceiveProps: function() {},
UNSAFE_componentWillUpdate: function() {},
});
",
None,
Some(serde_json::json!({ "settings": { "react": { "version": "16.4.0" } } })),
),
(
"
class Foo extends React.Component {
componentWillMount() {}
componentWillReceiveProps() {}
componentWillUpdate() {}
}
",
None,
Some(serde_json::json!({ "settings": { "react": { "version": "16.4.0" } } })),
),
(
"
class Foo extends React.Component {
UNSAFE_componentWillMount() {}
UNSAFE_componentWillReceiveProps() {}
UNSAFE_componentWillUpdate() {}
}
",
None,
Some(serde_json::json!({ "settings": { "react": { "version": "16.2.0" } } })),
),
(
"
const Foo = createReactClass({
componentWillMount: function() {},
componentWillReceiveProps: function() {},
componentWillUpdate: function() {},
});
",
None,
Some(serde_json::json!({ "settings": { "react": { "version": "16.4.0" } } })),
),
(
"
const Foo = createReactClass({
UNSAFE_componentWillMount: function() {},
UNSAFE_componentWillReceiveProps: function() {},
UNSAFE_componentWillUpdate: function() {},
});
",
None,
Some(serde_json::json!({ "settings": { "react": { "version": "16.2.0" } } })),
),
];

let fail = vec![
(
"
class Foo extends React.Component {
componentWillMount() {}
componentWillReceiveProps() {}
componentWillUpdate() {}
}
",
Some(serde_json::json!([{ "checkAliases": true }])),
Some(serde_json::json!({ "settings": { "react": { "version": "16.4.0" } } })),
),
(
"
class Foo extends React.Component {
UNSAFE_componentWillMount() {}
UNSAFE_componentWillReceiveProps() {}
UNSAFE_componentWillUpdate() {}
}
",
None,
Some(serde_json::json!({ "settings": { "react": { "version": "16.3.0" } } })),
),
(
"
const Foo = createReactClass({
componentWillMount: function() {},
componentWillReceiveProps: function() {},
componentWillUpdate: function() {},
});
",
Some(serde_json::json!([{ "checkAliases": true }])),
Some(serde_json::json!({ "settings": { "react": { "version": "16.3.0" } } })),
),
(
"
const Foo = createReactClass({
UNSAFE_componentWillMount: function() {},
UNSAFE_componentWillReceiveProps: function() {},
UNSAFE_componentWillUpdate: function() {},
});
",
None,
Some(serde_json::json!({ "settings": { "react": { "version": "16.3.0" } } })),
),
];

Tester::new(NoUnsafe::NAME, NoUnsafe::PLUGIN, pass, fail).test_and_snapshot();
}
Loading
Loading