diff --git a/apps/oxlint/src/snapshots/fixtures__issue_11644_-c .oxlintrc.json@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__issue_11644_-c .oxlintrc.json@oxlint.snap index 4eaedefe525c9..44069a0801afb 100644 --- a/apps/oxlint/src/snapshots/fixtures__issue_11644_-c .oxlintrc.json@oxlint.snap +++ b/apps/oxlint/src/snapshots/fixtures__issue_11644_-c .oxlintrc.json@oxlint.snap @@ -6,7 +6,7 @@ arguments: -c .oxlintrc.json working directory: fixtures/issue_11644 ---------- Found 0 warnings and 0 errors. -Finished in ms on 1 file with 161 rules using 1 threads. +Finished in ms on 1 file with 162 rules using 1 threads. ---------- CLI result: LintSucceeded ---------- diff --git a/crates/oxc_linter/src/config/mod.rs b/crates/oxc_linter/src/config/mod.rs index 4c734193780bc..bd19caa39175d 100644 --- a/crates/oxc_linter/src/config/mod.rs +++ b/crates/oxc_linter/src/config/mod.rs @@ -21,8 +21,6 @@ pub use overrides::OxlintOverrides; pub use oxlintrc::Oxlintrc; pub use plugins::LintPlugins; pub use rules::{ESLintRule, OxlintRules}; - -#[expect(unused)] pub use settings::{OxlintSettings, ReactVersion, jsdoc::JSDocPluginSettings}; #[derive(Debug, Default, Clone)] diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index e507295555e94..becb225d8d7fc 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -2453,6 +2453,12 @@ 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> = + Some(&AstTypesBitset::from_types(&[AstType::MethodDefinition, AstType::ObjectProperty])); + 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; diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 4d1c85c1563ae..92b01dc826ed1 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -389,6 +389,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; @@ -1085,6 +1086,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, diff --git a/crates/oxc_linter/src/rules/react/no_unsafe.rs b/crates/oxc_linter/src/rules/react/no_unsafe.rs new file mode 100644 index 0000000000000..cfc8d2c25849d --- /dev/null +++ b/crates/oxc_linter/src/rules/react/no_unsafe.rs @@ -0,0 +1,321 @@ +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, + config::ReactVersion, + context::{ContextHost, LintContext}, + rule::{DefaultRuleConfig, Rule}, + utils::{get_parent_component, is_es5_component}, +}; + +fn no_unsafe_diagnostic(method_name: &str, span: Span) -> OxcDiagnostic { + let replacement = match method_name { + "componentWillMount" | "UNSAFE_componentWillMount" => "componentDidMount", + "componentWillReceiveProps" | "UNSAFE_componentWillReceiveProps" => { + "getDerivedStateFromProps" + } + "componentWillUpdate" | "UNSAFE_componentWillUpdate" => "componentDidUpdate", + _ => "alternative lifecycle methods", + }; + + OxcDiagnostic::warn(format!("Unsafe lifecycle method `{method_name}` is not allowed")) + .with_help(format!( + "Use `{replacement}` instead. See https://legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html" + )) + .with_label(span) +} + +#[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. + /// + /// ### 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 { + serde_json::from_value::>(value) + .unwrap_or_default() + .into_inner() + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { + AstKind::MethodDefinition(method_def) => { + let react_version = ctx.settings().react.version.as_ref(); + + 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) => { + let react_version = ctx.settings().react.version.as_ref(); + + if 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; + } + } + } + } + _ => {} + } + } + + 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<&ReactVersion>) -> bool { + // React 16.3 introduced the UNSAFE_ prefixed lifecycle methods + let check_unsafe_prefix = + react_version.is_none_or(|v| v.major() > 16 || (v.major() == 16 && v.minor() >= 3)); + + match name { + "UNSAFE_componentWillMount" + | "UNSAFE_componentWillReceiveProps" + | "UNSAFE_componentWillUpdate" + if check_unsafe_prefix => + { + true + } + "componentWillMount" | "componentWillReceiveProps" | "componentWillUpdate" + if check_aliases => + { + true + } + _ => false, + } +} + +#[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(); +} diff --git a/crates/oxc_linter/src/snapshots/react_no_unsafe.snap b/crates/oxc_linter/src/snapshots/react_no_unsafe.snap new file mode 100644 index 0000000000000..abbad6de86624 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/react_no_unsafe.snap @@ -0,0 +1,110 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-react(no-unsafe): Unsafe lifecycle method `componentWillMount` is not allowed + ╭─[no_unsafe.tsx:3:14] + 2 │ class Foo extends React.Component { + 3 │ componentWillMount() {} + · ────────────────── + 4 │ componentWillReceiveProps() {} + ╰──── + help: Use `componentDidMount` instead. See https://legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html + + ⚠ eslint-plugin-react(no-unsafe): Unsafe lifecycle method `componentWillReceiveProps` is not allowed + ╭─[no_unsafe.tsx:4:14] + 3 │ componentWillMount() {} + 4 │ componentWillReceiveProps() {} + · ───────────────────────── + 5 │ componentWillUpdate() {} + ╰──── + help: Use `getDerivedStateFromProps` instead. See https://legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html + + ⚠ eslint-plugin-react(no-unsafe): Unsafe lifecycle method `componentWillUpdate` is not allowed + ╭─[no_unsafe.tsx:5:14] + 4 │ componentWillReceiveProps() {} + 5 │ componentWillUpdate() {} + · ─────────────────── + 6 │ } + ╰──── + help: Use `componentDidUpdate` instead. See https://legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html + + ⚠ eslint-plugin-react(no-unsafe): Unsafe lifecycle method `UNSAFE_componentWillMount` is not allowed + ╭─[no_unsafe.tsx:3:14] + 2 │ class Foo extends React.Component { + 3 │ UNSAFE_componentWillMount() {} + · ───────────────────────── + 4 │ UNSAFE_componentWillReceiveProps() {} + ╰──── + help: Use `componentDidMount` instead. See https://legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html + + ⚠ eslint-plugin-react(no-unsafe): Unsafe lifecycle method `UNSAFE_componentWillReceiveProps` is not allowed + ╭─[no_unsafe.tsx:4:14] + 3 │ UNSAFE_componentWillMount() {} + 4 │ UNSAFE_componentWillReceiveProps() {} + · ──────────────────────────────── + 5 │ UNSAFE_componentWillUpdate() {} + ╰──── + help: Use `getDerivedStateFromProps` instead. See https://legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html + + ⚠ eslint-plugin-react(no-unsafe): Unsafe lifecycle method `UNSAFE_componentWillUpdate` is not allowed + ╭─[no_unsafe.tsx:5:14] + 4 │ UNSAFE_componentWillReceiveProps() {} + 5 │ UNSAFE_componentWillUpdate() {} + · ────────────────────────── + 6 │ } + ╰──── + help: Use `componentDidUpdate` instead. See https://legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html + + ⚠ eslint-plugin-react(no-unsafe): Unsafe lifecycle method `componentWillMount` is not allowed + ╭─[no_unsafe.tsx:3:14] + 2 │ const Foo = createReactClass({ + 3 │ componentWillMount: function() {}, + · ────────────────── + 4 │ componentWillReceiveProps: function() {}, + ╰──── + help: Use `componentDidMount` instead. See https://legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html + + ⚠ eslint-plugin-react(no-unsafe): Unsafe lifecycle method `componentWillReceiveProps` is not allowed + ╭─[no_unsafe.tsx:4:14] + 3 │ componentWillMount: function() {}, + 4 │ componentWillReceiveProps: function() {}, + · ───────────────────────── + 5 │ componentWillUpdate: function() {}, + ╰──── + help: Use `getDerivedStateFromProps` instead. See https://legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html + + ⚠ eslint-plugin-react(no-unsafe): Unsafe lifecycle method `componentWillUpdate` is not allowed + ╭─[no_unsafe.tsx:5:14] + 4 │ componentWillReceiveProps: function() {}, + 5 │ componentWillUpdate: function() {}, + · ─────────────────── + 6 │ }); + ╰──── + help: Use `componentDidUpdate` instead. See https://legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html + + ⚠ eslint-plugin-react(no-unsafe): Unsafe lifecycle method `UNSAFE_componentWillMount` is not allowed + ╭─[no_unsafe.tsx:3:14] + 2 │ const Foo = createReactClass({ + 3 │ UNSAFE_componentWillMount: function() {}, + · ───────────────────────── + 4 │ UNSAFE_componentWillReceiveProps: function() {}, + ╰──── + help: Use `componentDidMount` instead. See https://legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html + + ⚠ eslint-plugin-react(no-unsafe): Unsafe lifecycle method `UNSAFE_componentWillReceiveProps` is not allowed + ╭─[no_unsafe.tsx:4:14] + 3 │ UNSAFE_componentWillMount: function() {}, + 4 │ UNSAFE_componentWillReceiveProps: function() {}, + · ──────────────────────────────── + 5 │ UNSAFE_componentWillUpdate: function() {}, + ╰──── + help: Use `getDerivedStateFromProps` instead. See https://legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html + + ⚠ eslint-plugin-react(no-unsafe): Unsafe lifecycle method `UNSAFE_componentWillUpdate` is not allowed + ╭─[no_unsafe.tsx:5:14] + 4 │ UNSAFE_componentWillReceiveProps: function() {}, + 5 │ UNSAFE_componentWillUpdate: function() {}, + · ────────────────────────── + 6 │ }); + ╰──── + help: Use `componentDidUpdate` instead. See https://legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html