Skip to content

Commit

Permalink
Auto merge of #12287 - Jarcho:issue_12250, r=llogiq
Browse files Browse the repository at this point in the history
Add lint `manual_inspect`

fixes #12250

A great example of a lint that sounds super simple, but has a pile of edge cases.

changelog: Add lint `manual_inspect`
  • Loading branch information
bors committed Jun 15, 2024
2 parents 3e5a02b + 404a6de commit debcef0
Show file tree
Hide file tree
Showing 16 changed files with 964 additions and 105 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5520,6 +5520,7 @@ Released 2018-09-13
[`manual_find_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_find_map
[`manual_flatten`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_flatten
[`manual_hash_one`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_hash_one
[`manual_inspect`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_inspect
[`manual_instant_elapsed`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_instant_elapsed
[`manual_is_ascii_check`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_ascii_check
[`manual_is_finite`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_finite
Expand Down
2 changes: 1 addition & 1 deletion clippy_config/src/msrvs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ macro_rules! msrv_aliases {
// names may refer to stabilized feature flags or library items
msrv_aliases! {
1,77,0 { C_STR_LITERALS }
1,76,0 { PTR_FROM_REF }
1,76,0 { PTR_FROM_REF, OPTION_RESULT_INSPECT }
1,71,0 { TUPLE_ARRAY_CONVERSIONS, BUILD_HASHER_HASH_ONE }
1,70,0 { OPTION_RESULT_IS_VARIANT_AND, BINARY_HEAP_RETAIN }
1,68,0 { PATH_MAIN_SEPARATOR_STR }
Expand Down
4 changes: 2 additions & 2 deletions clippy_lints/src/casts/ref_as_ptr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ pub(super) fn check<'tcx>(

if matches!(cast_from.kind(), ty::Ref(..))
&& let ty::RawPtr(_, to_mutbl) = cast_to.kind()
&& let Some(use_cx) = expr_use_ctxt(cx, expr)
&& let use_cx = expr_use_ctxt(cx, expr)
// TODO: only block the lint if `cast_expr` is a temporary
&& !matches!(use_cx.node, ExprUseNode::LetStmt(_) | ExprUseNode::ConstStatic(_))
&& !matches!(use_cx.use_node(cx), ExprUseNode::LetStmt(_) | ExprUseNode::ConstStatic(_))
{
let core_or_std = if is_no_std_crate(cx) { "core" } else { "std" };
let fn_name = match to_mutbl {
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 @@ -402,6 +402,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
crate::methods::MANUAL_C_STR_LITERALS_INFO,
crate::methods::MANUAL_FILTER_MAP_INFO,
crate::methods::MANUAL_FIND_MAP_INFO,
crate::methods::MANUAL_INSPECT_INFO,
crate::methods::MANUAL_IS_VARIANT_AND_INFO,
crate::methods::MANUAL_NEXT_BACK_INFO,
crate::methods::MANUAL_OK_OR_INFO,
Expand Down
46 changes: 20 additions & 26 deletions clippy_lints/src/dereference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,18 +260,13 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> {
(None, kind) => {
let expr_ty = typeck.expr_ty(expr);
let use_cx = expr_use_ctxt(cx, expr);
let adjusted_ty = match &use_cx {
Some(use_cx) => match use_cx.adjustments {
[.., a] => a.target,
_ => expr_ty,
},
_ => typeck.expr_ty_adjusted(expr),
};
let adjusted_ty = use_cx.adjustments.last().map_or(expr_ty, |a| a.target);

match (use_cx, kind) {
(Some(use_cx), RefOp::Deref) => {
match kind {
RefOp::Deref if use_cx.same_ctxt => {
let use_node = use_cx.use_node(cx);
let sub_ty = typeck.expr_ty(sub_expr);
if let ExprUseNode::FieldAccess(name) = use_cx.node
if let ExprUseNode::FieldAccess(name) = use_node
&& !use_cx.moved_before_use
&& !ty_contains_field(sub_ty, name.name)
{
Expand All @@ -288,9 +283,9 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> {
} else if sub_ty.is_ref()
// Linting method receivers would require verifying that name lookup
// would resolve the same way. This is complicated by trait methods.
&& !use_cx.node.is_recv()
&& let Some(ty) = use_cx.node.defined_ty(cx)
&& TyCoercionStability::for_defined_ty(cx, ty, use_cx.node.is_return()).is_deref_stable()
&& !use_node.is_recv()
&& let Some(ty) = use_node.defined_ty(cx)
&& TyCoercionStability::for_defined_ty(cx, ty, use_node.is_return()).is_deref_stable()
{
self.state = Some((
State::ExplicitDeref { mutability: None },
Expand All @@ -301,7 +296,7 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> {
));
}
},
(_, RefOp::Method { mutbl, is_ufcs })
RefOp::Method { mutbl, is_ufcs }
if !is_lint_allowed(cx, EXPLICIT_DEREF_METHODS, expr.hir_id)
// Allow explicit deref in method chains. e.g. `foo.deref().bar()`
&& (is_ufcs || !in_postfix_position(cx, expr)) =>
Expand All @@ -319,7 +314,7 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> {
},
));
},
(Some(use_cx), RefOp::AddrOf(mutability)) => {
RefOp::AddrOf(mutability) if use_cx.same_ctxt => {
// Find the number of times the borrow is auto-derefed.
let mut iter = use_cx.adjustments.iter();
let mut deref_count = 0usize;
Expand All @@ -338,10 +333,11 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> {
};
};

let stability = use_cx.node.defined_ty(cx).map_or(TyCoercionStability::None, |ty| {
TyCoercionStability::for_defined_ty(cx, ty, use_cx.node.is_return())
let use_node = use_cx.use_node(cx);
let stability = use_node.defined_ty(cx).map_or(TyCoercionStability::None, |ty| {
TyCoercionStability::for_defined_ty(cx, ty, use_node.is_return())
});
let can_auto_borrow = match use_cx.node {
let can_auto_borrow = match use_node {
ExprUseNode::FieldAccess(_)
if !use_cx.moved_before_use && matches!(sub_expr.kind, ExprKind::Field(..)) =>
{
Expand All @@ -353,7 +349,7 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> {
// deref through `ManuallyDrop<_>` will not compile.
!adjust_derefs_manually_drop(use_cx.adjustments, expr_ty)
},
ExprUseNode::Callee | ExprUseNode::FieldAccess(_) => true,
ExprUseNode::Callee | ExprUseNode::FieldAccess(_) if !use_cx.moved_before_use => true,
ExprUseNode::MethodArg(hir_id, _, 0) if !use_cx.moved_before_use => {
// Check for calls to trait methods where the trait is implemented
// on a reference.
Expand All @@ -363,9 +359,7 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> {
// priority.
if let Some(fn_id) = typeck.type_dependent_def_id(hir_id)
&& let Some(trait_id) = cx.tcx.trait_of_item(fn_id)
&& let arg_ty = cx
.tcx
.erase_regions(use_cx.adjustments.last().map_or(expr_ty, |a| a.target))
&& let arg_ty = cx.tcx.erase_regions(adjusted_ty)
&& let ty::Ref(_, sub_ty, _) = *arg_ty.kind()
&& let args =
typeck.node_args_opt(hir_id).map(|args| &args[1..]).unwrap_or_default()
Expand Down Expand Up @@ -443,7 +437,7 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> {
count: deref_count - required_refs,
msg,
stability,
for_field_access: if let ExprUseNode::FieldAccess(name) = use_cx.node
for_field_access: if let ExprUseNode::FieldAccess(name) = use_node
&& !use_cx.moved_before_use
{
Some(name.name)
Expand All @@ -453,7 +447,7 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> {
}),
StateData {
first_expr: expr,
adjusted_ty: use_cx.adjustments.last().map_or(expr_ty, |a| a.target),
adjusted_ty,
},
));
} else if stability.is_deref_stable()
Expand All @@ -465,12 +459,12 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> {
State::Borrow { mutability },
StateData {
first_expr: expr,
adjusted_ty: use_cx.adjustments.last().map_or(expr_ty, |a| a.target),
adjusted_ty,
},
));
}
},
(None, _) | (_, RefOp::Method { .. }) => (),
_ => {},
}
},
(
Expand Down
233 changes: 233 additions & 0 deletions clippy_lints/src/methods/manual_inspect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
use clippy_config::msrvs::{self, Msrv};
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::source::{get_source_text, with_leading_whitespace, SpanRange};
use clippy_utils::ty::get_field_by_name;
use clippy_utils::visitors::{for_each_expr, for_each_expr_with_closures};
use clippy_utils::{expr_use_ctxt, is_diag_item_method, is_diag_trait_item, path_to_local_id, ExprUseNode};
use core::ops::ControlFlow;
use rustc_errors::Applicability;
use rustc_hir::{BindingMode, BorrowKind, ByRef, ClosureKind, Expr, ExprKind, Mutability, Node, PatKind};
use rustc_lint::LateContext;
use rustc_middle::ty::adjustment::{Adjust, Adjustment, AutoBorrow, AutoBorrowMutability};
use rustc_span::{sym, BytePos, Span, Symbol, DUMMY_SP};

use super::MANUAL_INSPECT;

#[expect(clippy::too_many_lines)]
pub(crate) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &Expr<'_>, name: &str, name_span: Span, msrv: &Msrv) {
if let ExprKind::Closure(c) = arg.kind
&& matches!(c.kind, ClosureKind::Closure)
&& let typeck = cx.typeck_results()
&& let Some(fn_id) = typeck.type_dependent_def_id(expr.hir_id)
&& (is_diag_trait_item(cx, fn_id, sym::Iterator)
|| (msrv.meets(msrvs::OPTION_RESULT_INSPECT)
&& (is_diag_item_method(cx, fn_id, sym::Option) || is_diag_item_method(cx, fn_id, sym::Result))))
&& let body = cx.tcx.hir().body(c.body)
&& let [param] = body.params
&& let PatKind::Binding(BindingMode(ByRef::No, Mutability::Not), arg_id, _, None) = param.pat.kind
&& let arg_ty = typeck.node_type(arg_id)
&& let ExprKind::Block(block, _) = body.value.kind
&& let Some(final_expr) = block.expr
&& !block.stmts.is_empty()
&& path_to_local_id(final_expr, arg_id)
&& typeck.expr_adjustments(final_expr).is_empty()
{
let mut requires_copy = false;
let mut requires_deref = false;

// The number of unprocessed return expressions.
let mut ret_count = 0u32;

// The uses for which processing is delayed until after the visitor.
let mut delayed = vec![];

let ctxt = arg.span.ctxt();
let can_lint = for_each_expr(block.stmts, |e| {
if let ExprKind::Closure(c) = e.kind {
// Nested closures don't need to treat returns specially.
let _: Option<!> = for_each_expr_with_closures(cx, cx.tcx.hir().body(c.body).value, |e| {
if path_to_local_id(e, arg_id) {
let (kind, same_ctxt) = check_use(cx, e);
match (kind, same_ctxt && e.span.ctxt() == ctxt) {
(_, false) | (UseKind::Deref | UseKind::Return(..), true) => {
requires_copy = true;
requires_deref = true;
},
(UseKind::AutoBorrowed, true) => {},
(UseKind::WillAutoDeref, true) => {
requires_copy = true;
},
(kind, true) => delayed.push(kind),
}
}
ControlFlow::Continue(())
});
} else if matches!(e.kind, ExprKind::Ret(_)) {
ret_count += 1;
} else if path_to_local_id(e, arg_id) {
let (kind, same_ctxt) = check_use(cx, e);
match (kind, same_ctxt && e.span.ctxt() == ctxt) {
(UseKind::Return(..), false) => {
return ControlFlow::Break(());
},
(_, false) | (UseKind::Deref, true) => {
requires_copy = true;
requires_deref = true;
},
(UseKind::AutoBorrowed, true) => {},
(UseKind::WillAutoDeref, true) => {
requires_copy = true;
},
(kind @ UseKind::Return(_), true) => {
ret_count -= 1;
delayed.push(kind);
},
(kind, true) => delayed.push(kind),
}
}
ControlFlow::Continue(())
})
.is_none();

if ret_count != 0 {
// A return expression that didn't return the original value was found.
return;
}

let mut edits = Vec::with_capacity(delayed.len() + 3);
let mut addr_of_edits = Vec::with_capacity(delayed.len());
for x in delayed {
match x {
UseKind::Return(s) => edits.push((with_leading_whitespace(cx, s).set_span_pos(s), String::new())),
UseKind::Borrowed(s) => {
if let Some(src) = get_source_text(cx, s)
&& let Some(src) = src.as_str()
&& let trim_src = src.trim_start_matches(|x| matches!(x, ' ' | '\t' | '\n' | '\r' | '('))
&& trim_src.starts_with('&')
{
let range = s.into_range();
#[expect(clippy::cast_possible_truncation)]
let start = BytePos(range.start.0 + (src.len() - trim_src.len()) as u32);
addr_of_edits.push(((start..BytePos(start.0 + 1)).set_span_pos(s), String::new()));
} else {
requires_copy = true;
requires_deref = true;
}
},
UseKind::FieldAccess(name, e) => {
let Some(mut ty) = get_field_by_name(cx.tcx, arg_ty.peel_refs(), name) else {
requires_copy = true;
continue;
};
let mut prev_expr = e;

for (_, parent) in cx.tcx.hir().parent_iter(e.hir_id) {
if let Node::Expr(e) = parent {
match e.kind {
ExprKind::Field(_, name)
if let Some(fty) = get_field_by_name(cx.tcx, ty.peel_refs(), name.name) =>
{
ty = fty;
prev_expr = e;
continue;
},
ExprKind::AddrOf(BorrowKind::Ref, ..) => break,
_ if matches!(
typeck.expr_adjustments(prev_expr).first(),
Some(Adjustment {
kind: Adjust::Borrow(AutoBorrow::Ref(_, AutoBorrowMutability::Not))
| Adjust::Deref(_),
..
})
) =>
{
break;
},
_ => {},
}
}
requires_copy |= !ty.is_copy_modulo_regions(cx.tcx, cx.param_env);
break;
}
},
// Already processed uses.
UseKind::AutoBorrowed | UseKind::WillAutoDeref | UseKind::Deref => {},
}
}

if can_lint
&& (!requires_copy || arg_ty.is_copy_modulo_regions(cx.tcx, cx.param_env))
// This case could be handled, but a fair bit of care would need to be taken.
&& (!requires_deref || arg_ty.is_freeze(cx.tcx, cx.param_env))
{
if requires_deref {
edits.push((param.span.shrink_to_lo(), "&".into()));
} else {
edits.extend(addr_of_edits);
}
edits.push((
name_span,
String::from(match name {
"map" => "inspect",
"map_err" => "inspect_err",
_ => return,
}),
));
edits.push((
with_leading_whitespace(cx, final_expr.span).set_span_pos(final_expr.span),
String::new(),
));
let app = if edits.iter().any(|(s, _)| s.from_expansion()) {
Applicability::MaybeIncorrect
} else {
Applicability::MachineApplicable
};
span_lint_and_then(cx, MANUAL_INSPECT, name_span, "", |diag| {
diag.multipart_suggestion("try", edits, app);
});
}
}
}

enum UseKind<'tcx> {
AutoBorrowed,
WillAutoDeref,
Deref,
Return(Span),
Borrowed(Span),
FieldAccess(Symbol, &'tcx Expr<'tcx>),
}

/// Checks how the value is used, and whether it was used in the same `SyntaxContext`.
fn check_use<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) -> (UseKind<'tcx>, bool) {
let use_cx = expr_use_ctxt(cx, e);
if use_cx
.adjustments
.first()
.is_some_and(|a| matches!(a.kind, Adjust::Deref(_)))
{
return (UseKind::AutoBorrowed, use_cx.same_ctxt);
}
let res = match use_cx.use_node(cx) {
ExprUseNode::Return(_) => {
if let ExprKind::Ret(Some(e)) = use_cx.node.expect_expr().kind {
UseKind::Return(e.span)
} else {
return (UseKind::Return(DUMMY_SP), false);
}
},
ExprUseNode::FieldAccess(name) => UseKind::FieldAccess(name.name, use_cx.node.expect_expr()),
ExprUseNode::Callee | ExprUseNode::MethodArg(_, _, 0)
if use_cx
.adjustments
.first()
.is_some_and(|a| matches!(a.kind, Adjust::Borrow(AutoBorrow::Ref(_, AutoBorrowMutability::Not)))) =>
{
UseKind::AutoBorrowed
},
ExprUseNode::Callee | ExprUseNode::MethodArg(_, _, 0) => UseKind::WillAutoDeref,
ExprUseNode::AddrOf(BorrowKind::Ref, _) => UseKind::Borrowed(use_cx.node.expect_expr().span),
_ => UseKind::Deref,
};
(res, use_cx.same_ctxt)
}
Loading

0 comments on commit debcef0

Please sign in to comment.