Skip to content

Commit

Permalink
new manual_option_as_slice lint
Browse files Browse the repository at this point in the history
  • Loading branch information
llogiq committed Jan 22, 2025
1 parent c024c13 commit c548460
Show file tree
Hide file tree
Showing 10 changed files with 424 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5764,6 +5764,7 @@ Released 2018-09-13
[`manual_non_exhaustive`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_non_exhaustive
[`manual_ok_err`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_ok_err
[`manual_ok_or`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_ok_or
[`manual_option_as_slice`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_option_as_slice
[`manual_pattern_char_comparison`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_pattern_char_comparison
[`manual_range_contains`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_range_contains
[`manual_range_patterns`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_range_patterns
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
crate::manual_let_else::MANUAL_LET_ELSE_INFO,
crate::manual_main_separator_str::MANUAL_MAIN_SEPARATOR_STR_INFO,
crate::manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE_INFO,
crate::manual_option_as_slice::MANUAL_OPTION_AS_SLICE_INFO,
crate::manual_range_patterns::MANUAL_RANGE_PATTERNS_INFO,
crate::manual_rem_euclid::MANUAL_REM_EUCLID_INFO,
crate::manual_retain::MANUAL_RETAIN_INFO,
Expand Down
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ mod manual_is_power_of_two;
mod manual_let_else;
mod manual_main_separator_str;
mod manual_non_exhaustive;
mod manual_option_as_slice;
mod manual_range_patterns;
mod manual_rem_euclid;
mod manual_retain;
Expand Down Expand Up @@ -974,5 +975,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
store.register_late_pass(move |_| Box::new(arbitrary_source_item_ordering::ArbitrarySourceItemOrdering::new(conf)));
store.register_late_pass(|_| Box::new(unneeded_struct_pattern::UnneededStructPattern));
store.register_late_pass(|_| Box::<unnecessary_semicolon::UnnecessarySemicolon>::default());
store.register_late_pass(|_| Box::new(manual_option_as_slice::ManualOptionAsSlice::new(conf)));
// add lints here, do not remove this comment, it's used in `new_lint`
}
219 changes: 219 additions & 0 deletions clippy_lints/src/manual_option_as_slice.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
use clippy_config::Conf;
use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg};
use clippy_utils::{is_none_arm, msrvs, peel_hir_expr_refs};
use rustc_errors::Applicability;
use rustc_hir::def::{DefKind, Res};
use rustc_hir::{Arm, Expr, ExprKind, LangItem, Pat, PatKind, QPath, is_range_literal};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty;
use rustc_session::impl_lint_pass;
use rustc_span::{Span, Symbol, sym};

declare_clippy_lint! {
/// ### What it does
/// This detects various manual reimplementations of `Option::as_slice`.
///
/// ### Why is this bad?
/// Those implementations are both more complex than calling `as_slice`
/// and unlike that incur a branch, pessimizing performance and leading
/// to more generated code.
///
/// ### Example
/// ```no_run
///# let opt = Some(1);
/// _ = opt.as_ref().map_or(&[][..], std::slice::from_ref);
/// _ = match opt.as_ref() {
/// Some(f) => std::slice::from_ref(f),
/// None => &[],
/// };
/// ```
/// Use instead:
/// ```no_run
///# let opt = Some(1);
/// _ = opt.as_slice();
/// _ = opt.as_slice();
/// ```
#[clippy::version = "1.85.0"]
pub MANUAL_OPTION_AS_SLICE,
complexity,
"manual `Option::as_slice`"
}

pub struct ManualOptionAsSlice {
msrv: msrvs::Msrv,
}

impl ManualOptionAsSlice {
pub fn new(conf: &Conf) -> Self {
Self {
msrv: conf.msrv.clone(),
}
}
}

impl_lint_pass!(ManualOptionAsSlice => [MANUAL_OPTION_AS_SLICE]);

impl LateLintPass<'_> for ManualOptionAsSlice {
extract_msrv_attr!(LateContext);

fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
let span = expr.span;
if span.from_expansion() || !self.msrv.meets(msrvs::OPTION_AS_SLICE) {
return;
}
match expr.kind {
ExprKind::Match(scrutinee, [arm1, arm2], _) => {
if is_none_arm(cx, arm2) && check_arms(cx, arm2, arm1)
|| is_none_arm(cx, arm1) && check_arms(cx, arm1, arm2)
{
check_as_ref(cx, scrutinee, span);
}
},
ExprKind::If(cond, then, Some(other)) => {
if let ExprKind::Let(let_expr) = cond.kind
&& let Some(binding) = extract_ident_from_some_pat(cx, let_expr.pat)
&& check_some_body(cx, binding, then)
&& is_empty_slice(cx, other.peel_blocks())
{
check_as_ref(cx, let_expr.init, span);
}
},
ExprKind::MethodCall(seg, callee, [], _) => {
if seg.ident.name.as_str() == "unwrap_or_default" {
check_map(cx, callee, span);
}
},
ExprKind::MethodCall(seg, callee, [or], _) => match seg.ident.name.as_str() {
"unwrap_or" => {
if is_empty_slice(cx, or) {
check_map(cx, callee, span);
}
},
"unwrap_or_else" => {
if returns_empty_slice(cx, or) {
check_map(cx, callee, span);
}
},
_ => {},
},
ExprKind::MethodCall(seg, callee, [or_else, map], _) => match seg.ident.name.as_str() {
"map_or" => {
if is_empty_slice(cx, or_else) && is_slice_from_ref(cx, map) {
check_as_ref(cx, callee, span);
}
},
"map_or_else" => {
if returns_empty_slice(cx, or_else) && is_slice_from_ref(cx, map) {
check_as_ref(cx, callee, span);
}
},
_ => {},
},
_ => {},
}
}
}

fn check_map(cx: &LateContext<'_>, map: &Expr<'_>, span: Span) {
if let ExprKind::MethodCall(seg, callee, [mapping], _) = map.kind
&& seg.ident.name == sym::map
&& is_slice_from_ref(cx, mapping)
{
check_as_ref(cx, callee, span);
}
}

fn check_as_ref(cx: &LateContext<'_>, expr: &Expr<'_>, span: Span) {
if let ExprKind::MethodCall(seg, callee, [], _) = expr.kind
&& seg.ident.name == sym::as_ref
&& let ty::Adt(adtdef, ..) = cx.typeck_results().expr_ty(callee).kind()
&& cx.tcx.is_diagnostic_item(sym::Option, adtdef.did())
{
if let Some(snippet) = clippy_utils::source::snippet_opt(cx, callee.span) {
span_lint_and_sugg(
cx,
MANUAL_OPTION_AS_SLICE,
span,
"use `Option::as_slice`",
"use",
format!("{snippet}.as_slice()"),
Applicability::MachineApplicable,
);
} else {
span_lint(cx, MANUAL_OPTION_AS_SLICE, span, "use `Option_as_slice`");
}
}
}

fn extract_ident_from_some_pat(cx: &LateContext<'_>, pat: &Pat<'_>) -> Option<Symbol> {
if let PatKind::TupleStruct(QPath::Resolved(None, path), [binding], _) = pat.kind
&& let Res::Def(DefKind::Ctor(..), def_id) = path.res
&& let PatKind::Binding(_mode, _hir_id, ident, _inner_pat) = binding.kind
&& clippy_utils::is_lang_item_or_ctor(cx, def_id, LangItem::OptionSome)
{
Some(ident.name)
} else {
None
}
}

/// Returns true if `expr` is `std::slice::from_ref(<name>)`. Used in `if let`s.
fn check_some_body(cx: &LateContext<'_>, name: Symbol, expr: &Expr<'_>) -> bool {
if let ExprKind::Call(slice_from_ref, [arg]) = expr.peel_blocks().kind
&& is_slice_from_ref(cx, slice_from_ref)
&& let ExprKind::Path(QPath::Resolved(None, path)) = arg.kind
&& let [seg] = path.segments
{
seg.ident.name == name
} else {
false
}
}

fn check_arms(cx: &LateContext<'_>, none_arm: &Arm<'_>, some_arm: &Arm<'_>) -> bool {
if none_arm.guard.is_none()
&& some_arm.guard.is_none()
&& is_empty_slice(cx, none_arm.body)
&& let Some(name) = extract_ident_from_some_pat(cx, some_arm.pat)
{
check_some_body(cx, name, some_arm.body)
} else {
false
}
}

fn returns_empty_slice(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
match expr.kind {
ExprKind::Path(_) => clippy_utils::is_path_diagnostic_item(cx, expr, sym::default_fn),
ExprKind::Closure(cl) => is_empty_slice(cx, cx.tcx.hir().body(cl.body).value),
_ => false,
}
}

/// Returns if expr returns an empty slice. If:
/// - An indexing operation to an empty array with a built-in range. `[][..]`
/// - An indexing operation with a zero-ended range. `expr[..0]`
/// - A reference to an empty array. `&[]`
/// - Or a call to `Default::default`.
fn is_empty_slice(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
let expr = peel_hir_expr_refs(expr.peel_blocks()).0;
match expr.kind {
ExprKind::Index(arr, range, _) => match arr.kind {
ExprKind::Array([]) => is_range_literal(range),
ExprKind::Array(_) => {
let Some(range) = clippy_utils::higher::Range::hir(range) else {
return false;
};
range.end.is_some_and(|e| clippy_utils::is_integer_const(cx, e, 0))
},
_ => false,
},
ExprKind::Array([]) => true,
ExprKind::Call(def, []) => clippy_utils::is_path_diagnostic_item(cx, def, sym::default_fn),
_ => false,
}
}

fn is_slice_from_ref(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
clippy_utils::is_expr_path_def_path(cx, expr, &["core", "slice", "raw", "from_ref"])
}
10 changes: 1 addition & 9 deletions clippy_lints/src/matches/match_as_ref.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::source::snippet_with_applicability;
use clippy_utils::{is_res_lang_ctor, path_res, peel_blocks};
use clippy_utils::{is_none_arm, is_res_lang_ctor, path_res, peel_blocks};
use rustc_errors::Applicability;
use rustc_hir::{Arm, BindingMode, ByRef, Expr, ExprKind, LangItem, Mutability, PatKind, QPath};
use rustc_lint::LateContext;
Expand Down Expand Up @@ -55,14 +55,6 @@ pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], expr:
}
}

// Checks if arm has the form `None => None`
fn is_none_arm(cx: &LateContext<'_>, arm: &Arm<'_>) -> bool {
matches!(
arm.pat.kind,
PatKind::Path(ref qpath) if is_res_lang_ctor(cx, cx.qpath_res(qpath, arm.pat.hir_id), LangItem::OptionNone)
)
}

// Checks if arm has the form `Some(ref v) => Some(v)` (checks for `ref` and `ref mut`)
fn is_ref_some_arm(cx: &LateContext<'_>, arm: &Arm<'_>) -> Option<Mutability> {
if let PatKind::TupleStruct(ref qpath, [first_pat, ..], _) = arm.pat.kind
Expand Down
8 changes: 8 additions & 0 deletions clippy_utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,14 @@ pub fn is_wild(pat: &Pat<'_>) -> bool {
matches!(pat.kind, PatKind::Wild)
}

// Checks if arm has the form `None => _`
pub fn is_none_arm(cx: &LateContext<'_>, arm: &Arm<'_>) -> bool {
matches!(
arm.pat.kind,
PatKind::Path(ref qpath) if is_res_lang_ctor(cx, cx.qpath_res(qpath, arm.pat.hir_id),OptionNone)
)
}

/// Checks if the given `QPath` belongs to a type alias.
pub fn is_ty_alias(qpath: &QPath<'_>) -> bool {
match *qpath {
Expand Down
1 change: 1 addition & 0 deletions clippy_utils/src/msrvs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ msrv_aliases! {
1,80,0 { BOX_INTO_ITER }
1,77,0 { C_STR_LITERALS }
1,76,0 { PTR_FROM_REF, OPTION_RESULT_INSPECT }
1,75,0 { OPTION_AS_SLICE }
1,74,0 { REPR_RUST }
1,73,0 { MANUAL_DIV_CEIL }
1,71,0 { TUPLE_ARRAY_CONVERSIONS, BUILD_HASHER_HASH_ONE }
Expand Down
62 changes: 62 additions & 0 deletions tests/ui/manual_option_as_slice.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#![warn(clippy::manual_option_as_slice)]
#![allow(clippy::redundant_closure, clippy::unwrap_or_default)]

fn check(x: Option<u32>) {
_ = x.as_slice();

_ = x.as_slice();

_ = x.as_slice();
//~^ manual_option_as_slice

_ = x.as_slice();
//~^ manual_option_as_slice

_ = x.as_slice();
//~^ manual_option_as_slice

_ = x.as_slice();
//~^ manual_option_as_slice

{
use std::slice::from_ref;
_ = x.as_slice();
//~^ manual_option_as_slice
}

// possible false positives
let y = x.as_ref();
_ = match y {
// as_ref outside
Some(f) => &[f][..],
None => &[][..],
};
_ = match x.as_ref() {
Some(f) => std::slice::from_ref(f),
None => &[0],
};
_ = match x.as_ref() {
Some(42) => &[23],
Some(f) => std::slice::from_ref(f),
None => &[],
};
let b = &[42];
_ = if let Some(_f) = x.as_ref() {
std::slice::from_ref(b)
} else {
&[]
};
_ = x.as_ref().map_or(&[42][..], std::slice::from_ref);
_ = x.as_ref().map_or_else(|| &[42][..1], std::slice::from_ref);
_ = x.as_ref().map(|f| std::slice::from_ref(f)).unwrap_or_default();
}

#[clippy::msrv = "1.74"]
fn check_msrv(x: Option<u32>) {
_ = x.as_ref().map_or(&[][..], std::slice::from_ref);
}

fn main() {
check(Some(1));
check_msrv(Some(175));
}
Loading

0 comments on commit c548460

Please sign in to comment.