-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
New lint: default_numeric_fallback #6662
Conversation
Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @flip1995 (or someone else) soon. If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes. Please see the contribution instructions for more information. |
547bc1b
to
c350f89
Compare
@flip1995 I finished the first implementation, though CI failed |
c350f89
to
e32e4de
Compare
I added some tests mainly related to |
Changes: Rewrote |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me preface this with: I hate giving reviews like this, when so much work was put into this.
So I don't think we can walk every type we find just to check if somewhere in the type some type has to be inferred. This is just way too overkill for this lint.
I also don't quite understand why this lint has to be so complicated. If I had to implement this, I would just implement it on a big match on the ExprKind
inside the check_expr
function. To avoid linting let
statements where types are defined, you can just save the HitId
of the local.init
expression and then skip this expression in check_expr
.
Looking at the tests, the only thing you want to lint are int or float literals. And the only case you don't want to lint is if you are inside a let binding where a type definition is in the let binding. You can achieve this with:
fn check_stmt(&mut self, cx: &LateContext<'_>, stmt: &Stmt<'_>) {
if_chain! {
if let StmtKind::Local(local) = stmt.kind;
if local.ty.is_some();
if let Some(init) = local.init;
then {
self.skip_lint.insert(init.hir_id);
}
}
}
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
let parent_iter = cx.tcx.hir().parent_iter(expr.hir_id);
if_chain! {
if let ExprKind::Lit(lit) = expr.kind;
if matches!(lit.node, LitKind::Int(_, LitIntType::Unsuffixed) | LitKind::Float(_, LitFloatType::Unsuffixed));
if let Some(ty) = cx.typeck_results().expr_ty(expr);
if matches!(ty.kind, ty::Int(IntTy::I32) | ty::Float(FloatTy::F64));
if !parent_iter.any(|(hir_id, _)| self.skip_lint.contains(hir_id));
then {
span_lint_and_sugg(..);
}
}
}
I didn't test this code, but I'm fairly confident that it should work like this or similar.
error: default numeric fallback might occur | ||
--> $DIR/default_numeric_fallback.rs:39:16 | ||
| | ||
LL | let x: _ = 13; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't lint this, because specifying the type _
really means "I don't care about this type".
error: default numeric fallback might occur | ||
--> $DIR/default_numeric_fallback.rs:40:22 | ||
| | ||
LL | let x: [_; 3] = [1, 2, 3]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
error: default numeric fallback might occur | ||
--> $DIR/default_numeric_fallback.rs:41:27 | ||
| | ||
LL | let x: (_, i32) = (1, 2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a FP, because the type of the second tuple item has been specified.
Let me explain. fn foo<T>(t:T) -> Foo {}
let x: Foo = foo(13); |
Also this case would cause FP in your implementation. fn foo() -> i32 {
13
} |
Just in case: I don't care at all even if I need to remove all the lines I wrote, I'm sorry if I took up your time though, as for me I was happy to learn a lot through this implementation. |
Anyway I'll rewrite this tomorrow. Thanks for reviewing. |
Changes: Simplify In fact, I can't yet be convinced that this implementation is right. A user wrote a code like below. fn foo(x: i32) -> i32 {}
fn main() {
// FP!
let x = foo(1);
} The current implementation would lint this although that's FP, so the user added a type annotation to avoid FP. fn foo(x: i32) -> i32 {}
fn main() {
let x: _ = foo(1);
} After months or so, the user noticed that they have to rewrite it so that fn foo<T: Integer>(x: T) -> i32 {}
fn main() {
// FN! Default numeric fallback has occured.
let x: _ = foo(1);
} This seems a disaster for me. The "complex" version can avoid this kind of disaster, and we can reduce the complexity in exchange for some increase of FPs or by allowing some FNs. |
As a reference, I simplified the "complex" version and reduced FPs by allowing FNs. |
No, the
Yes, I guess it would. But I think this is a really constructed example, because you wouldn't specify a type here. Even if you change it to fn foo<T>(t:T) -> Foo<T> {}
let x: Foo<_> = foo(13); I don't think this would be a FN, because you specified
Why is that a FP? The type is not immediately visible for the I now start to think that our interpretations of what the lint should do is different. You say, that only places where actual fallback occurs should be linted. I think all places where no type is specified immediately in the same statement should be linted.
Adding In the end all your work you put into this helped to produce the ICE reproducer, no one else managed to come up with. So your work was definitely not in vain. |
tests/ui/default_numeric_fallback.rs
Outdated
fn ret_i31() -> i32 { | ||
23 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should keep all the tests, so if someone else wants to enhance/modify the lint there are already tests in place, that can be used for reference. Also then I will see, how my suggested changes changed the behavior of the lint.
Consider this case. fn foo<T, U> foo(t: T) -> U {
...
}
let x: MyType = foo(1); In this case, T and U are irrelevant, so I still think this is FN. And I think there is another problem in the current implementation. If an init expression is fn main() {
let x: MyType = {
// FN.
let y = 13;
...
};
} |
Changes: In order to make the comparison easier, I pushed another version that addresses the problems I pointed out. This implementation is almost the same as this. |
e8a9cbe
to
93796b2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And I think there is another problem in the current implementation. If an init expression is
ExprKind::Block
and theLocal
has type annotation, the current implementation doesn't lint anything in the block
Yeah your right, that is really too restrictive.
The problem I have with making this lint too complex is that complexity makes bugs more likely and also makes it harder to fix bugs. And IMO this is a kind of lint, that should not require this complexity, when choosing the right heuristics.
The current implementation idea LGTM. But I don't think you need an extra visitor, since LateLintPass
already is a visitor. You can just push the arg types in check_expr
and then pop them in check_expr_post
. The arguments get visited after the {Method}Call
anyway. That way you also get rid of the restriction, that it only works if the call is on a Path
.
Also you have to be careful with calling fn_sig
on Closure
s. This will ICE
Please also add back all the tests you already had and the examples you gave in the comments of the PR with a comment each, why it should (not) lint.
let ty_bound = self.ty_bounds.last().unwrap(); | ||
if_chain! { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let ty_bound = self.ty_bounds.last().unwrap(); | |
if_chain! { | |
if_chain! { | |
if let Some(ty_bound) = self.ty_bounds.last(); |
Yeah, I completely agree.
I agree on this in almost all cases. But in this case, I think we can't use it as a visitor because there is no
Thanks for pointing it out! I didn't realize it.
Ok, I'll add them later. |
Ah right. Please add something like "This lint can only be allowed on function levels and above to the |
Changes:
|
Changes: Handle struct constructor case. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks really good to me now.
I think linting in return places is good, because normally the return place is not as close to the fn sig as in the tests, so specifying the type more locally makes sense to me. Not regarding correctness, but readability.
One small change left to make.
Changes: Use |
Perfect! I think we found a good balance for this lint. Thanks for all your work! @bors r+
I really like those comments summarizing your latest changes, btw 👍 |
📌 Commit 9b0c1eb has been approved by |
☀️ Test successful - checks-action_dev_test, checks-action_remark_test, checks-action_test |
@flip1995 Thanks for your patience in the long discussion. I really appreciate your help! |
fixes #6064
r? @flip1995
As we discussed in here and here, I start implementing this lint from the strictest version.
In this PR, I'll allow the below two cases to pass the lint to reduce FPs.
Local
ifLocal
has a type annotation, for example:Call
orMethodCall
if corresponding arguments of their signature have concrete types, for example:changelog: Added restriction lint:
default_numeric_fallback