Skip to content

Commit 4d4ef00

Browse files
author
Yusuf Raji
committed
Add single_option_map lint
1 parent 390286d commit 4d4ef00

File tree

6 files changed

+201
-0
lines changed

6 files changed

+201
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6067,6 +6067,7 @@ Released 2018-09-13
60676067
[`single_element_loop`]: https://rust-lang.github.io/rust-clippy/master/index.html#single_element_loop
60686068
[`single_match`]: https://rust-lang.github.io/rust-clippy/master/index.html#single_match
60696069
[`single_match_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#single_match_else
6070+
[`single_option_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#single_option_map
60706071
[`single_range_in_vec_init`]: https://rust-lang.github.io/rust-clippy/master/index.html#single_range_in_vec_init
60716072
[`size_of_in_element_count`]: https://rust-lang.github.io/rust-clippy/master/index.html#size_of_in_element_count
60726073
[`size_of_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#size_of_ref

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
684684
crate::single_call_fn::SINGLE_CALL_FN_INFO,
685685
crate::single_char_lifetime_names::SINGLE_CHAR_LIFETIME_NAMES_INFO,
686686
crate::single_component_path_imports::SINGLE_COMPONENT_PATH_IMPORTS_INFO,
687+
crate::single_option_map::SINGLE_OPTION_MAP_INFO,
687688
crate::single_range_in_vec_init::SINGLE_RANGE_IN_VEC_INIT_INFO,
688689
crate::size_of_in_element_count::SIZE_OF_IN_ELEMENT_COUNT_INFO,
689690
crate::size_of_ref::SIZE_OF_REF_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ mod significant_drop_tightening;
339339
mod single_call_fn;
340340
mod single_char_lifetime_names;
341341
mod single_component_path_imports;
342+
mod single_option_map;
342343
mod single_range_in_vec_init;
343344
mod size_of_in_element_count;
344345
mod size_of_ref;
@@ -978,5 +979,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
978979
store.register_late_pass(|_| Box::<unnecessary_semicolon::UnnecessarySemicolon>::default());
979980
store.register_late_pass(move |_| Box::new(non_std_lazy_statics::NonStdLazyStatic::new(conf)));
980981
store.register_late_pass(|_| Box::new(manual_option_as_slice::ManualOptionAsSlice::new(conf)));
982+
store.register_late_pass(|_| Box::new(single_option_map::SingleOptionMap));
981983
// add lints here, do not remove this comment, it's used in `new_lint`
982984
}

clippy_lints/src/single_option_map.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use clippy_utils::diagnostics::span_lint_and_help;
2+
use clippy_utils::ty::is_type_diagnostic_item;
3+
use clippy_utils::{path_res, peel_blocks};
4+
use rustc_hir::def::Res;
5+
use rustc_hir::def_id::LocalDefId;
6+
use rustc_hir::intravisit::FnKind;
7+
use rustc_hir::{Body, ExprKind, FnDecl, FnRetTy};
8+
use rustc_lint::{LateContext, LateLintPass};
9+
use rustc_session::declare_lint_pass;
10+
use rustc_span::{Span, sym};
11+
12+
declare_clippy_lint! {
13+
/// ### What it does
14+
/// Checks for functions with method calls to `.map(_)` on an arg
15+
/// of type `Option` as the outermost expression.
16+
///
17+
/// ### Why is this bad?
18+
/// Taking and returning an `Option<T>` may require additional
19+
/// `Some(_)` and `unwrap` if all you have is a `T`.
20+
///
21+
/// ### Example
22+
/// ```no_run
23+
/// fn double(param: Option<u32>) -> Option<u32> {
24+
/// param.map(|x| x * 2)
25+
/// }
26+
/// ```
27+
/// Use instead:
28+
/// ```no_run
29+
/// fn double(param: u32) -> u32 {
30+
/// param * 2
31+
/// }
32+
/// ```
33+
#[clippy::version = "1.86.0"]
34+
pub SINGLE_OPTION_MAP,
35+
nursery,
36+
"Checks for functions with method calls to `.map(_)` on an arg of type `Option` as the outermost expression."
37+
}
38+
39+
declare_lint_pass!(SingleOptionMap => [SINGLE_OPTION_MAP]);
40+
41+
impl<'tcx> LateLintPass<'tcx> for SingleOptionMap {
42+
fn check_fn(
43+
&mut self,
44+
cx: &LateContext<'tcx>,
45+
kind: FnKind<'tcx>,
46+
decl: &'tcx FnDecl<'tcx>,
47+
body: &'tcx Body<'tcx>,
48+
span: Span,
49+
_fn_def: LocalDefId,
50+
) {
51+
if let FnRetTy::Return(_ret) = decl.output
52+
&& matches!(kind, FnKind::ItemFn(_, _, _) | FnKind::Method(_, _))
53+
{
54+
let func_body = peel_blocks(body.value);
55+
if let ExprKind::MethodCall(method_name, callee, args, _span) = func_body.kind
56+
&& method_name.ident.name == sym::map
57+
&& let callee_type = cx.typeck_results().expr_ty(callee)
58+
&& is_type_diagnostic_item(cx, callee_type, sym::Option)
59+
&& let ExprKind::Path(_path) = callee.kind
60+
&& let Res::Local(_id) = path_res(cx, callee)
61+
&& matches!(path_res(cx, callee), Res::Local(_id))
62+
&& !matches!(args[0].kind, ExprKind::Path(_))
63+
{
64+
if let ExprKind::Closure(closure) = args[0].kind {
65+
let Body { params: [..], value } = cx.tcx.hir().body(closure.body);
66+
if let ExprKind::Call(func, f_args) = value.kind
67+
&& matches!(func.kind, ExprKind::Path(_))
68+
&& f_args.iter().all(|arg| matches!(arg.kind, ExprKind::Path(_)))
69+
{
70+
return;
71+
} else if let ExprKind::MethodCall(_segment, receiver, method_args, _span) = value.kind
72+
&& matches!(receiver.kind, ExprKind::Path(_))
73+
&& method_args.iter().all(|arg| matches!(arg.kind, ExprKind::Path(_)))
74+
&& method_args.iter().all(|arg| matches!(path_res(cx, arg), Res::Local(_)))
75+
{
76+
return;
77+
}
78+
}
79+
80+
span_lint_and_help(
81+
cx,
82+
SINGLE_OPTION_MAP,
83+
span,
84+
"`fn` that only maps over argument",
85+
None,
86+
"move the `.map` to the caller or to an `_opt` function",
87+
);
88+
}
89+
}
90+
}
91+
}

tests/ui/single_option_map.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#![warn(clippy::single_option_map)]
2+
3+
use std::sync::atomic::{AtomicUsize, Ordering};
4+
5+
static ATOM: AtomicUsize = AtomicUsize::new(42);
6+
static MAYBE_ATOMIC: Option<&AtomicUsize> = Some(&ATOM);
7+
8+
fn h(arg: Option<u32>) -> Option<u32> {
9+
//~^ ERROR: `fn` that only maps over argument
10+
arg.map(|x| x * 2)
11+
}
12+
13+
fn j(arg: Option<u64>) -> Option<u64> {
14+
//~^ ERROR: `fn` that only maps over argument
15+
arg.map(|x| x * 2)
16+
}
17+
18+
fn mul_args(a: String, b: u64) -> String {
19+
a
20+
}
21+
22+
fn mul_args_opt(a: Option<String>, b: u64) -> Option<String> {
23+
//~^ ERROR: `fn` that only maps over argument
24+
a.map(|val| mul_args(val, b + 1))
25+
}
26+
27+
// No lint: no `Option` argument argument
28+
fn maps_static_option() -> Option<usize> {
29+
MAYBE_ATOMIC.map(|a| a.load(Ordering::Relaxed))
30+
}
31+
32+
// No lint: wrapped by another function
33+
fn manipulate(i: i32) -> i32 {
34+
i + 1
35+
}
36+
// No lint: wraps another function to do the optional thing
37+
fn manipulate_opt(opt_i: Option<i32>) -> Option<i32> {
38+
opt_i.map(manipulate)
39+
}
40+
41+
// No lint: maps other than the receiver
42+
fn map_not_arg(arg: Option<u32>) -> Option<u32> {
43+
maps_static_option().map(|_| arg.unwrap())
44+
}
45+
46+
// No lint: wrapper function with η-expanded form
47+
#[allow(clippy::redundant_closure)]
48+
fn manipulate_opt_explicit(opt_i: Option<i32>) -> Option<i32> {
49+
opt_i.map(|x| manipulate(x))
50+
}
51+
52+
// No lint
53+
fn multi_args(a: String, b: bool, c: u64) -> String {
54+
a
55+
}
56+
57+
// No lint: contains only map of a closure that binds other arguments
58+
fn multi_args_opt(a: Option<String>, b: bool, c: u64) -> Option<String> {
59+
a.map(|a| multi_args(a, b, c))
60+
}
61+
62+
fn main() {
63+
let answer = Some(42u32);
64+
let h_result = h(answer);
65+
66+
let answer = Some(42u64);
67+
let j_result = j(answer);
68+
maps_static_option();
69+
}

tests/ui/single_option_map.stderr

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
error: `fn` that only maps over argument
2+
--> tests/ui/single_option_map.rs:8:1
3+
|
4+
LL | / fn h(arg: Option<u32>) -> Option<u32> {
5+
LL | |
6+
LL | | arg.map(|x| x * 2)
7+
LL | | }
8+
| |_^
9+
|
10+
= help: move the `.map` to the caller or to an `_opt` function
11+
= note: `-D clippy::single-option-map` implied by `-D warnings`
12+
= help: to override `-D warnings` add `#[allow(clippy::single_option_map)]`
13+
14+
error: `fn` that only maps over argument
15+
--> tests/ui/single_option_map.rs:13:1
16+
|
17+
LL | / fn j(arg: Option<u64>) -> Option<u64> {
18+
LL | |
19+
LL | | arg.map(|x| x * 2)
20+
LL | | }
21+
| |_^
22+
|
23+
= help: move the `.map` to the caller or to an `_opt` function
24+
25+
error: `fn` that only maps over argument
26+
--> tests/ui/single_option_map.rs:22:1
27+
|
28+
LL | / fn mul_args_opt(a: Option<String>, b: u64) -> Option<String> {
29+
LL | |
30+
LL | | a.map(|val| mul_args(val, b + 1))
31+
LL | | }
32+
| |_^
33+
|
34+
= help: move the `.map` to the caller or to an `_opt` function
35+
36+
error: aborting due to 3 previous errors
37+

0 commit comments

Comments
 (0)