feat(linter/eslint-plugin-react): Implement no-set-state (#3975)

Rule Detail:


[link](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-set-state.md)
This commit is contained in:
Jelle van der Waa 2024-06-30 13:11:33 +02:00 committed by GitHub
parent 1852ea4fd2
commit 10a3c9a052
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 209 additions and 0 deletions

View file

@ -217,6 +217,7 @@ mod react {
pub mod no_find_dom_node;
pub mod no_is_mounted;
pub mod no_render_return_value;
pub mod no_set_state;
pub mod no_string_refs;
pub mod no_unescaped_entities;
pub mod no_unknown_property;
@ -687,6 +688,7 @@ oxc_macros::declare_all_lint_rules! {
react::no_direct_mutation_state,
react::no_find_dom_node,
react::no_render_return_value,
react::no_set_state,
react::no_string_refs,
react::no_unescaped_entities,
react::no_is_mounted,

View file

@ -0,0 +1,165 @@
use oxc_ast::{ast::Expression, AstKind};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};
use crate::{
context::LintContext,
rule::Rule,
utils::{get_parent_es5_component, get_parent_es6_component},
AstNode,
};
fn no_set_state_diagnostic(span0: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("eslint-plugin-react(no-set-state): Do not use setState").with_label(span0)
}
#[derive(Debug, Default, Clone)]
pub struct NoSetState;
declare_oxc_lint!(
/// ### What it does
///
/// Disallow the usage of `this.setState` in React components.
///
/// ### Why is this bad?
///
/// When using an architecture that separates your application state from your UI components
/// (e.g. Flux), it may be desirable to forbid the use of local component state. This rule is
/// especially helpful in read-only applications (that don't use forms), since local component
/// state should rarely be necessary in such cases.
///
/// ### Example
/// ```javascript
/// var Hello = createReactClass({
/// getInitialState: function() {
/// return {
/// name: this.props.name
/// };
/// },
/// handleClick: function() {
/// this.setState({
/// name: this.props.name.toUpperCase()
/// });
/// },
/// render: function() {
/// return <div onClick={this.handleClick.bind(this)}>Hello {this.state.name}</div>;
/// }
/// });
/// ```
NoSetState,
style,
);
impl Rule for NoSetState {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::CallExpression(call_expr) = node.kind() else {
return;
};
let Some(member_expr) = call_expr.callee.as_member_expression() else {
return;
};
if !matches!(member_expr.object(), Expression::ThisExpression(_))
|| !member_expr.static_property_name().is_some_and(|str| str == "setState")
|| !(get_parent_es5_component(node, ctx).is_some()
|| get_parent_es6_component(ctx).is_some())
{
return;
}
ctx.diagnostic(no_set_state_diagnostic(call_expr.callee.span()));
}
}
#[test]
fn test() {
use crate::tester::Tester;
let pass = vec![
"
var Hello = function() {
this.setState({})
};
",
"
var Hello = createReactClass({
render: function() {
return <div>Hello {this.props.name}</div>;
}
});
",
"
var Hello = createReactClass({
componentDidUpdate: function() {
someNonMemberFunction(arg);
this.someHandler = this.setState;
},
render: function() {
return <div>Hello {this.props.name}</div>;
}
});
",
];
let fail = vec![
"
var Hello = createReactClass({
componentDidUpdate: function() {
this.setState({
name: this.props.name.toUpperCase()
});
},
render: function() {
return <div>Hello {this.state.name}</div>;
}
});
",
"
var Hello = createReactClass({
someMethod: function() {
this.setState({
name: this.props.name.toUpperCase()
});
},
render: function() {
return <div onClick={this.someMethod.bind(this)}>Hello {this.state.name}</div>;
}
});
",
"
class Hello extends React.Component {
someMethod() {
this.setState({
name: this.props.name.toUpperCase()
});
}
render() {
return <div onClick={this.someMethod.bind(this)}>Hello {this.state.name}</div>;
}
};
",
"
class Hello extends React.Component {
someMethod = () => {
this.setState({
name: this.props.name.toUpperCase()
});
}
render() {
return <div onClick={this.someMethod.bind(this)}>Hello {this.state.name}</div>;
}
};
",
"
class Hello extends React.Component {
render() {
return <div onMouseEnter={() => this.setState({dropdownIndex: index})} />;
}
};
",
];
Tester::new(NoSetState::NAME, pass, fail).test_and_snapshot();
}

View file

@ -0,0 +1,42 @@
---
source: crates/oxc_linter/src/tester.rs
---
⚠ eslint-plugin-react(no-set-state): Do not use setState
╭─[no_set_state.tsx:4:16]
3 │ componentDidUpdate: function() {
4 │ this.setState({
· ─────────────
5 │ name: this.props.name.toUpperCase()
╰────
⚠ eslint-plugin-react(no-set-state): Do not use setState
╭─[no_set_state.tsx:4:16]
3 │ someMethod: function() {
4 │ this.setState({
· ─────────────
5 │ name: this.props.name.toUpperCase()
╰────
⚠ eslint-plugin-react(no-set-state): Do not use setState
╭─[no_set_state.tsx:4:16]
3 │ someMethod() {
4 │ this.setState({
· ─────────────
5 │ name: this.props.name.toUpperCase()
╰────
⚠ eslint-plugin-react(no-set-state): Do not use setState
╭─[no_set_state.tsx:4:16]
3 │ someMethod = () => {
4 │ this.setState({
· ─────────────
5 │ name: this.props.name.toUpperCase()
╰────
⚠ eslint-plugin-react(no-set-state): Do not use setState
╭─[no_set_state.tsx:4:48]
3 │ render() {
4 │ return <div onMouseEnter={() => this.setState({dropdownIndex: index})} />;
· ─────────────
5 │ }
╰────