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
Lint Option.map(f)
where f returns unit
#1467
Conversation
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.
Sorry, I'm not a regular Clippy contributor or anything, but I just wanted to mention that this construct is not called "nil function" in Rust – as Rust doesn't have a nil
value! :)
Functions without return values return an empty tuple (()
), called Unit. (To add to the confusion, this is also where you'd say a function returns void
in C, but the common definition of Void is that it describes a diverging function, e.g. an endless loop, or a process abort.)
clippy_lints/src/map_nil_fn.rs
Outdated
pub struct Pass; | ||
|
||
/// **What it does:** Checks for usage of `Option.map(f)` where f is a nil | ||
/// function or closure |
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.
In Rust, it's not called "nil function". It's actually a function that returns Unit (()
)
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.
Given that this code also handles uninhabited/never types, it's not just unit, so we might want to keep the function names the same. However, the user-facing lint name and documentation should probably just handle unit; using a diverging expression in map
sounds like a very rare use 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.
Whoops, thanks for the explanation. I will tidy up the user facing portions to consistently use unit
. I added the divergent support just for completeness and I couldn't think of any usecases apart from maybe _.map(std::process::exit)
. I can remove it to simplify the code slightly
clippy_lints/src/map_nil_fn.rs
Outdated
/// x.map(|msg| log_err_msg(format_msg(msg))) | ||
/// ``` | ||
/// The correct use would be: | ||
/// ```rust |
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'd put blank lines around code blocks
clippy_lints/src/map_nil_fn.rs
Outdated
// The expression inside a closure may or may not have surrounding braces and | ||
// semicolons, which causes problems when generating a suggestion. Given an | ||
// expression that evaluates to '()' or '!', recursively remove useless braces | ||
// and semi-colons until is suitable for including in the suggestion template |
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.
could probably be a doc comment with three slashes
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 good! Minor issues, then we can land this
CHANGELOG.md
Outdated
@@ -374,6 +374,7 @@ All notable changes to this project will be documented in this file. | |||
[`nonsensical_open_options`]: https://github.com/Manishearth/rust-clippy/wiki#nonsensical_open_options | |||
[`not_unsafe_ptr_arg_deref`]: https://github.com/Manishearth/rust-clippy/wiki#not_unsafe_ptr_arg_deref | |||
[`ok_expect`]: https://github.com/Manishearth/rust-clippy/wiki#ok_expect | |||
[`option_map_nil_fn`]: https://github.com/Manishearth/rust-clippy/wiki#option_map_nil_fn |
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.
It's called unit.
README.md
Outdated
@@ -295,6 +295,7 @@ name | |||
[nonsensical_open_options](https://github.com/Manishearth/rust-clippy/wiki#nonsensical_open_options) | warn | nonsensical combination of options for opening a file | |||
[not_unsafe_ptr_arg_deref](https://github.com/Manishearth/rust-clippy/wiki#not_unsafe_ptr_arg_deref) | warn | public functions dereferencing raw pointer arguments but not marked `unsafe` | |||
[ok_expect](https://github.com/Manishearth/rust-clippy/wiki#ok_expect) | warn | using `ok().expect()`, which gives worse error messages than calling `expect` directly on the Result | |||
[option_map_nil_fn](https://github.com/Manishearth/rust-clippy/wiki#option_map_nil_fn) | allow | using `Option.map(f)`, where f is a nil function or closure |
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.
"Function or closure returning unit"
clippy_lints/src/map_nil_fn.rs
Outdated
/// ``` | ||
declare_lint! { | ||
pub OPTION_MAP_NIL_FN, | ||
Allow, |
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.
Can this be Warn
? I'm not sure, since .map()
is also a rather common construct even with unit functs, and I'm not sure if if let
really is unambiguously better. I personally use if let
as much as possible, though it took me some time to switch over to that (and you'll see a lot of pre-2014 code using it).
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 use map
when I have a ready-made function that fits. If I needed to insert a closure that returns unit, I'd pick if let
.
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 agree that for the fn
case that if let
isn't obviously better so it should stay Deny
. But I do think if let
is clearer for closures though. Maybe we could split it into two lints:
OPTION_MAP_UNIT_FN
which isAllow
OPTION_MAP_UNIT_CLOSURE
which isDeny
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 Deny
a lint unless we can be reasonably sure to have found an error. Warn
should be preferred here IMHO.
clippy_lints/src/map_nil_fn.rs
Outdated
#[derive(Clone)] | ||
pub struct Pass; | ||
|
||
/// **What it does:** Checks for usage of `Option.map(f)` where f is a nil |
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.
What if we instead lint for Option.map(f)
where the result isn't used? Covers non-unit functions too. The code can be similar to the must_use lint from rustc.
Lint is fine as is too though.
https://github.com/rust-lang/rust/blob/master/src/librustc_lint/unused.rs#L110
clippy_lints/src/map_nil_fn.rs
Outdated
// expression that evaluates to '()' or '!', recursively remove useless braces | ||
// and semi-colons until is suitable for including in the suggestion template | ||
fn reduce_nil_expression<'a>(cx: &LateContext, expr: &'a hir::Expr) -> Option<Span> { | ||
if !is_nil_expression(cx, expr) { |
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 wonder if the bulk of this function could be moved into utils as a general-purpose closure-reducing function.
clippy_lints/src/map_nil_fn.rs
Outdated
pub struct Pass; | ||
|
||
/// **What it does:** Checks for usage of `Option.map(f)` where f is a nil | ||
/// function or closure |
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.
Given that this code also handles uninhabited/never types, it's not just unit, so we might want to keep the function names the same. However, the user-facing lint name and documentation should probably just handle unit; using a diverging expression in map
sounds like a very rare use case.
Feel free to review clippy PRs 😄 |
@philipturnbull are you planning to continue this PR or do you want someone to take over? |
Friendly ping @philipturnbull |
So sorry 😞 I missed your first ping. I haven't got time to work on this atm, so someone else can take over this PR. One problem I had was some "analysis paralysis" over classifying things as Allow/Warn/Deny. As discussed in other comments there doesn't seem to be one canonical style so it isn't obvious what the default values should be. |
OK. Don't worry about classifications. Usually we make stuff warn by default as long as they don't have false positives. Deny by default is reserved for bad bugs that eat laudry. Even if we get it wrong, it can quickly be fixed later |
@oli-obk I would like to finish this one up. I have a rebased branch ready but should I open a new PR or continue (force) pushing to |
@phansch feel free to force-push here, that way all the discussion stays in one place. |
Option.map(f)
where f returns nilOption.map(f)
where f returns nil
1ec3aca
to
d19082a
Compare
Option.map(f)
where f returns nilOption.map(f)
where f returns nil
tests/ui/option_map_unit_fn.stderr
Outdated
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^- | ||
| | | ||
| help: try this: `if let Some(value) = x.field { ... }` | ||
|
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.
Is it possible to make this suggestion contain the expression?
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.
Yup, looks like it's possible. Tomorrow I want to add a bunch more test cases with multiple statements and expressions and see how those behave.
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.
Okay, I have to take that back as I'm currently stuck here.
The culprit is this match block:
Currently it only covers two cases:
- no statement, one expression
- one statement, no expression
For any other case it returns None
; resulting in the placeholder ...
.
The cases not covered are blocks that:
- contain both statements and expressions
{ foo; bar; "baz" }
- contain multiple statements
{ abc; def; }
- span multiple lines (these get turned into a single-line suggestion as it is currently)
Is there a way to get the full span of inside a block, without the braces directly, without doing all the reduction? I had a look at the utils, but there was only remove_blocks
that returns the input block expression if it contains any statements.
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 don't think we have anything for that. For now, just make the None
case use span_lint_approximate
tests/ui/option_map_unit_fn.stderr
Outdated
35 | x.field.map(do_nothing); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^- | ||
| | | ||
| help: try this: `if let Some(...) = x.field { do_nothing(...) }` |
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.
Maybe we could generate a variable name? The name could be taken from the variable that is matched on.
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.
Something like x_field
or field
in this case? I guess it could cause problems (with rustfix?) if there is already a variable with the same name as the generated one?
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.
There could be, but let's do it anyway and mark the suggestion as approximate
hir::StmtDecl(ref d, _) => Some(d.span), | ||
hir::StmtExpr(ref e, _) => Some(e.span), | ||
hir::StmtSemi(ref e, _) => { | ||
if is_unit_expression(cx, e) { |
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.
Any particular reason we don't just keep the semicolon always?
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 pushed 85ad541 to remove it and it seems fine. The new stderr output doesn't show any weird changes, so I guess it was just an optical thing.
@oli-obk I will have a look at the suggestion improvements later today, thanks! Do you think it also makes sense to expand this to |
Yes that makes sense |
d19082a
to
85ad541
Compare
Option.map(f)
where f returns nilOption.map(f)
where f returns unit
Option.map(f)
where f returns unitOption.map(f)
where f returns unit
663d5c8
to
53f1a98
Compare
`reduce_nil_closure` mixed together a) 'is this a nil closure?' and b) 'can it be reduced to a simple expression?'. Split the logic into two functions so we can still generate a basic warning when the closure can't be simplified.
This was a bit messed up after a bigger rebase.
Rust does not have nil.
Given a map call like `x.field.map ...` the suggestion will contain: `if let Some(x_field) ...` Given a map call like `x.map ...` the suggestion will contain: `if let Some(_x) ...` Otherwise it will suggest: `if let Some(_) ...`
85befa8
to
b61affb
Compare
b61affb
to
4f4e20c
Compare
Ok, the last three commits contain the (basic) let binding variable suggestions,make two of the three suggestions approximate and also add the lint for Result.map ✨ If the diff is too big, I can also open a separate PR for the Result.map lint. |
Option.map(f)
where f returns unitOption.map(f)
where f returns unit
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.
Just some nits.
clippy_lints/src/map_unit_fn.rs
Outdated
#[derive(Clone)] | ||
pub struct Pass; | ||
|
||
/// **What it does:** Checks for usage of `Option.map(f)` where f is a function |
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.
Write Option
in lowercase
clippy_lints/src/map_unit_fn.rs
Outdated
/// or closure that returns the unit type. | ||
/// | ||
/// **Why is this bad?** Readability, this can be written more clearly with | ||
/// an if statement |
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.
an if let statement
clippy_lints/src/map_unit_fn.rs
Outdated
"using `Option.map(f)`, where f is a function or closure that returns ()" | ||
} | ||
|
||
/// **What it does:** Checks for usage of `Result.map(f)` where f is a function |
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.
lowercase Result
clippy_lints/src/map_unit_fn.rs
Outdated
/// or closure that returns the unit type. | ||
/// | ||
/// **Why is this bad?** Readability, this can be written more clearly with | ||
/// an if statement |
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.
also if let
clippy_lints/src/map_unit_fn.rs
Outdated
declare_clippy_lint! { | ||
pub RESULT_MAP_UNIT_FN, | ||
complexity, | ||
"using `Result.map(f)`, where f is a function or closure that returns ()" |
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.
lowercase Result
clippy_lints/src/map_unit_fn.rs
Outdated
Approx(String) | ||
} | ||
|
||
let suggestion = if let Some(expr_span) = reduce_unit_expression(cx, closure_expr) { |
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.
just move this if let
into the lint closure. That way you don't need the intermediate enum.
Should be fine now :) |
This fixes #1352.
The logic for finding instances is relatively easy. Most of the code is dealing with generating nice, valid suggestions.