-
-
Notifications
You must be signed in to change notification settings - Fork 859
feat(linter): implement react/no-unsafe #16532
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+440
−3
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
f4849b2
feat(linter/plugins): add react/no-unsafe
Kenzo-Wada c2e6852
resolve comments
Kenzo-Wada 12b64c9
Merge branch 'main' into feat/react-no-unsafe
Kenzo-Wada 38b5110
fix lint error
Kenzo-Wada 1a712a1
Update crates/oxc_linter/src/rules/react/no_unsafe.rs
Kenzo-Wada 9fe813a
[autofix.ci] apply automated fixes
autofix-ci[bot] 4f397ad
Merge branch 'main' into feat/react-no-unsafe
Kenzo-Wada d377f6f
chore: refactor as match
Kenzo-Wada 487519d
chore: refact recommendations
Kenzo-Wada c742395
chore: run test
Kenzo-Wada be81c40
chore: run lintgen
Kenzo-Wada 7f2894d
chore: accept snapshots
Kenzo-Wada 651a3d2
chore: accept snapshots
Kenzo-Wada ed10e66
fi
camc314 ea37e62
u
camc314 9992481
fix link
camc314 f647fe1
Merge branch 'main' into feat/react-no-unsafe
camc314 b0f3366
u
camc314 94e71fe
iu
camc314 6dd2055
u
camc314 87d0cf3
u
camc314 71702e6
Merge branch 'main' into feat/react-no-unsafe
camc314 e99b09a
iu
camc314 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| } | ||
Kenzo-Wada marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| #[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. | ||
camc314 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// | ||
| /// ### 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::<DefaultRuleConfig<NoUnsafe>>(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(); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.