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 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 // check current node is this.state.xx
fn is_state_member_expression(expression: &StaticMemberExpression<'_>) -> bool { fn is_state_member_expression(expression: &StaticMemberExpression<'_>) -> bool {
if let Expression::ThisExpression(_) = &expression.object { 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 { fn should_ignore_component<'a, 'b>(node: &'b AstNode<'a>, ctx: &'b LintContext<'a>) -> bool {
let mut is_constructor: bool = false; let mut is_constructor = false;
let mut is_call_expression_node: bool = false; let mut is_call_expression = false;
let mut is_component: bool = false; let mut is_component = false;
for parent in ctx.nodes().iter_parents(node.id()) { for parent in ctx.nodes().iter_parents(node.id()) {
if let AstKind::MethodDefinition(method_def) = parent.kind() { 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() { if matches!(parent.kind(), AstKind::CallExpression(_)) {
is_call_expression_node = true; is_call_expression = true;
} }
if is_es6_component(parent) || is_es5_component(parent) { if is_es6_component(parent) || is_es5_component(parent) {
is_component = true; is_component = true;
} }
}
is_constructor && !is_call_expression_node || !is_component if matches!(parent.kind(), AstKind::Class(_)) {
} break;
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) => { (is_constructor && !is_call_expression) || !is_component
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));
}
}
}
_ => {}
}
}
} }
#[test] #[test]
@ -199,15 +203,11 @@ fn test() {
use crate::tester::Tester; use crate::tester::Tester;
let pass = vec![ let pass = vec![
(
"var Hello = createReactClass({ "var Hello = createReactClass({
render: function() { render: function() {
return <div>Hello {this.props.name}</div>; return <div>Hello {this.props.name}</div>;
} }
});", });",
None,
),
(
" "
var Hello = createReactClass({ var Hello = createReactClass({
render: function() { render: function() {
@ -217,16 +217,10 @@ fn test() {
} }
}); });
", ",
None,
),
(
" "
var Hello = 'foo'; var Hello = 'foo';
module.exports = {}; module.exports = {};
", ",
None,
),
(
" "
class Hello { class Hello {
getFoo() { getFoo() {
@ -235,9 +229,6 @@ fn test() {
} }
} }
", ",
None,
),
(
" "
class Hello extends React.Component { class Hello extends React.Component {
constructor() { constructor() {
@ -245,9 +236,6 @@ fn test() {
} }
} }
", ",
None,
),
(
" "
class Hello extends React.Component { class Hello extends React.Component {
constructor() { constructor() {
@ -255,9 +243,6 @@ fn test() {
} }
} }
", ",
None,
),
(
" "
class OneComponent extends Component { class OneComponent extends Component {
constructor() { constructor() {
@ -271,12 +256,21 @@ 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![ let fail = vec![
(
r#" r#"
var Hello = createReactClass({ var Hello = createReactClass({
@ -297,9 +291,6 @@ fn test() {
} }
}); });
"#, "#,
None,
),
(
" "
var Hello = createReactClass({ var Hello = createReactClass({
render: function() { render: function() {
@ -308,9 +299,6 @@ fn test() {
} }
}); });
", ",
None,
),
(
r#" r#"
var Hello = createReactClass({ var Hello = createReactClass({
render: function() { render: function() {
@ -319,9 +307,6 @@ fn test() {
} }
}); });
"#, "#,
None,
),
(
r#" r#"
var Hello = createReactClass({ var Hello = createReactClass({
render: function() { render: function() {
@ -330,9 +315,6 @@ fn test() {
} }
}); });
"#, "#,
None,
),
(
r#" r#"
var Hello = createReactClass({ var Hello = createReactClass({
render: function() { render: function() {
@ -342,9 +324,6 @@ fn test() {
} }
}); });
"#, "#,
None,
),
(
r#" r#"
class Hello extends React.Component { class Hello extends React.Component {
constructor() { constructor() {
@ -355,9 +334,6 @@ fn test() {
} }
} }
"#, "#,
None,
),
(
r#" r#"
class Hello extends React.Component { class Hello extends React.Component {
constructor(props) { constructor(props) {
@ -368,9 +344,6 @@ fn test() {
} }
} }
"#, "#,
None,
),
(
r#" r#"
class Hello extends React.Component { class Hello extends React.Component {
componentWillMount() { componentWillMount() {
@ -378,9 +351,6 @@ fn test() {
} }
} }
"#, "#,
None,
),
(
r#" r#"
class Hello extends React.Component { class Hello extends React.Component {
componentDidMount() { componentDidMount() {
@ -388,9 +358,6 @@ fn test() {
} }
} }
"#, "#,
None,
),
(
r#" r#"
class Hello extends React.Component { class Hello extends React.Component {
componentWillReceiveProps() { componentWillReceiveProps() {
@ -398,9 +365,6 @@ fn test() {
} }
} }
"#, "#,
None,
),
(
r#" r#"
class Hello extends React.Component { class Hello extends React.Component {
shouldComponentUpdate() { shouldComponentUpdate() {
@ -408,9 +372,6 @@ fn test() {
} }
} }
"#, "#,
None,
),
(
r#" r#"
class Hello extends React.Component { class Hello extends React.Component {
componentWillUpdate() { componentWillUpdate() {
@ -418,9 +379,6 @@ fn test() {
} }
} }
"#, "#,
None,
),
(
r#" r#"
class Hello extends React.Component { class Hello extends React.Component {
componentDidUpdate() { componentDidUpdate() {
@ -428,9 +386,6 @@ fn test() {
} }
} }
"#, "#,
None,
),
(
r#" r#"
class Hello extends React.Component { class Hello extends React.Component {
componentWillUnmount() { componentWillUnmount() {
@ -438,8 +393,6 @@ fn test() {
} }
} }
"#, "#,
None,
),
]; ];
Tester::new(NoDirectMutationState::NAME, pass, fail).test_and_snapshot(); Tester::new(NoDirectMutationState::NAME, pass, fail).test_and_snapshot();