fix(linter/no-direct-mutation-state): false positive when class is declared inside a CallExpression (#3294)

fixes #3290
This commit is contained in:
Boshen 2024-05-15 14:07:01 +00:00
parent 8ff1ffba74
commit e12323f4f9

View file

@ -84,6 +84,44 @@ declare_oxc_lint!(
correctness
);
impl Rule for NoDirectMutationState {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
match node.kind() {
AstKind::AssignmentExpression(assignment_expr) => {
if should_ignore_component(node, ctx) {
return;
}
if let Some(assignment) = assignment_expr.left.as_simple_assignment_target() {
if let Some(outer_member_expression) = get_outer_member_expression(assignment) {
if is_state_member_expression(outer_member_expression) {
ctx.diagnostic(no_direct_mutation_state_diagnostic(
assignment_expr.left.span(),
));
}
}
}
}
AstKind::UpdateExpression(update_expr) => {
if should_ignore_component(node, ctx) {
return;
}
if let Some(outer_member_expression) =
get_outer_member_expression(&update_expr.argument)
{
if is_state_member_expression(outer_member_expression) {
ctx.diagnostic(no_direct_mutation_state_diagnostic(update_expr.span));
}
}
}
_ => {}
}
}
}
// check current node is this.state.xx
fn is_state_member_expression(expression: &StaticMemberExpression<'_>) -> bool {
if let Expression::ThisExpression(_) = &expression.object {
@ -133,9 +171,9 @@ fn get_static_member_expression_obj<'a, 'b>(
}
fn should_ignore_component<'a, 'b>(node: &'b AstNode<'a>, ctx: &'b LintContext<'a>) -> bool {
let mut is_constructor: bool = false;
let mut is_call_expression_node: bool = false;
let mut is_component: bool = false;
let mut is_constructor = false;
let mut is_call_expression = false;
let mut is_component = false;
for parent in ctx.nodes().iter_parents(node.id()) {
if let AstKind::MethodDefinition(method_def) = parent.kind() {
@ -144,54 +182,20 @@ fn should_ignore_component<'a, 'b>(node: &'b AstNode<'a>, ctx: &'b LintContext<'
}
}
if let AstKind::CallExpression(_) = parent.kind() {
is_call_expression_node = true;
if matches!(parent.kind(), AstKind::CallExpression(_)) {
is_call_expression = true;
}
if is_es6_component(parent) || is_es5_component(parent) {
is_component = true;
}
}
is_constructor && !is_call_expression_node || !is_component
}
impl Rule for NoDirectMutationState {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
match node.kind() {
AstKind::AssignmentExpression(assignment_expr) => {
if should_ignore_component(node, ctx) {
return;
}
if let Some(assignment) = assignment_expr.left.as_simple_assignment_target() {
if let Some(outer_member_expression) = get_outer_member_expression(assignment) {
if is_state_member_expression(outer_member_expression) {
ctx.diagnostic(no_direct_mutation_state_diagnostic(
assignment_expr.left.span(),
));
}
}
}
}
AstKind::UpdateExpression(update_expr) => {
if should_ignore_component(node, ctx) {
return;
}
if let Some(outer_member_expression) =
get_outer_member_expression(&update_expr.argument)
{
if is_state_member_expression(outer_member_expression) {
ctx.diagnostic(no_direct_mutation_state_diagnostic(update_expr.span));
}
}
}
_ => {}
if matches!(parent.kind(), AstKind::Class(_)) {
break;
}
}
(is_constructor && !is_call_expression) || !is_component
}
#[test]
@ -199,16 +203,12 @@ fn test() {
use crate::tester::Tester;
let pass = vec![
(
"var Hello = createReactClass({
"var Hello = createReactClass({
render: function() {
return <div>Hello {this.props.name}</div>;
}
});",
None,
),
(
"
"
var Hello = createReactClass({
render: function() {
var obj = {state: {}};
@ -217,17 +217,11 @@ fn test() {
}
});
",
None,
),
(
"
"
var Hello = 'foo';
module.exports = {};
",
None,
),
(
"
"
class Hello {
getFoo() {
this.state.foo = 'bar'
@ -235,30 +229,21 @@ fn test() {
}
}
",
None,
),
(
"
"
class Hello extends React.Component {
constructor() {
this.state.foo = 'bar'
}
}
",
None,
),
(
"
"
class Hello extends React.Component {
constructor() {
this.state.foo = 1;
}
}
",
None,
),
(
"
"
class OneComponent extends Component {
constructor() {
super();
@ -271,13 +256,22 @@ fn test() {
}
}
",
None,
),
"
describe('Component spec', () => {
it('should apply default props on rerender', () => {
class Outer extends Component {
constructor() {
super();
this.state = { i: 1 };
}
}
});
});
",
];
let fail = vec![
(
r#"
r#"
var Hello = createReactClass({
componentWillMount() {
@ -297,10 +291,7 @@ fn test() {
}
});
"#,
None,
),
(
"
"
var Hello = createReactClass({
render: function() {
this.state.foo++;
@ -308,10 +299,7 @@ fn test() {
}
});
",
None,
),
(
r#"
r#"
var Hello = createReactClass({
render: function() {
this.state.person.name= "bar"
@ -319,10 +307,7 @@ fn test() {
}
});
"#,
None,
),
(
r#"
r#"
var Hello = createReactClass({
render: function() {
this.state.person.name.first = "bar"
@ -330,10 +315,7 @@ fn test() {
}
});
"#,
None,
),
(
r#"
r#"
var Hello = createReactClass({
render: function() {
this.state.person.name.first = "bar"
@ -342,10 +324,7 @@ fn test() {
}
});
"#,
None,
),
(
r#"
r#"
class Hello extends React.Component {
constructor() {
someFn()
@ -355,10 +334,7 @@ fn test() {
}
}
"#,
None,
),
(
r#"
r#"
class Hello extends React.Component {
constructor(props) {
super(props)
@ -368,78 +344,55 @@ fn test() {
}
}
"#,
None,
),
(
r#"
r#"
class Hello extends React.Component {
componentWillMount() {
this.state.foo = "bar"
}
}
"#,
None,
),
(
r#"
r#"
class Hello extends React.Component {
componentDidMount() {
this.state.foo = "bar"
}
}
"#,
None,
),
(
r#"
r#"
class Hello extends React.Component {
componentWillReceiveProps() {
this.state.foo = "bar"
}
}
"#,
None,
),
(
r#"
r#"
class Hello extends React.Component {
shouldComponentUpdate() {
this.state.foo = "bar"
}
}
"#,
None,
),
(
r#"
r#"
class Hello extends React.Component {
componentWillUpdate() {
this.state.foo = "bar"
}
}
"#,
None,
),
(
r#"
r#"
class Hello extends React.Component {
componentDidUpdate() {
this.state.foo = "bar"
}
}
"#,
None,
),
(
r#"
r#"
class Hello extends React.Component {
componentWillUnmount() {
this.state.foo = "bar"
}
}
"#,
None,
),
];
Tester::new(NoDirectMutationState::NAME, pass, fail).test_and_snapshot();