Skip to content

Commit

Permalink
feat(linter/eslint-plugin-react): Implement no-set-state
Browse files Browse the repository at this point in the history
  • Loading branch information
jelly committed Jun 29, 2024
1 parent 0c81fbe commit 5e7b3a2
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
165 changes: 165 additions & 0 deletions crates/oxc_linter/src/rules/react/no_set_state.rs
Original file line number Diff line number Diff line change
@@ -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();
}
42 changes: 42 additions & 0 deletions crates/oxc_linter/src/snapshots/no_set_state.snap
Original file line number Diff line number Diff line change
@@ -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]
3componentDidUpdate: function() {
4this.setState({
· ─────────────
5 │ name: this.props.name.toUpperCase()
╰────

eslint-plugin-react(no-set-state): Do not use setState
╭─[no_set_state.tsx:4:16]
3someMethod: function() {
4this.setState({
· ─────────────
5 │ name: this.props.name.toUpperCase()
╰────

eslint-plugin-react(no-set-state): Do not use setState
╭─[no_set_state.tsx:4:16]
3someMethod() {
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 = () => {
4this.setState({
· ─────────────
5 │ name: this.props.name.toUpperCase()
╰────

eslint-plugin-react(no-set-state): Do not use setState
╭─[no_set_state.tsx:4:48]
3render() {
4 │ return <div onMouseEnter={() => this.setState({dropdownIndex: index})} />;
· ─────────────
5 │ }
╰────

0 comments on commit 5e7b3a2

Please sign in to comment.