Skip to content

fix: False positive unnecessary else block diagnostic #16567

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

Closed

Conversation

ShoyuVanilla
Copy link
Member

Fixes #16556

RA detects the else blocks of if-else statements as "unnecessary" if the if-then block's last expression is a never type - e.g. return keyword -.
But this shouldn't be done to something like

let foo = if bar.is_err {
    return;
} else {
    bar.unwrap().0
};

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Feb 14, 2024
@ShoyuVanilla
Copy link
Member Author

More details are in my comment of the original issue

@davidsemakula
Copy link
Contributor

Thanks for the fix @ShoyuVanilla
I've left suggested changes and a few other considerations as inline comments

@ShoyuVanilla
Copy link
Member Author

ShoyuVanilla commented Feb 15, 2024

Thanks for the fix @ShoyuVanilla I've left suggested changes and a few other considerations as inline comments

Thanks a lot! But where can I see your reviews? BTW I'm in my office now, so it would be few hours later when I reflect them 😄

Oh, I can see your reviews now

Comment on lines 293 to 316
if let Some(else_branch) = else_branch {
// If else branch has a tail, it is an "expression" that produces a value,
// e.g. `let a = if { ... } else { ... };` and this `else` is not unnecessary
let mut branch = *else_branch;
loop {
match body.exprs[branch] {
Expr::Block { tail: Some(_), .. } => return,
Expr::If { then_branch, else_branch, .. } => {
if let Expr::Block { tail: Some(_), .. } = body.exprs[then_branch] {
return;
}
if let Some(else_branch) = else_branch {
// Continue checking for branches like `if { ... } else if { ... } else...`
branch = else_branch;
continue;
}
}
_ => break,
}
break;
}
} else {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't be looking for a tail expression in the else branch, you should instead be checking whether the if expression has a let statement as a "direct ancestor" (i.e. either a parent or an ancestor with only if expressions btn it and the current if expression - the latter is mostly to prevent the diagnostic from being to opinionated) 🙂

Suggested change
if let Some(else_branch) = else_branch {
// If else branch has a tail, it is an "expression" that produces a value,
// e.g. `let a = if { ... } else { ... };` and this `else` is not unnecessary
let mut branch = *else_branch;
loop {
match body.exprs[branch] {
Expr::Block { tail: Some(_), .. } => return,
Expr::If { then_branch, else_branch, .. } => {
if let Expr::Block { tail: Some(_), .. } = body.exprs[then_branch] {
return;
}
if let Some(else_branch) = else_branch {
// Continue checking for branches like `if { ... } else if { ... } else...`
branch = else_branch;
continue;
}
}
_ => break,
}
break;
}
} else {
return;
}
if else_branch.is_none() {
return;
}
let (body, source_map) = db.body_with_source_map(self.owner);
let Ok(source_ptr) = source_map.expr_syntax(id) else {
return;
};
let root = source_ptr.file_syntax(db.upcast());
let ast::Expr::IfExpr(if_expr) = source_ptr.value.to_node(&root) else {
return;
};
let mut top_if_expr = if_expr;
loop {
let parent = top_if_expr.syntax().parent();
let has_parent_let_stmt =
parent.as_ref().map_or(false, |node| ast::LetStmt::can_cast(node.kind()));
if has_parent_let_stmt {
// Bail if parent or direct ancestor is a let stmt.
return;
}
let Some(parent_if_expr) = parent.and_then(ast::IfExpr::cast) else {
// Parent is neither an if expr nor a let stmt.
break;
};
// Check parent if expr.
top_if_expr = parent_if_expr;
}

You'll also need to swap out the body: &Body parameter for db: &dyn HirDatabase

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's also possible to replace the loop in the suggestion with

let has_parent_let_stmt = if_expr.syntax().parent().and_then(ast::LetStmt::cast).is_some();
if has_parent_let_stmt {
    return;
}

i.e. only look for a parent let stmt.

But that would mean this new test would be linted

let _x = if a {
    return;
} else if b {
    return;
} else if c {
//^^^^ 💡 weak: remove unnecessary else block
    1
} else {
    return;
};

and fixed with

let _x = if a {
    return;
} else {
    if b {
        return;
    }
    if c {
        1
    } else {
        return;
    }
};

I'm not quite sure if that's desirable or too opinionated.
So I think its best to wait for one of the maintainers to chime in for that part.

Copy link
Member Author

@ShoyuVanilla ShoyuVanilla Feb 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should instead be checking whether the if expression has a let statement as a "direct ancestor"

Yes, this would be the right way for handling let a = if ... cases and I had considered that method - though quite ugly way iterating exprs once more for let bindings 😅, not as sophisticated as yours(I've learned a lot from your suggestion codes) - but I think that we should also filter out the cases that if-else expression is a tail expression of the other block, like the following example.

let foo = match bar {
    ...
    _ => {
        if baz {
            return;
        } else {
            a
        }
    }
};

I've tried to express this in the test code bellow you commented, but yes, as you pointed, that case is not well explaining this and should be linted as unnecessary.

Thinking about this again, it's tempting to take the method you suggested + extra filtering for the case that whole if-else chain is a tail expression in a block. I'll gonna look into this more once I get home

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what it's worth, I still think the example above should still be linted and fixed with below 🙂

let foo = match bar {
    ...
    _ => {
        if baz {
            return;
        }
        a
    }
};

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, I was dumb. You're right 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, well I'm dumber for writing this diagnostic and not thinking about let statements at all in the first place 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not at all. I think that writing something not exists is the hardest thing 😄
BTW, after applying your suggestions, I'm gonna think about nicer way to fix the triple if-else case you've mentioned. (I'm still at work 😢 )
Thank you for your kind and smart advices 👍

Comment on lines 406 to 408
} else {
0
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should still be linted

@davidsemakula
Copy link
Contributor

Thanks for the fix @ShoyuVanilla I've left suggested changes and a few other considerations as inline comments

Thanks a lot! But where can I see your reviews? BTW I'm in my office now, so it would be few hours later when I reflect them 😄

I guess I'd actually forgotten to hit submit 😅 ... you should be able to see them now 🙂

@Veykril Veykril added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Feb 15, 2024
Copy link
Contributor

@davidsemakula davidsemakula left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! @ShoyuVanilla I left a couple of nits you can fix that were originally added by me 🙂

ast::ElseBranch::Block(ref block) => {
block.statements().map(|stmt| format!("\n{indent}{stmt}")).join("")
}
ast::ElseBranch::Block(ref block) => block
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this was added by me 😅 ... but you can remove this unnecessary ref 🙂.

.statements()
.map(|stmt| format!("\n{indent}{stmt}"))
.chain(block.tail_expr().map(|tail| format!("\n{indent}{tail}")))
.join(""),
ast::ElseBranch::IfExpr(ref nested_if_expr) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: same here, you can remove this unnecessary ref, again my fault not yours 😅.

@davidsemakula
Copy link
Contributor

@ShoyuVanilla also would you consider adding the minor changes in #16575 to this PR? I would then close it in favor of this to reduce the review burden on the maintainers 🙂

@ShoyuVanilla
Copy link
Member Author

@ShoyuVanilla also would you consider adding the minor changes in #16575 to this PR? I would then close it in favor of this to reduce the review burden on the maintainers 🙂

Sure!

Comment on lines +305 to +320
let mut top_if_expr = if_expr;
loop {
let parent = top_if_expr.syntax().parent();
let has_parent_let_stmt =
parent.as_ref().map_or(false, |node| ast::LetStmt::can_cast(node.kind()));
if has_parent_let_stmt {
// Bail if parent or direct ancestor is a let stmt.
return;
}
let Some(parent_if_expr) = parent.and_then(ast::IfExpr::cast) else {
// Parent is neither an if expr nor a let stmt.
break;
};
// Check parent if expr.
top_if_expr = parent_if_expr;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't actually catch all the cases where this is wrong. The transformation is only legal if this if expression is an expression statement.

As foo(if a { return 1 } else { 0 }) shouldn't trigger this either for example, no let statements involved

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally parsing and such is rather expensive (ideally we could solve this without the AST, but for that we need a tree traversal here) so let's do this check directly before the self.diagnostics.push(BodyValidationDiagnostic::RemoveUnnecessaryElse { if_expr: id }) line

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'll do this tomorrow (It's midnight here 😄)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ShoyuVanilla I can also highjack this if that's fine with you ... still relatively early for me 🙂

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure you can

@davidsemakula
Copy link
Contributor

Changes moved to #16590

bors added a commit that referenced this pull request Feb 19, 2024
… r=Veykril

fix: Fix false positives for "unnecessary else" diagnostic

Completes #16567 by `@ShoyuVanilla` (see #16567 (comment))
Fixes #16556
@ShoyuVanilla ShoyuVanilla deleted the fix-necessary-else branch July 12, 2024 23:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

remove-unnecessary-else bad suggestions and false positives on thread_local macro
4 participants