Skip to content

Commit 09e4659

Browse files
committed
Auto merge of #9373 - lukaslueg:result_large_err, r=Alexendoo
Initial implementation `result_large_err` This is a shot at #6560, #4652, and #3884. The lint checks for `Result` being returned from functions/methods where the `Err` variant is larger than a configurable threshold (the default of which is 128 bytes). There has been some discussion around this, which I'll try to quickly summarize: * A large `Err`-variant may force an equally large `Result` if `Err` is actually bigger than `Ok`. * There is a cost involved in large `Result`, as LLVM may choose to `memcpy` them around above a certain size. * We usually expect the `Err` variant to be seldomly used, but pay the cost every time. * `Result` returned from library code has a high chance of bubbling up the call stack, getting stuffed into `MyLibError { IoError(std::io::Error), ParseError(parselib::Error), ...}`, exacerbating the problem. This PR deliberately does not take into account comparing the `Ok` to the `Err` variant (e.g. a ratio, or one being larger than the other). Rather we choose an absolute threshold for `Err`'s size, above which we warn. The reason for this is that `Err`s probably get `map_err`'ed further up the call stack, and we can't draw conclusions from the ratio at the point where the `Result` is returned. A relative threshold would also be less predictable, while not accounting for the cost of LLVM being forced to generate less efficient code if the `Err`-variant is _large_ in absolute terms. We lint private functions as well as public functions, as the perf-cost applies to in-crate code as well. In order to account for type-parameters, I conjured up `fn approx_ty_size`. The function relies on `LateContext::layout_of` to compute the actual size, and in case of failure (e.g. due to generics) tries to come up with an "at least size". In the latter case, the size of obviously wrong, but the inspected size certainly can't be smaller than that. Please give the approach a heavy dose of review, as I'm not actually familiar with the type-system at all (read: I have no idea what I'm doing). The approach does, however flimsy it is, allow us to successfully lint situations like ```rust pub union UnionError<T: Copy> { _maybe: T, _or_perhaps_even: (T, [u8; 512]), } // We know `UnionError<T>` will be at least 512 bytes, no matter what `T` is pub fn param_large_union<T: Copy>() -> Result<(), UnionError<T>> { Ok(()) } ``` I've given some refactoring to `functions/result_unit_err.rs` to re-use some bits. This is also the groundwork for #6409 The default threshold is 128 because of #4652 (comment) `lintcheck` does not trigger this lint for a threshold of 128. It does warn for 64, though. The suggestion currently is the following, which is just a placeholder for discussion to be had. I did have the computed size in a `span_label`. However, that might cause both ui-tests here and lints elsewhere to become flaky wrt to their output (as the size is platform dependent). ``` error: the `Err`-variant returned via this `Result` is very large --> $DIR/result_large_err.rs:36:34 | LL | pub fn param_large_error<R>() -> Result<(), (u128, R, FullyDefinedLargeError)> { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The `Err` variant is unusually large, at least 128 bytes ``` changelog: Add [`result_large_err`] lint
2 parents 4df6032 + 66a9705 commit 09e4659

13 files changed

+401
-71
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4025,6 +4025,7 @@ Released 2018-09-13
40254025
[`replace_consts`]: https://rust-lang.github.io/rust-clippy/master/index.html#replace_consts
40264026
[`rest_pat_in_fully_bound_structs`]: https://rust-lang.github.io/rust-clippy/master/index.html#rest_pat_in_fully_bound_structs
40274027
[`result_expect_used`]: https://rust-lang.github.io/rust-clippy/master/index.html#result_expect_used
4028+
[`result_large_err`]: https://rust-lang.github.io/rust-clippy/master/index.html#result_large_err
40284029
[`result_map_or_into_option`]: https://rust-lang.github.io/rust-clippy/master/index.html#result_map_or_into_option
40294030
[`result_map_unit_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#result_map_unit_fn
40304031
[`result_map_unwrap_or_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#result_map_unwrap_or_else

clippy_lints/src/functions/mod.rs

+51-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
mod must_use;
22
mod not_unsafe_ptr_arg_deref;
3-
mod result_unit_err;
3+
mod result;
44
mod too_many_arguments;
55
mod too_many_lines;
66

@@ -217,17 +217,62 @@ declare_clippy_lint! {
217217
"public function returning `Result` with an `Err` type of `()`"
218218
}
219219

220+
declare_clippy_lint! {
221+
/// ### What it does
222+
/// Checks for functions that return `Result` with an unusually large
223+
/// `Err`-variant.
224+
///
225+
/// ### Why is this bad?
226+
/// A `Result` is at least as large as the `Err`-variant. While we
227+
/// expect that variant to be seldomly used, the compiler needs to reserve
228+
/// and move that much memory every single time.
229+
///
230+
/// ### Known problems
231+
/// The size determined by Clippy is platform-dependent.
232+
///
233+
/// ### Examples
234+
/// ```rust
235+
/// pub enum ParseError {
236+
/// UnparsedBytes([u8; 512]),
237+
/// UnexpectedEof,
238+
/// }
239+
///
240+
/// // The `Result` has at least 512 bytes, even in the `Ok`-case
241+
/// pub fn parse() -> Result<(), ParseError> {
242+
/// Ok(())
243+
/// }
244+
/// ```
245+
/// should be
246+
/// ```
247+
/// pub enum ParseError {
248+
/// UnparsedBytes(Box<[u8; 512]>),
249+
/// UnexpectedEof,
250+
/// }
251+
///
252+
/// // The `Result` is slightly larger than a pointer
253+
/// pub fn parse() -> Result<(), ParseError> {
254+
/// Ok(())
255+
/// }
256+
/// ```
257+
#[clippy::version = "1.64.0"]
258+
pub RESULT_LARGE_ERR,
259+
perf,
260+
"function returning `Result` with large `Err` type"
261+
}
262+
220263
#[derive(Copy, Clone)]
221264
pub struct Functions {
222265
too_many_arguments_threshold: u64,
223266
too_many_lines_threshold: u64,
267+
large_error_threshold: u64,
224268
}
225269

226270
impl Functions {
227-
pub fn new(too_many_arguments_threshold: u64, too_many_lines_threshold: u64) -> Self {
271+
pub fn new(too_many_arguments_threshold: u64, too_many_lines_threshold: u64, large_error_threshold: u64) -> Self {
228272
Self {
229273
too_many_arguments_threshold,
230274
too_many_lines_threshold,
275+
large_error_threshold,
231276
}
232277
}
233278
}
@@ -240,6 +285,7 @@ impl_lint_pass!(Functions => [
240285
DOUBLE_MUST_USE,
241286
MUST_USE_CANDIDATE,
242287
RESULT_UNIT_ERR,
288+
RESULT_LARGE_ERR,
243289
]);
244290

245291
impl<'tcx> LateLintPass<'tcx> for Functions {
@@ -259,18 +305,18 @@ impl<'tcx> LateLintPass<'tcx> for Functions {
259305

260306
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) {
261307
must_use::check_item(cx, item);
262-
result_unit_err::check_item(cx, item);
308+
result::check_item(cx, item, self.large_error_threshold);
263309
}
264310

265311
fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::ImplItem<'_>) {
266312
must_use::check_impl_item(cx, item);
267-
result_unit_err::check_impl_item(cx, item);
313+
result::check_impl_item(cx, item, self.large_error_threshold);
268314
}
269315

270316
fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::TraitItem<'_>) {
271317
too_many_arguments::check_trait_item(cx, item, self.too_many_arguments_threshold);
272318
not_unsafe_ptr_arg_deref::check_trait_item(cx, item);
273319
must_use::check_trait_item(cx, item);
274-
result_unit_err::check_trait_item(cx, item);
320+
result::check_trait_item(cx, item, self.large_error_threshold);
275321
}
276322
}

clippy_lints/src/functions/result.rs

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use rustc_errors::Diagnostic;
2+
use rustc_hir as hir;
3+
use rustc_lint::{LateContext, LintContext};
4+
use rustc_middle::lint::in_external_macro;
5+
use rustc_middle::ty::{self, Ty};
6+
use rustc_span::{sym, Span};
7+
use rustc_typeck::hir_ty_to_ty;
8+
9+
use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_then};
10+
use clippy_utils::trait_ref_of_method;
11+
use clippy_utils::ty::{approx_ty_size, is_type_diagnostic_item};
12+
13+
use super::{RESULT_LARGE_ERR, RESULT_UNIT_ERR};
14+
15+
/// The type of the `Err`-variant in a `std::result::Result` returned by the
16+
/// given `FnDecl`
17+
fn result_err_ty<'tcx>(
18+
cx: &LateContext<'tcx>,
19+
decl: &hir::FnDecl<'tcx>,
20+
item_span: Span,
21+
) -> Option<(&'tcx hir::Ty<'tcx>, Ty<'tcx>)> {
22+
if !in_external_macro(cx.sess(), item_span)
23+
&& let hir::FnRetTy::Return(hir_ty) = decl.output
24+
&& let ty = hir_ty_to_ty(cx.tcx, hir_ty)
25+
&& is_type_diagnostic_item(cx, ty, sym::Result)
26+
&& let ty::Adt(_, substs) = ty.kind()
27+
{
28+
let err_ty = substs.type_at(1);
29+
Some((hir_ty, err_ty))
30+
} else {
31+
None
32+
}
33+
}
34+
35+
pub(super) fn check_item<'tcx>(cx: &LateContext<'tcx>, item: &hir::Item<'tcx>, large_err_threshold: u64) {
36+
if let hir::ItemKind::Fn(ref sig, _generics, _) = item.kind
37+
&& let Some((hir_ty, err_ty)) = result_err_ty(cx, sig.decl, item.span)
38+
{
39+
if cx.access_levels.is_exported(item.def_id) {
40+
let fn_header_span = item.span.with_hi(sig.decl.output.span().hi());
41+
check_result_unit_err(cx, err_ty, fn_header_span);
42+
}
43+
check_result_large_err(cx, err_ty, hir_ty.span, large_err_threshold);
44+
}
45+
}
46+
47+
pub(super) fn check_impl_item<'tcx>(cx: &LateContext<'tcx>, item: &hir::ImplItem<'tcx>, large_err_threshold: u64) {
48+
// Don't lint if method is a trait's implementation, we can't do anything about those
49+
if let hir::ImplItemKind::Fn(ref sig, _) = item.kind
50+
&& let Some((hir_ty, err_ty)) = result_err_ty(cx, sig.decl, item.span)
51+
&& trait_ref_of_method(cx, item.def_id).is_none()
52+
{
53+
if cx.access_levels.is_exported(item.def_id) {
54+
let fn_header_span = item.span.with_hi(sig.decl.output.span().hi());
55+
check_result_unit_err(cx, err_ty, fn_header_span);
56+
}
57+
check_result_large_err(cx, err_ty, hir_ty.span, large_err_threshold);
58+
}
59+
}
60+
61+
pub(super) fn check_trait_item<'tcx>(cx: &LateContext<'tcx>, item: &hir::TraitItem<'tcx>, large_err_threshold: u64) {
62+
if let hir::TraitItemKind::Fn(ref sig, _) = item.kind {
63+
let fn_header_span = item.span.with_hi(sig.decl.output.span().hi());
64+
if let Some((hir_ty, err_ty)) = result_err_ty(cx, sig.decl, item.span) {
65+
if cx.access_levels.is_exported(item.def_id) {
66+
check_result_unit_err(cx, err_ty, fn_header_span);
67+
}
68+
check_result_large_err(cx, err_ty, hir_ty.span, large_err_threshold);
69+
}
70+
}
71+
}
72+
73+
fn check_result_unit_err(cx: &LateContext<'_>, err_ty: Ty<'_>, fn_header_span: Span) {
74+
if err_ty.is_unit() {
75+
span_lint_and_help(
76+
cx,
77+
RESULT_UNIT_ERR,
78+
fn_header_span,
79+
"this returns a `Result<_, ()>`",
80+
None,
81+
"use a custom `Error` type instead",
82+
);
83+
}
84+
}
85+
86+
fn check_result_large_err<'tcx>(cx: &LateContext<'tcx>, err_ty: Ty<'tcx>, hir_ty_span: Span, large_err_threshold: u64) {
87+
let ty_size = approx_ty_size(cx, err_ty);
88+
if ty_size >= large_err_threshold {
89+
span_lint_and_then(
90+
cx,
91+
RESULT_LARGE_ERR,
92+
hir_ty_span,
93+
"the `Err`-variant returned from this function is very large",
94+
|diag: &mut Diagnostic| {
95+
diag.span_label(hir_ty_span, format!("the `Err`-variant is at least {ty_size} bytes"));
96+
diag.help(format!("try reducing the size of `{err_ty}`, for example by boxing large elements or replacing it with `Box<{err_ty}>`"));
97+
},
98+
);
99+
}
100+
}

clippy_lints/src/functions/result_unit_err.rs

-66
This file was deleted.

clippy_lints/src/lib.register_all.rs

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
8080
LintId::of(functions::DOUBLE_MUST_USE),
8181
LintId::of(functions::MUST_USE_UNIT),
8282
LintId::of(functions::NOT_UNSAFE_PTR_ARG_DEREF),
83+
LintId::of(functions::RESULT_LARGE_ERR),
8384
LintId::of(functions::RESULT_UNIT_ERR),
8485
LintId::of(functions::TOO_MANY_ARGUMENTS),
8586
LintId::of(if_let_mutex::IF_LET_MUTEX),

clippy_lints/src/lib.register_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ store.register_lints(&[
171171
functions::MUST_USE_CANDIDATE,
172172
functions::MUST_USE_UNIT,
173173
functions::NOT_UNSAFE_PTR_ARG_DEREF,
174+
functions::RESULT_LARGE_ERR,
174175
functions::RESULT_UNIT_ERR,
175176
functions::TOO_MANY_ARGUMENTS,
176177
functions::TOO_MANY_LINES,

clippy_lints/src/lib.register_perf.rs

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ store.register_group(true, "clippy::perf", Some("clippy_perf"), vec![
77
LintId::of(escape::BOXED_LOCAL),
88
LintId::of(format_args::FORMAT_IN_FORMAT_ARGS),
99
LintId::of(format_args::TO_STRING_IN_FORMAT_ARGS),
10+
LintId::of(functions::RESULT_LARGE_ERR),
1011
LintId::of(large_const_arrays::LARGE_CONST_ARRAYS),
1112
LintId::of(large_enum_variant::LARGE_ENUM_VARIANT),
1213
LintId::of(loops::MANUAL_MEMCPY),

clippy_lints/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -668,10 +668,12 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
668668
store.register_late_pass(move || Box::new(disallowed_names::DisallowedNames::new(disallowed_names.clone())));
669669
let too_many_arguments_threshold = conf.too_many_arguments_threshold;
670670
let too_many_lines_threshold = conf.too_many_lines_threshold;
671+
let large_error_threshold = conf.large_error_threshold;
671672
store.register_late_pass(move || {
672673
Box::new(functions::Functions::new(
673674
too_many_arguments_threshold,
674675
too_many_lines_threshold,
676+
large_error_threshold,
675677
))
676678
});
677679
let doc_valid_idents = conf.doc_valid_idents.iter().cloned().collect::<FxHashSet<_>>();

clippy_lints/src/utils/conf.rs

+4
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,10 @@ define_Conf! {
379379
///
380380
/// Whether `dbg!` should be allowed in test functions
381381
(allow_dbg_in_tests: bool = false),
382+
/// Lint: RESULT_LARGE_ERR
383+
///
384+
/// The maximum size of the `Err`-variant in a `Result` returned from a function
385+
(large_error_threshold: u64 = 128),
382386
}
383387

384388
/// Search for the configuration file.

clippy_utils/src/ty.rs

+50
Original file line numberDiff line numberDiff line change
@@ -831,3 +831,53 @@ pub fn ty_is_fn_once_param<'tcx>(tcx: TyCtxt<'_>, ty: Ty<'tcx>, predicates: &'tc
831831
})
832832
.unwrap_or(false)
833833
}
834+
835+
/// Comes up with an "at least" guesstimate for the type's size, not taking into
836+
/// account the layout of type parameters.
837+
pub fn approx_ty_size<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> u64 {
838+
use rustc_middle::ty::layout::LayoutOf;
839+
if !is_normalizable(cx, cx.param_env, ty) {
840+
return 0;
841+
}
842+
match (cx.layout_of(ty).map(|layout| layout.size.bytes()), ty.kind()) {
843+
(Ok(size), _) => size,
844+
(Err(_), ty::Tuple(list)) => list.as_substs().types().map(|t| approx_ty_size(cx, t)).sum(),
845+
(Err(_), ty::Array(t, n)) => {
846+
n.try_eval_usize(cx.tcx, cx.param_env).unwrap_or_default() * approx_ty_size(cx, *t)
847+
},
848+
(Err(_), ty::Adt(def, subst)) if def.is_struct() => def
849+
.variants()
850+
.iter()
851+
.map(|v| {
852+
v.fields
853+
.iter()
854+
.map(|field| approx_ty_size(cx, field.ty(cx.tcx, subst)))
855+
.sum::<u64>()
856+
})
857+
.sum(),
858+
(Err(_), ty::Adt(def, subst)) if def.is_enum() => def
859+
.variants()
860+
.iter()
861+
.map(|v| {
862+
v.fields
863+
.iter()
864+
.map(|field| approx_ty_size(cx, field.ty(cx.tcx, subst)))
865+
.sum::<u64>()
866+
})
867+
.max()
868+
.unwrap_or_default(),
869+
(Err(_), ty::Adt(def, subst)) if def.is_union() => def
870+
.variants()
871+
.iter()
872+
.map(|v| {
873+
v.fields
874+
.iter()
875+
.map(|field| approx_ty_size(cx, field.ty(cx.tcx, subst)))
876+
.max()
877+
.unwrap_or_default()
878+
})
879+
.max()
880+
.unwrap_or_default(),
881+
(Err(_), _) => 0,
882+
}
883+
}

tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ error: error reading Clippy's configuration file `$DIR/clippy.toml`: unknown fie
1919
enforced-import-renames
2020
enum-variant-name-threshold
2121
enum-variant-size-threshold
22+
large-error-threshold
2223
literal-representation-threshold
2324
max-fn-params-bools
2425
max-include-file-size

0 commit comments

Comments
 (0)