Skip to content

Commit ad60005

Browse files
committed
feat: new lint concealed_obvious_default
1 parent 600a490 commit ad60005

29 files changed

+701
-261
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5694,6 +5694,7 @@ Released 2018-09-13
56945694
[`collection_is_never_read`]: https://rust-lang.github.io/rust-clippy/master/index.html#collection_is_never_read
56955695
[`comparison_chain`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_chain
56965696
[`comparison_to_empty`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_to_empty
5697+
[`concealed_obvious_default`]: https://rust-lang.github.io/rust-clippy/master/index.html#concealed_obvious_default
56975698
[`confusing_method_to_numeric_cast`]: https://rust-lang.github.io/rust-clippy/master/index.html#confusing_method_to_numeric_cast
56985699
[`const_is_empty`]: https://rust-lang.github.io/rust-clippy/master/index.html#const_is_empty
56995700
[`const_static_lifetime`]: https://rust-lang.github.io/rust-clippy/master/index.html#const_static_lifetime

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
359359
crate::methods::CLONE_ON_COPY_INFO,
360360
crate::methods::CLONE_ON_REF_PTR_INFO,
361361
crate::methods::COLLAPSIBLE_STR_REPLACE_INFO,
362+
crate::methods::CONCEALED_OBVIOUS_DEFAULT_INFO,
362363
crate::methods::CONST_IS_EMPTY_INFO,
363364
crate::methods::DOUBLE_ENDED_ITERATOR_LAST_INFO,
364365
crate::methods::DRAIN_COLLECT_INFO,
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
use rustc_span::Symbol;
2+
3+
use clippy_utils::diagnostics::span_lint_and_sugg;
4+
use clippy_utils::sym;
5+
use rustc_errors::Applicability;
6+
use rustc_hir as hir;
7+
use rustc_lint::LateContext;
8+
use rustc_middle::ty::{self, Ty};
9+
use rustc_span::Span;
10+
11+
use super::CONCEALED_OBVIOUS_DEFAULT;
12+
13+
pub(super) fn check(cx: &LateContext<'_>, recv: &hir::Expr<'_>, method_name: Symbol, call_span: Span) {
14+
// Type of the expression which invoked the method
15+
let recv_ty = cx.typeck_results().expr_ty(recv);
16+
17+
// Option::<bool>::unwrap_or_default()
18+
// ^^^^^^^^^^^^^^^^^^^
19+
// if the call comes from expansion, bail
20+
if call_span.from_expansion() {
21+
return;
22+
}
23+
24+
// Only consider algebraic data types e.g. an `Option`.
25+
// Their generics are represented by `generic_args`
26+
if let ty::Adt(adt, generic_args) = recv_ty.kind()
27+
// `name_of_generic`, is e.g. a `sym::Option`
28+
&& let Some(name_of_generic) = cx.tcx.get_diagnostic_name(adt.did())
29+
&& let Some((message, suggestion)) = CONCEALING_METHODS.into_iter().find(|concealing| {
30+
name_of_generic == concealing.ty && method_name == concealing.method
31+
}).and_then(|concealing| {
32+
let ty = generic_args.type_at(concealing.generic_index);
33+
extract_obvious_default(cx, ty).map(|(default, ty)| {
34+
let method = (concealing.fmt_msg)(ty);
35+
(
36+
format!("method {method} conceals the underlying type"),
37+
(concealing.fmt_sugg)(default),
38+
)
39+
})
40+
})
41+
{
42+
span_lint_and_sugg(
43+
cx,
44+
CONCEALED_OBVIOUS_DEFAULT,
45+
call_span,
46+
message,
47+
"write the default type explicitly".to_string(),
48+
suggestion,
49+
Applicability::MachineApplicable,
50+
);
51+
}
52+
}
53+
54+
/// Method which conceals an underlying type of a generic
55+
struct ConcealingMethod {
56+
/// Generic which contains the concealing method, e.g. `Option<T>`
57+
ty: Symbol,
58+
/// Index of the concealed generic type, e.g. `0` for `Option<T>`
59+
generic_index: usize,
60+
/// The concealing method, e.g. `unwrap_or_default`
61+
method: Symbol,
62+
/// Format the lint's message
63+
///
64+
/// Receives the concealed type, e.g. for `Option<bool>` receives `bool`
65+
fmt_msg: fn(&'static str) -> String,
66+
/// Format the lint's suggestion
67+
///
68+
/// Receives the default of the concealed type, e.g. for `Option<bool>` receives `false`,
69+
/// as `bool::default()` is `false`
70+
fmt_sugg: fn(&'static str) -> String,
71+
}
72+
73+
/// List of methods which use `Default` trait under the hood,
74+
/// but they have an alternative non-`Default` method
75+
///
76+
/// For example, there is `Option::unwrap_or_default` which is almost the same
77+
/// as `Option::unwrap_or`, but the type does not have to be provided and the
78+
/// `Default` implementation is used.
79+
const CONCEALING_METHODS: [ConcealingMethod; 4] = [
80+
ConcealingMethod {
81+
ty: sym::Result,
82+
// Result<T, E>
83+
// ^ want
84+
generic_index: 0,
85+
method: sym::unwrap_or_default,
86+
fmt_msg: |ty| format!("Result::<{ty}, _>::unwrap_or_default()"),
87+
fmt_sugg: |val| format!("unwrap_or({val})"),
88+
},
89+
ConcealingMethod {
90+
ty: sym::Option,
91+
// Option<T>
92+
// ^ want
93+
generic_index: 0,
94+
method: sym::unwrap_or_default,
95+
fmt_msg: |ty| format!("Option::<{ty}>::unwrap_or_default()"),
96+
fmt_sugg: |val| format!("unwrap_or({val})"),
97+
},
98+
ConcealingMethod {
99+
ty: sym::HashMapEntry,
100+
// Entry<'a, K, V, A = Global>
101+
// ^ want
102+
generic_index: 2,
103+
method: sym::or_default,
104+
fmt_msg: |ty| format!("hash_map::Entry::<'_, _, {ty}>::or_default()"),
105+
fmt_sugg: |val| format!("or_insert({val})"),
106+
},
107+
ConcealingMethod {
108+
ty: sym::BTreeEntry,
109+
// Entry<'a, K, V, A = Global>
110+
// ^ want
111+
generic_index: 2,
112+
method: sym::or_default,
113+
fmt_msg: |ty| format!("btree_map::Entry::<'_, _, {ty}>::or_default()"),
114+
fmt_sugg: |val| format!("or_insert({val})"),
115+
},
116+
];
117+
118+
/// Get default value of a type with an obvious default.
119+
///
120+
/// # Returns
121+
///
122+
/// If the type has an obvious default:
123+
///
124+
/// - Default for the type
125+
/// - The type as it should be displayed in the lint message
126+
///
127+
/// If the type is not considered to have an obvious default, return `None`.
128+
fn extract_obvious_default(cx: &LateContext<'_>, ty: Ty<'_>) -> Option<(&'static str, &'static str)> {
129+
match ty.peel_refs().kind() {
130+
ty::Int(ty) => Some(("0", ty.name_str())),
131+
ty::Uint(ty) => Some(("0", ty.name_str())),
132+
ty::Float(ty) => Some(("0.0", ty.name_str())),
133+
ty::Char => Some((r"'\0'", "char")),
134+
ty::Str => Some((r#""""#, "&str")),
135+
ty::Bool => Some(("false", "bool")),
136+
ty::Tuple(tys) if tys.is_empty() => Some(("()", "()")),
137+
ty::Adt(def, _) if cx.tcx.get_diagnostic_name(def.did()) == Some(sym::Option) => Some(("None", "Option<_>")),
138+
_ => None,
139+
}
140+
}

clippy_lints/src/methods/mod.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ mod clone_on_copy;
1414
mod clone_on_ref_ptr;
1515
mod cloned_instead_of_copied;
1616
mod collapsible_str_replace;
17+
mod concealed_obvious_default;
1718
mod double_ended_iterator_last;
1819
mod drain_collect;
1920
mod err_expect;
@@ -4565,6 +4566,46 @@ declare_clippy_lint! {
45654566
"hardcoded localhost IP address"
45664567
}
45674568

4569+
declare_clippy_lint! {
4570+
/// ### What it does
4571+
///
4572+
/// Checks for usages of methods like `Option::unwrap_or_default`,
4573+
/// for types where the `Default` implementation is obvious and can be written literally
4574+
///
4575+
/// ### Why is this bad?
4576+
///
4577+
/// You have to know what the underlying type is, instead of it being obvious from a glance
4578+
///
4579+
/// ### Example
4580+
/// ```no_run
4581+
/// fn f(
4582+
/// a: Option<&str>,
4583+
/// b: Option<usize>,
4584+
/// c: Option<Option<bool>>
4585+
/// ) {
4586+
/// let a = a.unwrap_or_default();
4587+
/// let b = b.unwrap_or_default();
4588+
/// let c = c.unwrap_or_default();
4589+
/// }
4590+
/// ```
4591+
/// Use instead:
4592+
/// ```no_run
4593+
/// fn f(
4594+
/// a: Option<&str>,
4595+
/// b: Option<usize>,
4596+
/// c: Option<Option<bool>>
4597+
/// ) {
4598+
/// let a = a.unwrap_or("");
4599+
/// let b = b.unwrap_or(0);
4600+
/// let c = c.unwrap_or(None);
4601+
/// }
4602+
/// ```
4603+
#[clippy::version = "1.89.0"]
4604+
pub CONCEALED_OBVIOUS_DEFAULT,
4605+
complexity,
4606+
".*or_default() obscures the underlying type"
4607+
}
4608+
45684609
#[expect(clippy::struct_excessive_bools)]
45694610
pub struct Methods {
45704611
avoid_breaking_exported_api: bool,
@@ -4744,6 +4785,7 @@ impl_lint_pass!(Methods => [
47444785
IO_OTHER_ERROR,
47454786
SWAP_WITH_TEMPORARY,
47464787
IP_CONSTANT,
4788+
CONCEALED_OBVIOUS_DEFAULT
47474789
]);
47484790

47494791
/// Extracts a method call name, args, and `Span` of the method name.
@@ -5544,7 +5586,7 @@ impl Methods {
55445586
}
55455587
}
55465588
// Handle method calls whose receiver and arguments may come from expansion
5547-
if let ExprKind::MethodCall(path, recv, args, _call_span) = expr.kind {
5589+
if let ExprKind::MethodCall(path, recv, args, call_span) = expr.kind {
55485590
match (path.ident.name, args) {
55495591
(sym::expect, [_]) if !matches!(method_call(recv), Some((sym::ok | sym::err, _, [], _, _))) => {
55505592
unwrap_expect_used::check(
@@ -5579,6 +5621,9 @@ impl Methods {
55795621
unwrap_expect_used::Variant::Unwrap,
55805622
);
55815623
},
5624+
(sym::or_default | sym::unwrap_or_default, []) => {
5625+
concealed_obvious_default::check(cx, recv, path.ident.name, call_span);
5626+
},
55825627
(sym::unwrap_err, []) => {
55835628
unwrap_expect_used::check(
55845629
cx,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//@aux-build:proc_macros.rs
2+
#![warn(clippy::concealed_obvious_default)]
3+
#![allow(dead_code)]
4+
#![allow(clippy::too_many_arguments)]
5+
6+
extern crate proc_macros;
7+
8+
use std::collections::{btree_map, hash_map};
9+
10+
fn test<'e, 'f, 'g, 'h>(
11+
a: Option<bool>,
12+
b: Option<Option<bool>>,
13+
c: Result<&str, ()>,
14+
d: Result<usize, ()>,
15+
e: btree_map::Entry<'e, (), f32>,
16+
e2: btree_map::Entry<'e, (), f32>,
17+
f: btree_map::Entry<'f, (), char>,
18+
f2: btree_map::Entry<'f, (), char>,
19+
g: hash_map::Entry<'g, (), ()>,
20+
g2: hash_map::Entry<'g, (), ()>,
21+
h: hash_map::Entry<'h, (), u8>,
22+
h2: hash_map::Entry<'h, (), u8>,
23+
) {
24+
_ = a.unwrap_or(false);
25+
//~^ concealed_obvious_default
26+
_ = a.unwrap_or(false);
27+
28+
_ = b.unwrap_or(None);
29+
//~^ concealed_obvious_default
30+
_ = b.unwrap_or(None);
31+
32+
_ = c.unwrap_or("");
33+
//~^ concealed_obvious_default
34+
_ = c.unwrap_or("");
35+
36+
_ = d.unwrap_or(0);
37+
//~^ concealed_obvious_default
38+
_ = d.unwrap_or(0);
39+
40+
_ = e.or_insert(0.0);
41+
//~^ concealed_obvious_default
42+
_ = e2.or_insert(0.0);
43+
44+
_ = f.or_insert('\0');
45+
//~^ concealed_obvious_default
46+
_ = f2.or_insert('\0');
47+
48+
_ = g.or_insert(());
49+
//~^ concealed_obvious_default
50+
_ = g2.or_insert(());
51+
52+
_ = h.or_insert(0);
53+
//~^ concealed_obvious_default
54+
_ = h2.or_insert(0);
55+
56+
//
57+
// If the receiver comes from macro expansion,
58+
// that's fine and we lint that.
59+
//
60+
61+
_ = proc_macros::external! {
62+
Some(false)
63+
}
64+
.unwrap_or(false);
65+
//~^ concealed_obvious_default
66+
67+
_ = proc_macros::external! {
68+
Some(false)
69+
}
70+
.unwrap_or(false);
71+
72+
//
73+
// However, if the method itself comes from
74+
// macro expansion, it should not be linted.
75+
//
76+
77+
_ = proc_macros::external! {
78+
Some(false).unwrap_or_default()
79+
};
80+
81+
_ = proc_macros::external! {
82+
Some(false).unwrap_or(false)
83+
};
84+
}
85+
86+
fn main() {}

0 commit comments

Comments
 (0)