blob: b44497ded301eed650601496aabce264eff5fc96 [file]
use clippy_config::Conf;
use clippy_utils::diagnostics::span_lint_and_help;
use clippy_utils::{get_parent_as_impl, has_repr_attr, is_bool};
use rustc_abi::ExternAbi;
use rustc_hir::intravisit::FnKind;
use rustc_hir::{Body, FnDecl, Item, ItemKind, TraitFn, TraitItem, TraitItemKind, Ty};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::impl_lint_pass;
use rustc_span::Span;
use rustc_span::def_id::LocalDefId;
declare_clippy_lint! {
/// ### What it does
/// Checks for excessive use of bools in function declarations.
///
/// ### Why is this bad?
/// Boolean parameters obscure meaning at the call site. The reader has to
/// remember what each `true` or `false` means and which position each flag
/// belongs in, with no help from the type system.
///
/// The best replacement depends on what role the bool plays:
///
/// - Use a two-variant enum when one parameter selects a mode.
/// - Split one function into two named operations when the choices are
/// distinct actions.
/// - Group multiple related flags into a struct or options type when they
/// travel together.
/// - Move the setting onto `self` or an existing config/context parameter
/// when it is really part of the surrounding state.
///
/// This optimizes for readability of the calling code, which future readers
/// encounter more often than the function declaration.
///
/// The `max-fn-params-bools` configuration accepts `0`, which lints on any
/// function with a bool parameter.
///
/// ### Example
/// ```rust,ignore
/// fn render_document(show_hidden: bool, is_draft: bool) { ... }
/// ```
///
/// Use instead:
/// ```rust,ignore
/// enum Visibility {
/// ShowHidden,
/// HideHidden,
/// }
///
/// enum DocumentKind {
/// Draft,
/// Final,
/// }
///
/// fn render_document(visibility: Visibility, kind: DocumentKind) { ... }
///
/// // or split the operation when the choices are distinct
/// fn render_draft() { ... }
/// fn render_final() { ... }
/// ```
#[clippy::version = "1.43.0"]
pub FN_PARAMS_EXCESSIVE_BOOLS,
pedantic,
"using too many bools in function parameters"
}
declare_clippy_lint! {
/// ### What it does
/// Checks for excessive
/// use of bools in structs.
///
/// ### Why is this bad?
/// Excessive bools in a struct is often a sign that
/// the type is being used to represent a state
/// machine, which is much better implemented as an
/// enum.
///
/// The reason an enum is better for state machines
/// over structs is that enums more easily forbid
/// invalid states.
///
/// Structs with too many booleans may benefit from refactoring
/// into multi variant enums for better readability and API.
///
/// ### Example
/// ```no_run
/// struct S {
/// is_pending: bool,
/// is_processing: bool,
/// is_finished: bool,
/// }
/// ```
///
/// Use instead:
/// ```no_run
/// enum S {
/// Pending,
/// Processing,
/// Finished,
/// }
/// ```
#[clippy::version = "1.43.0"]
pub STRUCT_EXCESSIVE_BOOLS,
pedantic,
"using too many bools in a struct"
}
impl_lint_pass!(ExcessiveBools => [
FN_PARAMS_EXCESSIVE_BOOLS,
STRUCT_EXCESSIVE_BOOLS,
]);
pub struct ExcessiveBools {
max_struct_bools: u64,
max_fn_params_bools: u64,
}
impl ExcessiveBools {
pub fn new(conf: &'static Conf) -> Self {
Self {
max_struct_bools: conf.max_struct_bools,
max_fn_params_bools: conf.max_fn_params_bools,
}
}
}
fn has_n_bools<'tcx>(iter: impl Iterator<Item = &'tcx Ty<'tcx>>, mut count: u64) -> bool {
iter.filter(|ty| is_bool(ty)).any(|_| {
let (x, overflow) = count.overflowing_sub(1);
count = x;
overflow
})
}
fn check_fn_decl(cx: &LateContext<'_>, decl: &FnDecl<'_>, sp: Span, max: u64) {
if has_n_bools(decl.inputs.iter(), max) && !sp.from_expansion() {
span_lint_and_help(
cx,
FN_PARAMS_EXCESSIVE_BOOLS,
sp,
format!("more than {max} bools in function parameters"),
None,
"consider refactoring bools into two-variant enums",
);
}
}
impl<'tcx> LateLintPass<'tcx> for ExcessiveBools {
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
if let ItemKind::Struct(_, _, variant_data) = &item.kind
&& variant_data.fields().len() as u64 > self.max_struct_bools
&& has_n_bools(
variant_data.fields().iter().map(|field| field.ty),
self.max_struct_bools,
)
&& !has_repr_attr(cx, item.hir_id())
&& !item.span.from_expansion()
{
span_lint_and_help(
cx,
STRUCT_EXCESSIVE_BOOLS,
item.span,
format!("more than {} bools in a struct", self.max_struct_bools),
None,
"consider using a state machine or refactoring bools into two-variant enums",
);
}
}
fn check_trait_item(&mut self, cx: &LateContext<'tcx>, trait_item: &'tcx TraitItem<'tcx>) {
// functions with a body are already checked by `check_fn`
if let TraitItemKind::Fn(fn_sig, TraitFn::Required(_)) = &trait_item.kind
&& fn_sig.header.abi == ExternAbi::Rust
&& fn_sig.decl.inputs.len() as u64 > self.max_fn_params_bools
{
check_fn_decl(cx, fn_sig.decl, fn_sig.span, self.max_fn_params_bools);
}
}
fn check_fn(
&mut self,
cx: &LateContext<'tcx>,
fn_kind: FnKind<'tcx>,
fn_decl: &'tcx FnDecl<'tcx>,
_: &'tcx Body<'tcx>,
span: Span,
def_id: LocalDefId,
) {
if let Some(fn_header) = fn_kind.header()
&& fn_header.abi == ExternAbi::Rust
&& fn_decl.inputs.len() as u64 > self.max_fn_params_bools
&& get_parent_as_impl(cx.tcx, cx.tcx.local_def_id_to_hir_id(def_id))
.is_none_or(|impl_item| impl_item.of_trait.is_none())
{
check_fn_decl(cx, fn_decl, span, self.max_fn_params_bools);
}
}
}