Skip to content

Commit 73a16c1

Browse files
committed
Suggest Option<&T> instead of &Option<T>
1 parent b367d34 commit 73a16c1

16 files changed

+736
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5871,6 +5871,7 @@ Released 2018-09-13
58715871
[`ref_as_ptr`]: https://rust-lang.github.io/rust-clippy/master/index.html#ref_as_ptr
58725872
[`ref_binding_to_reference`]: https://rust-lang.github.io/rust-clippy/master/index.html#ref_binding_to_reference
58735873
[`ref_in_deref`]: https://rust-lang.github.io/rust-clippy/master/index.html#ref_in_deref
5874+
[`ref_option`]: https://rust-lang.github.io/rust-clippy/master/index.html#ref_option
58745875
[`ref_option_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#ref_option_ref
58755876
[`ref_patterns`]: https://rust-lang.github.io/rust-clippy/master/index.html#ref_patterns
58765877
[`regex_macro`]: https://rust-lang.github.io/rust-clippy/master/index.html#regex_macro

book/src/lint_configuration.md

+1
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ Suppress lints whenever the suggested change would cause breakage for other crat
353353
* [`rc_buffer`](https://rust-lang.github.io/rust-clippy/master/index.html#rc_buffer)
354354
* [`rc_mutex`](https://rust-lang.github.io/rust-clippy/master/index.html#rc_mutex)
355355
* [`redundant_allocation`](https://rust-lang.github.io/rust-clippy/master/index.html#redundant_allocation)
356+
* [`ref_option`](https://rust-lang.github.io/rust-clippy/master/index.html#ref_option)
356357
* [`single_call_fn`](https://rust-lang.github.io/rust-clippy/master/index.html#single_call_fn)
357358
* [`trivially_copy_pass_by_ref`](https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref)
358359
* [`unnecessary_box_returns`](https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_box_returns)

clippy_config/src/conf.rs

+1
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ define_Conf! {
378378
rc_buffer,
379379
rc_mutex,
380380
redundant_allocation,
381+
ref_option,
381382
single_call_fn,
382383
trivially_copy_pass_by_ref,
383384
unnecessary_box_returns,

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
202202
crate::functions::MUST_USE_CANDIDATE_INFO,
203203
crate::functions::MUST_USE_UNIT_INFO,
204204
crate::functions::NOT_UNSAFE_PTR_ARG_DEREF_INFO,
205+
crate::functions::REF_OPTION_INFO,
205206
crate::functions::RENAMED_FUNCTION_PARAMS_INFO,
206207
crate::functions::RESULT_LARGE_ERR_INFO,
207208
crate::functions::RESULT_UNIT_ERR_INFO,

clippy_lints/src/functions/mod.rs

+57
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod impl_trait_in_params;
22
mod misnamed_getters;
33
mod must_use;
44
mod not_unsafe_ptr_arg_deref;
5+
mod ref_option;
56
mod renamed_function_params;
67
mod result;
78
mod too_many_arguments;
@@ -399,6 +400,50 @@ declare_clippy_lint! {
399400
"renamed function parameters in trait implementation"
400401
}
401402

403+
declare_clippy_lint! {
404+
/// ### What it does
405+
/// Warns when a function signature uses `&Option<T>` instead of `Option<&T>`.
406+
///
407+
/// ### Why is this bad?
408+
/// More flexibility, better memory optimization, and more idiomatic Rust code.
409+
///
410+
/// `&Option<T>` in a function signature breaks encapsulation because the caller must own T
411+
/// and move it into an Option to call with it. When returned, the owner must internally store
412+
/// it as `Option<T>` in order to return it.
413+
/// At a lower level `&Option<T>` points to memory that has `presence` bit flag + value,
414+
/// whereas `Option<&T>` is always optimized to a single pointer.
415+
///
416+
/// See this [YouTube video](https://www.youtube.com/watch?v=6c7pZYP_iIE) by
417+
/// Logan Smith for an in-depth explanation of why this is important.
418+
///
419+
/// ### Known problems
420+
/// This lint recommends changing the function signatures, but it cannot
421+
/// automatically change the function calls or the function implementations.
422+
///
423+
/// ### Example
424+
/// ```no_run
425+
/// // caller uses foo(&opt)
426+
/// fn foo(a: &Option<String>) {}
427+
/// # struct Unit {}
428+
/// # impl Unit {
429+
/// fn bar(&self) -> &Option<String> { &None }
430+
/// # }
431+
/// ```
432+
/// Use instead:
433+
/// ```no_run
434+
/// // caller should use foo(opt.as_ref())
435+
/// fn foo(a: Option<&String>) {}
436+
/// # struct Unit {}
437+
/// # impl Unit {
438+
/// fn bar(&self) -> Option<&String> { None }
439+
/// # }
440+
/// ```
441+
#[clippy::version = "1.82.0"]
442+
pub REF_OPTION,
443+
nursery,
444+
"function signature uses `&Option<T>` instead of `Option<&T>`"
445+
}
446+
402447
pub struct Functions {
403448
too_many_arguments_threshold: u64,
404449
too_many_lines_threshold: u64,
@@ -437,6 +482,7 @@ impl_lint_pass!(Functions => [
437482
MISNAMED_GETTERS,
438483
IMPL_TRAIT_IN_PARAMS,
439484
RENAMED_FUNCTION_PARAMS,
485+
REF_OPTION,
440486
]);
441487

442488
impl<'tcx> LateLintPass<'tcx> for Functions {
@@ -455,6 +501,16 @@ impl<'tcx> LateLintPass<'tcx> for Functions {
455501
not_unsafe_ptr_arg_deref::check_fn(cx, kind, decl, body, def_id);
456502
misnamed_getters::check_fn(cx, kind, decl, body, span);
457503
impl_trait_in_params::check_fn(cx, &kind, body, hir_id);
504+
ref_option::check_fn(
505+
cx,
506+
kind,
507+
decl,
508+
span,
509+
hir_id,
510+
def_id,
511+
body,
512+
self.avoid_breaking_exported_api,
513+
);
458514
}
459515

460516
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) {
@@ -475,5 +531,6 @@ impl<'tcx> LateLintPass<'tcx> for Functions {
475531
must_use::check_trait_item(cx, item);
476532
result::check_trait_item(cx, item, self.large_error_threshold);
477533
impl_trait_in_params::check_trait_item(cx, item, self.avoid_breaking_exported_api);
534+
ref_option::check_trait_item(cx, item, self.avoid_breaking_exported_api);
478535
}
479536
}
+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
use crate::functions::REF_OPTION;
2+
use clippy_utils::diagnostics::span_lint_and_then;
3+
use clippy_utils::is_trait_impl_item;
4+
use clippy_utils::source::snippet;
5+
use clippy_utils::ty::is_type_diagnostic_item;
6+
use rustc_errors::Applicability;
7+
use rustc_hir as hir;
8+
use rustc_hir::intravisit::FnKind;
9+
use rustc_hir::{FnDecl, HirId};
10+
use rustc_lint::LateContext;
11+
use rustc_middle::ty::{self, GenericArgKind, Mutability, Ty};
12+
use rustc_span::def_id::LocalDefId;
13+
use rustc_span::{Span, sym};
14+
15+
fn check_ty<'a>(cx: &LateContext<'a>, param: &rustc_hir::Ty<'a>, param_ty: Ty<'a>, fixes: &mut Vec<(Span, String)>) {
16+
if let ty::Ref(_, opt_ty, Mutability::Not) = param_ty.kind()
17+
&& is_type_diagnostic_item(cx, *opt_ty, sym::Option)
18+
&& let ty::Adt(_, opt_gen) = opt_ty.kind()
19+
&& let [gen] = opt_gen.as_slice()
20+
&& let GenericArgKind::Type(gen_ty) = gen.unpack()
21+
&& !gen_ty.is_ref()
22+
// Need to gen the original spans, so first parsing mid, and hir parsing afterward
23+
&& let hir::TyKind::Ref(lifetime, hir::MutTy { ty, .. }) = param.kind
24+
&& let hir::TyKind::Path(hir::QPath::Resolved(_, path)) = ty.kind
25+
&& let (Some(first), Some(last)) = (path.segments.first(), path.segments.last())
26+
&& let Some(hir::GenericArgs {
27+
args: [hir::GenericArg::Type(opt_ty)],
28+
..
29+
}) = last.args
30+
{
31+
let lifetime = snippet(cx, lifetime.ident.span, "..");
32+
fixes.push((
33+
param.span,
34+
format!(
35+
"{}<&{lifetime}{}{}>",
36+
snippet(cx, first.ident.span.to(last.ident.span), ".."),
37+
if lifetime.is_empty() { "" } else { " " },
38+
snippet(cx, opt_ty.span, "..")
39+
),
40+
));
41+
}
42+
}
43+
44+
fn check_fn_sig<'a>(cx: &LateContext<'a>, decl: &FnDecl<'a>, span: Span, sig: ty::FnSig<'a>) {
45+
let mut fixes = Vec::new();
46+
// Check function arguments' types
47+
for (param, param_ty) in decl.inputs.iter().zip(sig.inputs()) {
48+
check_ty(cx, param, *param_ty, &mut fixes);
49+
}
50+
// Check return type
51+
if let hir::FnRetTy::Return(ty) = &decl.output {
52+
check_ty(cx, ty, sig.output(), &mut fixes);
53+
}
54+
if !fixes.is_empty() {
55+
span_lint_and_then(
56+
cx,
57+
REF_OPTION,
58+
span,
59+
"it is more idiomatic to use `Option<&T>` instead of `&Option<T>`",
60+
|diag| {
61+
diag.multipart_suggestion("change this to", fixes, Applicability::Unspecified);
62+
},
63+
);
64+
}
65+
}
66+
67+
#[allow(clippy::too_many_arguments)]
68+
pub(crate) fn check_fn<'a>(
69+
cx: &LateContext<'a>,
70+
kind: FnKind<'_>,
71+
decl: &FnDecl<'a>,
72+
span: Span,
73+
hir_id: HirId,
74+
def_id: LocalDefId,
75+
body: &hir::Body<'_>,
76+
avoid_breaking_exported_api: bool,
77+
) {
78+
if avoid_breaking_exported_api && cx.effective_visibilities.is_exported(def_id) {
79+
return;
80+
}
81+
82+
if let FnKind::Closure = kind {
83+
// Compute the span of the closure parameters + return type if set
84+
let span = if let hir::FnRetTy::Return(out_ty) = &decl.output {
85+
if decl.inputs.is_empty() {
86+
out_ty.span
87+
} else {
88+
span.with_hi(out_ty.span.hi())
89+
}
90+
} else if let (Some(first), Some(last)) = (decl.inputs.first(), decl.inputs.last()) {
91+
first.span.to(last.span)
92+
} else {
93+
// No parameters - no point in checking
94+
return;
95+
};
96+
97+
// Figure out the signature of the closure
98+
let ty::Closure(_, args) = cx.typeck_results().expr_ty(body.value).kind() else {
99+
return;
100+
};
101+
let sig = args.as_closure().sig().skip_binder();
102+
103+
check_fn_sig(cx, decl, span, sig);
104+
} else if !is_trait_impl_item(cx, hir_id) {
105+
let sig = cx.tcx.fn_sig(def_id).instantiate_identity().skip_binder();
106+
check_fn_sig(cx, decl, span, sig);
107+
}
108+
}
109+
110+
pub(super) fn check_trait_item<'a>(
111+
cx: &LateContext<'a>,
112+
trait_item: &hir::TraitItem<'a>,
113+
avoid_breaking_exported_api: bool,
114+
) {
115+
if let hir::TraitItemKind::Fn(ref sig, _) = trait_item.kind
116+
&& !(avoid_breaking_exported_api && cx.effective_visibilities.is_exported(trait_item.owner_id.def_id))
117+
{
118+
let def_id = trait_item.owner_id.def_id;
119+
let ty_sig = cx.tcx.fn_sig(def_id).instantiate_identity().skip_binder();
120+
check_fn_sig(cx, sig.decl, sig.span, ty_sig);
121+
}
122+
}

tests/ui/ref_option/all/clippy.toml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
avoid-breaking-exported-api = false
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
avoid-breaking-exported-api = true
+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//@revisions: private all
2+
//@[private] rustc-env:CLIPPY_CONF_DIR=tests/ui/ref_option/private
3+
//@[all] rustc-env:CLIPPY_CONF_DIR=tests/ui/ref_option/all
4+
5+
#![allow(unused, clippy::needless_lifetimes, clippy::borrowed_box)]
6+
#![warn(clippy::ref_option)]
7+
8+
fn opt_u8(a: Option<&u8>) {}
9+
fn opt_gen<T>(a: Option<&T>) {}
10+
fn opt_string(a: std::option::Option<&String>) {}
11+
fn ret_string<'a>(p: &'a str) -> Option<&'a u8> {
12+
panic!()
13+
}
14+
fn ret_string_static() -> Option<&'static u8> {
15+
panic!()
16+
}
17+
fn mult_string(a: Option<&String>, b: Option<&Vec<u8>>) {}
18+
fn ret_box<'a>() -> Option<&'a Box<u8>> {
19+
panic!()
20+
}
21+
22+
pub fn pub_opt_string(a: Option<&String>) {}
23+
pub fn pub_mult_string(a: Option<&String>, b: Option<&Vec<u8>>) {}
24+
25+
pub trait PubTrait {
26+
fn pub_trait_opt(&self, a: Option<&Vec<u8>>);
27+
fn pub_trait_ret(&self) -> Option<&Vec<u8>>;
28+
}
29+
30+
trait PrivateTrait {
31+
fn trait_opt(&self, a: Option<&String>);
32+
fn trait_ret(&self) -> Option<&String>;
33+
}
34+
35+
pub struct PubStruct;
36+
37+
impl PubStruct {
38+
pub fn pub_opt_params(&self, a: Option<&()>) {}
39+
pub fn pub_opt_ret(&self) -> Option<&String> {
40+
panic!()
41+
}
42+
43+
fn private_opt_params(&self, a: Option<&()>) {}
44+
fn private_opt_ret(&self) -> Option<&String> {
45+
panic!()
46+
}
47+
}
48+
49+
// valid, don't change
50+
fn mut_u8(a: &mut Option<u8>) {}
51+
pub fn pub_mut_u8(a: &mut Option<String>) {}
52+
53+
// might be good to catch in the future
54+
fn mut_u8_ref(a: &mut &Option<u8>) {}
55+
pub fn pub_mut_u8_ref(a: &mut &Option<String>) {}
56+
fn lambdas() {
57+
// Not handled for now, not sure if we should
58+
let x = |a: &Option<String>| {};
59+
let x = |a: &Option<String>| -> &Option<String> { panic!() };
60+
}
61+
62+
fn main() {}

0 commit comments

Comments
 (0)