about summary refs log tree commit diff
path: root/clippy_utils
diff options
context:
space:
mode:
authorPhilipp Krones <hello@philkrones.com>2023-07-28 23:40:04 +0200
committerPhilipp Krones <hello@philkrones.com>2023-07-28 23:44:28 +0200
commit3d6024184108c584be87d32849dc0adacaa47770 (patch)
tree4df1883f983752de4d4e33bc6a28add00fefcf4c /clippy_utils
parent42d3370d84fd8503117a799ad6bb063a70656c9b (diff)
parentd3c5b488db292e139642a1eacffebc894b43b478 (diff)
Merge remote-tracking branch 'upstream/master' into rustup
Diffstat (limited to 'clippy_utils')
-rw-r--r--clippy_utils/src/diagnostics.rs4
-rw-r--r--clippy_utils/src/higher.rs1
-rw-r--r--clippy_utils/src/hir_utils.rs25
-rw-r--r--clippy_utils/src/lib.rs282
-rw-r--r--clippy_utils/src/mir/possible_origin.rs2
-rw-r--r--clippy_utils/src/paths.rs4
-rw-r--r--clippy_utils/src/qualify_min_const_fn.rs2
-rw-r--r--clippy_utils/src/ty.rs9
-rw-r--r--clippy_utils/src/ty/type_certainty/certainty.rs122
-rw-r--r--clippy_utils/src/ty/type_certainty/mod.rs315
-rw-r--r--clippy_utils/src/usage.rs3
11 files changed, 746 insertions, 23 deletions
diff --git a/clippy_utils/src/diagnostics.rs b/clippy_utils/src/diagnostics.rs
index edd87546a5f..210957a916f 100644
--- a/clippy_utils/src/diagnostics.rs
+++ b/clippy_utils/src/diagnostics.rs
@@ -83,9 +83,9 @@ pub fn span_lint_and_help<T: LintContext>(
     cx.struct_span_lint(lint, span, msg.to_string(), |diag| {
         let help = help.to_string();
         if let Some(help_span) = help_span {
-            diag.span_help(help_span, help.to_string());
+            diag.span_help(help_span, help);
         } else {
-            diag.help(help.to_string());
+            diag.help(help);
         }
         docs_link(diag, lint);
         diag
diff --git a/clippy_utils/src/higher.rs b/clippy_utils/src/higher.rs
index 357ac40b455..802adbd4d2d 100644
--- a/clippy_utils/src/higher.rs
+++ b/clippy_utils/src/higher.rs
@@ -138,6 +138,7 @@ impl<'hir> IfLet<'hir> {
 }
 
 /// An `if let` or `match` expression. Useful for lints that trigger on one or the other.
+#[derive(Debug)]
 pub enum IfLetOrMatch<'hir> {
     /// Any `match` expression
     Match(&'hir Expr<'hir>, &'hir [Arm<'hir>], MatchSource),
diff --git a/clippy_utils/src/hir_utils.rs b/clippy_utils/src/hir_utils.rs
index fb359ee3bbe..85b3b005f93 100644
--- a/clippy_utils/src/hir_utils.rs
+++ b/clippy_utils/src/hir_utils.rs
@@ -252,15 +252,15 @@ impl HirEqInterExpr<'_, '_, '_> {
             return false;
         }
 
-        if let Some((typeck_lhs, typeck_rhs)) = self.inner.maybe_typeck_results {
-            if let (Some(l), Some(r)) = (
+        if let Some((typeck_lhs, typeck_rhs)) = self.inner.maybe_typeck_results
+            && typeck_lhs.expr_ty(left) == typeck_rhs.expr_ty(right)
+            && let (Some(l), Some(r)) = (
                 constant_simple(self.inner.cx, typeck_lhs, left),
                 constant_simple(self.inner.cx, typeck_rhs, right),
-            ) {
-                if l == r {
-                    return true;
-                }
-            }
+            )
+            && l == r
+        {
+            return true;
         }
 
         let is_eq = match (
@@ -494,10 +494,13 @@ impl HirEqInterExpr<'_, '_, '_> {
             loop {
                 use TokenKind::{BlockComment, LineComment, Whitespace};
                 if left_data.macro_def_id != right_data.macro_def_id
-                    || (matches!(left_data.kind, ExpnKind::Macro(MacroKind::Bang, name) if name == sym::cfg)
-                        && !eq_span_tokens(self.inner.cx, left_data.call_site, right_data.call_site, |t| {
-                            !matches!(t, Whitespace | LineComment { .. } | BlockComment { .. })
-                        }))
+                    || (matches!(
+                        left_data.kind,
+                        ExpnKind::Macro(MacroKind::Bang, name)
+                        if name == sym::cfg || name == sym::option_env
+                    ) && !eq_span_tokens(self.inner.cx, left_data.call_site, right_data.call_site, |t| {
+                        !matches!(t, Whitespace | LineComment { .. } | BlockComment { .. })
+                    }))
                 {
                     // Either a different chain of macro calls, or different arguments to the `cfg` macro.
                     return false;
diff --git a/clippy_utils/src/lib.rs b/clippy_utils/src/lib.rs
index 035511e8912..d3a219f5326 100644
--- a/clippy_utils/src/lib.rs
+++ b/clippy_utils/src/lib.rs
@@ -89,21 +89,21 @@ use rustc_hir::intravisit::{walk_expr, FnKind, Visitor};
 use rustc_hir::LangItem::{OptionNone, OptionSome, ResultErr, ResultOk};
 use rustc_hir::{
     self as hir, def, Arm, ArrayLen, BindingAnnotation, Block, BlockCheckMode, Body, Closure, Destination, Expr,
-    ExprKind, FnDecl, HirId, Impl, ImplItem, ImplItemKind, ImplItemRef, IsAsync, Item, ItemKind, LangItem, Local,
-    MatchSource, Mutability, Node, OwnerId, Param, Pat, PatKind, Path, PathSegment, PrimTy, QPath, Stmt, StmtKind,
-    TraitItem, TraitItemRef, TraitRef, TyKind, UnOp,
+    ExprField, ExprKind, FnDecl, FnRetTy, GenericArgs, HirId, Impl, ImplItem, ImplItemKind, ImplItemRef, IsAsync, Item,
+    ItemKind, LangItem, Local, MatchSource, Mutability, Node, OwnerId, Param, Pat, PatKind, Path, PathSegment, PrimTy,
+    QPath, Stmt, StmtKind, TraitItem, TraitItemKind, TraitItemRef, TraitRef, TyKind, UnOp,
 };
 use rustc_lexer::{tokenize, TokenKind};
 use rustc_lint::{LateContext, Level, Lint, LintContext};
 use rustc_middle::hir::place::PlaceBase;
 use rustc_middle::mir::ConstantKind;
-use rustc_middle::ty as rustc_ty;
 use rustc_middle::ty::adjustment::{Adjust, Adjustment, AutoBorrow};
 use rustc_middle::ty::binding::BindingMode;
 use rustc_middle::ty::fast_reject::SimplifiedType;
 use rustc_middle::ty::layout::IntegerExt;
 use rustc_middle::ty::{
-    BorrowKind, ClosureKind, FloatTy, IntTy, Ty, TyCtxt, TypeAndMut, TypeVisitableExt, UintTy, UpvarCapture,
+    self as rustc_ty, Binder, BorrowKind, ClosureKind, FloatTy, IntTy, ParamEnv, ParamEnvAnd, Ty, TyCtxt, TypeAndMut,
+    TypeVisitableExt, UintTy, UpvarCapture,
 };
 use rustc_span::hygiene::{ExpnKind, MacroKind};
 use rustc_span::source_map::SourceMap;
@@ -113,7 +113,10 @@ use rustc_target::abi::Integer;
 
 use crate::consts::{constant, miri_to_const, Constant};
 use crate::higher::Range;
-use crate::ty::{can_partially_move_ty, expr_sig, is_copy, is_recursively_primitive_type, ty_is_fn_once_param};
+use crate::ty::{
+    adt_and_variant_of_res, can_partially_move_ty, expr_sig, is_copy, is_recursively_primitive_type,
+    ty_is_fn_once_param,
+};
 use crate::visitors::for_each_expr;
 
 use rustc_middle::hir::nested_filter;
@@ -2445,6 +2448,17 @@ pub fn is_in_cfg_test(tcx: TyCtxt<'_>, id: hir::HirId) -> bool {
         .any(is_cfg_test)
 }
 
+/// Checks if the item of any of its parents has `#[cfg(...)]` attribute applied.
+pub fn inherits_cfg(tcx: TyCtxt<'_>, def_id: LocalDefId) -> bool {
+    let hir = tcx.hir();
+
+    tcx.has_attr(def_id, sym::cfg)
+        || hir
+            .parent_iter(hir.local_def_id_to_hir_id(def_id))
+            .flat_map(|(parent_id, _)| hir.attrs(parent_id))
+            .any(|attr| attr.has_name(sym::cfg))
+}
+
 /// Checks whether item either has `test` attribute applied, or
 /// is a module with `test` in its name.
 ///
@@ -2499,6 +2513,262 @@ pub fn walk_to_expr_usage<'tcx, T>(
     None
 }
 
+/// A type definition as it would be viewed from within a function.
+#[derive(Clone, Copy)]
+pub enum DefinedTy<'tcx> {
+    // Used for locals and closures defined within the function.
+    Hir(&'tcx hir::Ty<'tcx>),
+    /// Used for function signatures, and constant and static values. This includes the `ParamEnv`
+    /// from the definition site.
+    Mir(ParamEnvAnd<'tcx, Binder<'tcx, Ty<'tcx>>>),
+}
+
+/// The context an expressions value is used in.
+pub struct ExprUseCtxt<'tcx> {
+    /// The parent node which consumes the value.
+    pub node: ExprUseNode<'tcx>,
+    /// Any adjustments applied to the type.
+    pub adjustments: &'tcx [Adjustment<'tcx>],
+    /// Whether or not the type must unify with another code path.
+    pub is_ty_unified: bool,
+    /// Whether or not the value will be moved before it's used.
+    pub moved_before_use: bool,
+}
+
+/// The node which consumes a value.
+pub enum ExprUseNode<'tcx> {
+    /// Assignment to, or initializer for, a local
+    Local(&'tcx Local<'tcx>),
+    /// Initializer for a const or static item.
+    ConstStatic(OwnerId),
+    /// Implicit or explicit return from a function.
+    Return(OwnerId),
+    /// Initialization of a struct field.
+    Field(&'tcx ExprField<'tcx>),
+    /// An argument to a function.
+    FnArg(&'tcx Expr<'tcx>, usize),
+    /// An argument to a method.
+    MethodArg(HirId, Option<&'tcx GenericArgs<'tcx>>, usize),
+    /// The callee of a function call.
+    Callee,
+    /// Access of a field.
+    FieldAccess(Ident),
+}
+impl<'tcx> ExprUseNode<'tcx> {
+    /// Checks if the value is returned from the function.
+    pub fn is_return(&self) -> bool {
+        matches!(self, Self::Return(_))
+    }
+
+    /// Checks if the value is used as a method call receiver.
+    pub fn is_recv(&self) -> bool {
+        matches!(self, Self::MethodArg(_, _, 0))
+    }
+
+    /// Gets the needed type as it's defined without any type inference.
+    pub fn defined_ty(&self, cx: &LateContext<'tcx>) -> Option<DefinedTy<'tcx>> {
+        match *self {
+            Self::Local(Local { ty: Some(ty), .. }) => Some(DefinedTy::Hir(ty)),
+            Self::ConstStatic(id) => Some(DefinedTy::Mir(
+                cx.param_env
+                    .and(Binder::dummy(cx.tcx.type_of(id).instantiate_identity())),
+            )),
+            Self::Return(id) => {
+                let hir_id = cx.tcx.hir().local_def_id_to_hir_id(id.def_id);
+                if let Some(Node::Expr(Expr {
+                    kind: ExprKind::Closure(c),
+                    ..
+                })) = cx.tcx.hir().find(hir_id)
+                {
+                    match c.fn_decl.output {
+                        FnRetTy::DefaultReturn(_) => None,
+                        FnRetTy::Return(ty) => Some(DefinedTy::Hir(ty)),
+                    }
+                } else {
+                    Some(DefinedTy::Mir(
+                        cx.param_env.and(cx.tcx.fn_sig(id).instantiate_identity().output()),
+                    ))
+                }
+            },
+            Self::Field(field) => match get_parent_expr_for_hir(cx, field.hir_id) {
+                Some(Expr {
+                    hir_id,
+                    kind: ExprKind::Struct(path, ..),
+                    ..
+                }) => adt_and_variant_of_res(cx, cx.qpath_res(path, *hir_id))
+                    .and_then(|(adt, variant)| {
+                        variant
+                            .fields
+                            .iter()
+                            .find(|f| f.name == field.ident.name)
+                            .map(|f| (adt, f))
+                    })
+                    .map(|(adt, field_def)| {
+                        DefinedTy::Mir(
+                            cx.tcx
+                                .param_env(adt.did())
+                                .and(Binder::dummy(cx.tcx.type_of(field_def.did).instantiate_identity())),
+                        )
+                    }),
+                _ => None,
+            },
+            Self::FnArg(callee, i) => {
+                let sig = expr_sig(cx, callee)?;
+                let (hir_ty, ty) = sig.input_with_hir(i)?;
+                Some(match hir_ty {
+                    Some(hir_ty) => DefinedTy::Hir(hir_ty),
+                    None => DefinedTy::Mir(
+                        sig.predicates_id()
+                            .map_or(ParamEnv::empty(), |id| cx.tcx.param_env(id))
+                            .and(ty),
+                    ),
+                })
+            },
+            Self::MethodArg(id, _, i) => {
+                let id = cx.typeck_results().type_dependent_def_id(id)?;
+                let sig = cx.tcx.fn_sig(id).skip_binder();
+                Some(DefinedTy::Mir(cx.tcx.param_env(id).and(sig.input(i))))
+            },
+            Self::Local(_) | Self::FieldAccess(..) | Self::Callee => None,
+        }
+    }
+}
+
+/// Gets the context an expression's value is used in.
+#[expect(clippy::too_many_lines)]
+pub fn expr_use_ctxt<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'tcx>) -> Option<ExprUseCtxt<'tcx>> {
+    let mut adjustments = [].as_slice();
+    let mut is_ty_unified = false;
+    let mut moved_before_use = false;
+    let ctxt = e.span.ctxt();
+    walk_to_expr_usage(cx, e, &mut |parent, child_id| {
+        // LocalTableInContext returns the wrong lifetime, so go use `expr_adjustments` instead.
+        if adjustments.is_empty() && let Node::Expr(e) = cx.tcx.hir().get(child_id) {
+            adjustments = cx.typeck_results().expr_adjustments(e);
+        }
+        match parent {
+            Node::Local(l) if l.span.ctxt() == ctxt => Some(ExprUseCtxt {
+                node: ExprUseNode::Local(l),
+                adjustments,
+                is_ty_unified,
+                moved_before_use,
+            }),
+            Node::Item(&Item {
+                kind: ItemKind::Static(..) | ItemKind::Const(..),
+                owner_id,
+                span,
+                ..
+            })
+            | Node::TraitItem(&TraitItem {
+                kind: TraitItemKind::Const(..),
+                owner_id,
+                span,
+                ..
+            })
+            | Node::ImplItem(&ImplItem {
+                kind: ImplItemKind::Const(..),
+                owner_id,
+                span,
+                ..
+            }) if span.ctxt() == ctxt => Some(ExprUseCtxt {
+                node: ExprUseNode::ConstStatic(owner_id),
+                adjustments,
+                is_ty_unified,
+                moved_before_use,
+            }),
+
+            Node::Item(&Item {
+                kind: ItemKind::Fn(..),
+                owner_id,
+                span,
+                ..
+            })
+            | Node::TraitItem(&TraitItem {
+                kind: TraitItemKind::Fn(..),
+                owner_id,
+                span,
+                ..
+            })
+            | Node::ImplItem(&ImplItem {
+                kind: ImplItemKind::Fn(..),
+                owner_id,
+                span,
+                ..
+            }) if span.ctxt() == ctxt => Some(ExprUseCtxt {
+                node: ExprUseNode::Return(owner_id),
+                adjustments,
+                is_ty_unified,
+                moved_before_use,
+            }),
+
+            Node::ExprField(field) if field.span.ctxt() == ctxt => Some(ExprUseCtxt {
+                node: ExprUseNode::Field(field),
+                adjustments,
+                is_ty_unified,
+                moved_before_use,
+            }),
+
+            Node::Expr(parent) if parent.span.ctxt() == ctxt => match parent.kind {
+                ExprKind::Ret(_) => Some(ExprUseCtxt {
+                    node: ExprUseNode::Return(OwnerId {
+                        def_id: cx.tcx.hir().body_owner_def_id(cx.enclosing_body.unwrap()),
+                    }),
+                    adjustments,
+                    is_ty_unified,
+                    moved_before_use,
+                }),
+                ExprKind::Closure(closure) => Some(ExprUseCtxt {
+                    node: ExprUseNode::Return(OwnerId { def_id: closure.def_id }),
+                    adjustments,
+                    is_ty_unified,
+                    moved_before_use,
+                }),
+                ExprKind::Call(func, args) => Some(ExprUseCtxt {
+                    node: match args.iter().position(|arg| arg.hir_id == child_id) {
+                        Some(i) => ExprUseNode::FnArg(func, i),
+                        None => ExprUseNode::Callee,
+                    },
+                    adjustments,
+                    is_ty_unified,
+                    moved_before_use,
+                }),
+                ExprKind::MethodCall(name, _, args, _) => Some(ExprUseCtxt {
+                    node: ExprUseNode::MethodArg(
+                        parent.hir_id,
+                        name.args,
+                        args.iter().position(|arg| arg.hir_id == child_id).map_or(0, |i| i + 1),
+                    ),
+                    adjustments,
+                    is_ty_unified,
+                    moved_before_use,
+                }),
+                ExprKind::Field(child, name) if child.hir_id == e.hir_id => Some(ExprUseCtxt {
+                    node: ExprUseNode::FieldAccess(name),
+                    adjustments,
+                    is_ty_unified,
+                    moved_before_use,
+                }),
+                ExprKind::If(e, _, _) | ExprKind::Match(e, _, _) if e.hir_id != child_id => {
+                    is_ty_unified = true;
+                    moved_before_use = true;
+                    None
+                },
+                ExprKind::Block(_, Some(_)) | ExprKind::Break(..) => {
+                    is_ty_unified = true;
+                    moved_before_use = true;
+                    None
+                },
+                ExprKind::Block(..) => {
+                    moved_before_use = true;
+                    None
+                },
+                _ => None,
+            },
+            _ => None,
+        }
+    })
+}
+
 /// Tokenizes the input while keeping the text associated with each token.
 pub fn tokenize_with_text(s: &str) -> impl Iterator<Item = (TokenKind, &str)> {
     let mut pos = 0;
diff --git a/clippy_utils/src/mir/possible_origin.rs b/clippy_utils/src/mir/possible_origin.rs
index 8e7513d740a..da04266863f 100644
--- a/clippy_utils/src/mir/possible_origin.rs
+++ b/clippy_utils/src/mir/possible_origin.rs
@@ -44,7 +44,7 @@ impl<'a, 'tcx> mir::visit::Visitor<'tcx> for PossibleOriginVisitor<'a, 'tcx> {
         let lhs = place.local;
         match rvalue {
             // Only consider `&mut`, which can modify origin place
-            mir::Rvalue::Ref(_, rustc_middle::mir::BorrowKind::Mut { .. }, borrowed) |
+            mir::Rvalue::Ref(_, mir::BorrowKind::Mut { .. }, borrowed) |
             // _2: &mut _;
             // _3 = move _2
             mir::Rvalue::Use(mir::Operand::Move(borrowed))  |
diff --git a/clippy_utils/src/paths.rs b/clippy_utils/src/paths.rs
index f3677e6f614..914ea85ac28 100644
--- a/clippy_utils/src/paths.rs
+++ b/clippy_utils/src/paths.rs
@@ -149,6 +149,7 @@ pub const VEC_AS_SLICE: [&str; 4] = ["alloc", "vec", "Vec", "as_slice"];
 pub const VEC_DEQUE_ITER: [&str; 5] = ["alloc", "collections", "vec_deque", "VecDeque", "iter"];
 pub const VEC_FROM_ELEM: [&str; 3] = ["alloc", "vec", "from_elem"];
 pub const VEC_NEW: [&str; 4] = ["alloc", "vec", "Vec", "new"];
+pub const VEC_WITH_CAPACITY: [&str; 4] = ["alloc", "vec", "Vec", "with_capacity"];
 pub const VEC_RESIZE: [&str; 4] = ["alloc", "vec", "Vec", "resize"];
 pub const WEAK_ARC: [&str; 3] = ["alloc", "sync", "Weak"];
 pub const WEAK_RC: [&str; 3] = ["alloc", "rc", "Weak"];
@@ -161,3 +162,6 @@ pub const OPTION_UNWRAP: [&str; 4] = ["core", "option", "Option", "unwrap"];
 pub const OPTION_EXPECT: [&str; 4] = ["core", "option", "Option", "expect"];
 pub const FORMATTER: [&str; 3] = ["core", "fmt", "Formatter"];
 pub const DEBUG_STRUCT: [&str; 4] = ["core", "fmt", "builders", "DebugStruct"];
+pub const ORD_CMP: [&str; 4] = ["core", "cmp", "Ord", "cmp"];
+#[expect(clippy::invalid_paths)] // not sure why it thinks this, it works so
+pub const BOOL_THEN: [&str; 4] = ["core", "bool", "<impl bool>", "then"];
diff --git a/clippy_utils/src/qualify_min_const_fn.rs b/clippy_utils/src/qualify_min_const_fn.rs
index e563e41ab2a..7a7ed41e527 100644
--- a/clippy_utils/src/qualify_min_const_fn.rs
+++ b/clippy_utils/src/qualify_min_const_fn.rs
@@ -14,7 +14,7 @@ use rustc_middle::mir::{
     Body, CastKind, NonDivergingIntrinsic, NullOp, Operand, Place, ProjectionElem, Rvalue, Statement, StatementKind,
     Terminator, TerminatorKind,
 };
-use rustc_middle::traits::{ImplSource, ObligationCause, BuiltinImplSource};
+use rustc_middle::traits::{BuiltinImplSource, ImplSource, ObligationCause};
 use rustc_middle::ty::adjustment::PointerCoercion;
 use rustc_middle::ty::{self, BoundConstness, GenericArgKind, TraitRef, Ty, TyCtxt};
 use rustc_semver::RustcVersion;
diff --git a/clippy_utils/src/ty.rs b/clippy_utils/src/ty.rs
index fd39a246f48..44ec8813f19 100644
--- a/clippy_utils/src/ty.rs
+++ b/clippy_utils/src/ty.rs
@@ -28,6 +28,9 @@ use std::iter;
 
 use crate::{match_def_path, path_res, paths};
 
+mod type_certainty;
+pub use type_certainty::expr_type_is_certain;
+
 /// Checks if the given type implements copy.
 pub fn is_copy<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool {
     ty.is_copy_modulo_regions(cx.tcx, cx.param_env)
@@ -739,7 +742,11 @@ fn sig_for_projection<'tcx>(cx: &LateContext<'tcx>, ty: AliasTy<'tcx>) -> Option
     let mut output = None;
     let lang_items = cx.tcx.lang_items();
 
-    for (pred, _) in cx.tcx.explicit_item_bounds(ty.def_id).iter_instantiated_copied(cx.tcx, ty.args) {
+    for (pred, _) in cx
+        .tcx
+        .explicit_item_bounds(ty.def_id)
+        .iter_instantiated_copied(cx.tcx, ty.args)
+    {
         match pred.kind().skip_binder() {
             ty::ClauseKind::Trait(p)
                 if (lang_items.fn_trait() == Some(p.def_id())
diff --git a/clippy_utils/src/ty/type_certainty/certainty.rs b/clippy_utils/src/ty/type_certainty/certainty.rs
new file mode 100644
index 00000000000..0e69ffa2212
--- /dev/null
+++ b/clippy_utils/src/ty/type_certainty/certainty.rs
@@ -0,0 +1,122 @@
+use rustc_hir::def_id::DefId;
+use std::fmt::Debug;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Certainty {
+    /// Determining the type requires contextual information.
+    Uncertain,
+
+    /// The type can be determined purely from subexpressions. If the argument is `Some(..)`, the
+    /// specific `DefId` is known. Such arguments are needed to handle path segments whose `res` is
+    /// `Res::Err`.
+    Certain(Option<DefId>),
+
+    /// The heuristic believes that more than one `DefId` applies to a type---this is a bug.
+    Contradiction,
+}
+
+pub trait Meet {
+    fn meet(self, other: Self) -> Self;
+}
+
+pub trait TryJoin: Sized {
+    fn try_join(self, other: Self) -> Option<Self>;
+}
+
+impl Meet for Option<DefId> {
+    fn meet(self, other: Self) -> Self {
+        match (self, other) {
+            (None, _) | (_, None) => None,
+            (Some(lhs), Some(rhs)) => (lhs == rhs).then_some(lhs),
+        }
+    }
+}
+
+impl TryJoin for Option<DefId> {
+    fn try_join(self, other: Self) -> Option<Self> {
+        match (self, other) {
+            (Some(lhs), Some(rhs)) => (lhs == rhs).then_some(Some(lhs)),
+            (Some(def_id), _) | (_, Some(def_id)) => Some(Some(def_id)),
+            (None, None) => Some(None),
+        }
+    }
+}
+
+impl Meet for Certainty {
+    fn meet(self, other: Self) -> Self {
+        match (self, other) {
+            (Certainty::Uncertain, _) | (_, Certainty::Uncertain) => Certainty::Uncertain,
+            (Certainty::Certain(lhs), Certainty::Certain(rhs)) => Certainty::Certain(lhs.meet(rhs)),
+            (Certainty::Certain(inner), _) | (_, Certainty::Certain(inner)) => Certainty::Certain(inner),
+            (Certainty::Contradiction, Certainty::Contradiction) => Certainty::Contradiction,
+        }
+    }
+}
+
+impl Certainty {
+    /// Join two `Certainty`s preserving their `DefId`s (if any). Generally speaking, this method
+    /// should be used only when `self` and `other` refer directly to types. Otherwise,
+    /// `join_clearing_def_ids` should be used.
+    pub fn join(self, other: Self) -> Self {
+        match (self, other) {
+            (Certainty::Contradiction, _) | (_, Certainty::Contradiction) => Certainty::Contradiction,
+
+            (Certainty::Certain(lhs), Certainty::Certain(rhs)) => {
+                if let Some(inner) = lhs.try_join(rhs) {
+                    Certainty::Certain(inner)
+                } else {
+                    debug_assert!(false, "Contradiction with {lhs:?} and {rhs:?}");
+                    Certainty::Contradiction
+                }
+            },
+
+            (Certainty::Certain(inner), _) | (_, Certainty::Certain(inner)) => Certainty::Certain(inner),
+
+            (Certainty::Uncertain, Certainty::Uncertain) => Certainty::Uncertain,
+        }
+    }
+
+    /// Join two `Certainty`s after clearing their `DefId`s. This method should be used when `self`
+    /// or `other` do not necessarily refer to types, e.g., when they are aggregations of other
+    /// `Certainty`s.
+    pub fn join_clearing_def_ids(self, other: Self) -> Self {
+        self.clear_def_id().join(other.clear_def_id())
+    }
+
+    pub fn clear_def_id(self) -> Certainty {
+        if matches!(self, Certainty::Certain(_)) {
+            Certainty::Certain(None)
+        } else {
+            self
+        }
+    }
+
+    pub fn with_def_id(self, def_id: DefId) -> Certainty {
+        if matches!(self, Certainty::Certain(_)) {
+            Certainty::Certain(Some(def_id))
+        } else {
+            self
+        }
+    }
+
+    pub fn to_def_id(self) -> Option<DefId> {
+        match self {
+            Certainty::Certain(inner) => inner,
+            _ => None,
+        }
+    }
+
+    pub fn is_certain(self) -> bool {
+        matches!(self, Self::Certain(_))
+    }
+}
+
+/// Think: `iter.all(/* is certain */)`
+pub fn meet(iter: impl Iterator<Item = Certainty>) -> Certainty {
+    iter.fold(Certainty::Certain(None), Certainty::meet)
+}
+
+/// Think: `iter.any(/* is certain */)`
+pub fn join(iter: impl Iterator<Item = Certainty>) -> Certainty {
+    iter.fold(Certainty::Uncertain, Certainty::join)
+}
diff --git a/clippy_utils/src/ty/type_certainty/mod.rs b/clippy_utils/src/ty/type_certainty/mod.rs
new file mode 100644
index 00000000000..67c4a293a86
--- /dev/null
+++ b/clippy_utils/src/ty/type_certainty/mod.rs
@@ -0,0 +1,315 @@
+//! A heuristic to tell whether an expression's type can be determined purely from its
+//! subexpressions, and the arguments and locals they use. Put another way, `expr_type_is_certain`
+//! tries to tell whether an expression's type can be determined without appeal to the surrounding
+//! context.
+//!
+//! This is, in some sense, a counterpart to `let_unit_value`'s `expr_needs_inferred_result`.
+//! Intuitively, that function determines whether an expression's type is needed for type inference,
+//! whereas `expr_type_is_certain` determines whether type inference is needed for an expression's
+//! type.
+//!
+//! As a heuristic, `expr_type_is_certain` may produce false negatives, but a false positive should
+//! be considered a bug.
+
+use crate::def_path_res;
+use rustc_hir::def::Res;
+use rustc_hir::def_id::DefId;
+use rustc_hir::intravisit::{walk_qpath, walk_ty, Visitor};
+use rustc_hir::{self as hir, Expr, ExprKind, GenericArgs, HirId, Node, PathSegment, QPath, TyKind};
+use rustc_lint::LateContext;
+use rustc_middle::ty::{self, AdtDef, GenericArgKind, Ty};
+use rustc_span::{Span, Symbol};
+
+mod certainty;
+use certainty::{join, meet, Certainty, Meet};
+
+pub fn expr_type_is_certain(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
+    expr_type_certainty(cx, expr).is_certain()
+}
+
+fn expr_type_certainty(cx: &LateContext<'_>, expr: &Expr<'_>) -> Certainty {
+    let certainty = match &expr.kind {
+        ExprKind::Unary(_, expr)
+        | ExprKind::Field(expr, _)
+        | ExprKind::Index(expr, _)
+        | ExprKind::AddrOf(_, _, expr) => expr_type_certainty(cx, expr),
+
+        ExprKind::Array(exprs) => join(exprs.iter().map(|expr| expr_type_certainty(cx, expr))),
+
+        ExprKind::Call(callee, args) => {
+            let lhs = expr_type_certainty(cx, callee);
+            let rhs = if type_is_inferrable_from_arguments(cx, expr) {
+                meet(args.iter().map(|arg| expr_type_certainty(cx, arg)))
+            } else {
+                Certainty::Uncertain
+            };
+            lhs.join_clearing_def_ids(rhs)
+        },
+
+        ExprKind::MethodCall(method, receiver, args, _) => {
+            let mut receiver_type_certainty = expr_type_certainty(cx, receiver);
+            // Even if `receiver_type_certainty` is `Certain(Some(..))`, the `Self` type in the method
+            // identified by `type_dependent_def_id(..)` can differ. This can happen as a result of a `deref`,
+            // for example. So update the `DefId` in `receiver_type_certainty` (if any).
+            if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id)
+                && let Some(self_ty_def_id) = adt_def_id(self_ty(cx, method_def_id))
+            {
+                receiver_type_certainty = receiver_type_certainty.with_def_id(self_ty_def_id);
+            };
+            let lhs = path_segment_certainty(cx, receiver_type_certainty, method, false);
+            let rhs = if type_is_inferrable_from_arguments(cx, expr) {
+                meet(
+                    std::iter::once(receiver_type_certainty).chain(args.iter().map(|arg| expr_type_certainty(cx, arg))),
+                )
+            } else {
+                Certainty::Uncertain
+            };
+            lhs.join(rhs)
+        },
+
+        ExprKind::Tup(exprs) => meet(exprs.iter().map(|expr| expr_type_certainty(cx, expr))),
+
+        ExprKind::Binary(_, lhs, rhs) => expr_type_certainty(cx, lhs).meet(expr_type_certainty(cx, rhs)),
+
+        ExprKind::Lit(_) => Certainty::Certain(None),
+
+        ExprKind::Cast(_, ty) => type_certainty(cx, ty),
+
+        ExprKind::If(_, if_expr, Some(else_expr)) => {
+            expr_type_certainty(cx, if_expr).join(expr_type_certainty(cx, else_expr))
+        },
+
+        ExprKind::Path(qpath) => qpath_certainty(cx, qpath, false),
+
+        ExprKind::Struct(qpath, _, _) => qpath_certainty(cx, qpath, true),
+
+        _ => Certainty::Uncertain,
+    };
+
+    let expr_ty = cx.typeck_results().expr_ty(expr);
+    if let Some(def_id) = adt_def_id(expr_ty) {
+        certainty.with_def_id(def_id)
+    } else {
+        certainty
+    }
+}
+
+struct CertaintyVisitor<'cx, 'tcx> {
+    cx: &'cx LateContext<'tcx>,
+    certainty: Certainty,
+}
+
+impl<'cx, 'tcx> CertaintyVisitor<'cx, 'tcx> {
+    fn new(cx: &'cx LateContext<'tcx>) -> Self {
+        Self {
+            cx,
+            certainty: Certainty::Certain(None),
+        }
+    }
+}
+
+impl<'cx, 'tcx> Visitor<'cx> for CertaintyVisitor<'cx, 'tcx> {
+    fn visit_qpath(&mut self, qpath: &'cx QPath<'_>, hir_id: HirId, _: Span) {
+        self.certainty = self.certainty.meet(qpath_certainty(self.cx, qpath, true));
+        if self.certainty != Certainty::Uncertain {
+            walk_qpath(self, qpath, hir_id);
+        }
+    }
+
+    fn visit_ty(&mut self, ty: &'cx hir::Ty<'_>) {
+        if matches!(ty.kind, TyKind::Infer) {
+            self.certainty = Certainty::Uncertain;
+        }
+        if self.certainty != Certainty::Uncertain {
+            walk_ty(self, ty);
+        }
+    }
+}
+
+fn type_certainty(cx: &LateContext<'_>, ty: &hir::Ty<'_>) -> Certainty {
+    // Handle `TyKind::Path` specially so that its `DefId` can be preserved.
+    //
+    // Note that `CertaintyVisitor::new` initializes the visitor's internal certainty to
+    // `Certainty::Certain(None)`. Furthermore, if a `TyKind::Path` is encountered while traversing
+    // `ty`, the result of the call to `qpath_certainty` is combined with the visitor's internal
+    // certainty using `Certainty::meet`. Thus, if the `TyKind::Path` were not treated specially here,
+    // the resulting certainty would be `Certainty::Certain(None)`.
+    if let TyKind::Path(qpath) = &ty.kind {
+        return qpath_certainty(cx, qpath, true);
+    }
+
+    let mut visitor = CertaintyVisitor::new(cx);
+    visitor.visit_ty(ty);
+    visitor.certainty
+}
+
+fn generic_args_certainty(cx: &LateContext<'_>, args: &GenericArgs<'_>) -> Certainty {
+    let mut visitor = CertaintyVisitor::new(cx);
+    visitor.visit_generic_args(args);
+    visitor.certainty
+}
+
+/// Tries to tell whether a `QPath` resolves to something certain, e.g., whether all of its path
+/// segments generic arguments are are instantiated.
+///
+/// `qpath` could refer to either a type or a value. The heuristic never needs the `DefId` of a
+/// value. So `DefId`s are retained only when `resolves_to_type` is true.
+fn qpath_certainty(cx: &LateContext<'_>, qpath: &QPath<'_>, resolves_to_type: bool) -> Certainty {
+    let certainty = match qpath {
+        QPath::Resolved(ty, path) => {
+            let len = path.segments.len();
+            path.segments.iter().enumerate().fold(
+                ty.map_or(Certainty::Uncertain, |ty| type_certainty(cx, ty)),
+                |parent_certainty, (i, path_segment)| {
+                    path_segment_certainty(cx, parent_certainty, path_segment, i != len - 1 || resolves_to_type)
+                },
+            )
+        },
+
+        QPath::TypeRelative(ty, path_segment) => {
+            path_segment_certainty(cx, type_certainty(cx, ty), path_segment, resolves_to_type)
+        },
+
+        QPath::LangItem(lang_item, _, _) => {
+            cx.tcx
+                .lang_items()
+                .get(*lang_item)
+                .map_or(Certainty::Uncertain, |def_id| {
+                    let generics = cx.tcx.generics_of(def_id);
+                    if generics.parent_count == 0 && generics.params.is_empty() {
+                        Certainty::Certain(if resolves_to_type { Some(def_id) } else { None })
+                    } else {
+                        Certainty::Uncertain
+                    }
+                })
+        },
+    };
+    debug_assert!(resolves_to_type || certainty.to_def_id().is_none());
+    certainty
+}
+
+fn path_segment_certainty(
+    cx: &LateContext<'_>,
+    parent_certainty: Certainty,
+    path_segment: &PathSegment<'_>,
+    resolves_to_type: bool,
+) -> Certainty {
+    let certainty = match update_res(cx, parent_certainty, path_segment).unwrap_or(path_segment.res) {
+        // A definition's type is certain if it refers to something without generics (e.g., a crate or module, or
+        // an unparameterized type), or the generics are instantiated with arguments that are certain.
+        //
+        // If the parent is uncertain, then the current path segment must account for the parent's generic arguments.
+        // Consider the following examples, where the current path segment is `None`:
+        // - `Option::None`             // uncertain; parent (i.e., `Option`) is uncertain
+        // - `Option::<Vec<u64>>::None` // certain; parent (i.e., `Option::<..>`) is certain
+        // - `Option::None::<Vec<u64>>` // certain; parent (i.e., `Option`) is uncertain
+        Res::Def(_, def_id) => {
+            // Checking `res_generics_def_id(..)` before calling `generics_of` avoids an ICE.
+            if cx.tcx.res_generics_def_id(path_segment.res).is_some() {
+                let generics = cx.tcx.generics_of(def_id);
+                let lhs = if (parent_certainty.is_certain() || generics.parent_count == 0) && generics.params.is_empty()
+                {
+                    Certainty::Certain(None)
+                } else {
+                    Certainty::Uncertain
+                };
+                let rhs = path_segment
+                    .args
+                    .map_or(Certainty::Uncertain, |args| generic_args_certainty(cx, args));
+                // See the comment preceding `qpath_certainty`. `def_id` could refer to a type or a value.
+                let certainty = lhs.join_clearing_def_ids(rhs);
+                if resolves_to_type {
+                    certainty.with_def_id(def_id)
+                } else {
+                    certainty
+                }
+            } else {
+                Certainty::Certain(None)
+            }
+        },
+
+        Res::PrimTy(_) | Res::SelfTyParam { .. } | Res::SelfTyAlias { .. } | Res::SelfCtor(_) => {
+            Certainty::Certain(None)
+        },
+
+        // `get_parent` because `hir_id` refers to a `Pat`, and we're interested in the node containing the `Pat`.
+        Res::Local(hir_id) => match cx.tcx.hir().get_parent(hir_id) {
+            // An argument's type is always certain.
+            Node::Param(..) => Certainty::Certain(None),
+            // A local's type is certain if its type annotation is certain or it has an initializer whose
+            // type is certain.
+            Node::Local(local) => {
+                let lhs = local.ty.map_or(Certainty::Uncertain, |ty| type_certainty(cx, ty));
+                let rhs = local
+                    .init
+                    .map_or(Certainty::Uncertain, |init| expr_type_certainty(cx, init));
+                let certainty = lhs.join(rhs);
+                if resolves_to_type {
+                    certainty
+                } else {
+                    certainty.clear_def_id()
+                }
+            },
+            _ => Certainty::Uncertain,
+        },
+
+        _ => Certainty::Uncertain,
+    };
+    debug_assert!(resolves_to_type || certainty.to_def_id().is_none());
+    certainty
+}
+
+/// For at least some `QPath::TypeRelative`, the path segment's `res` can be `Res::Err`.
+/// `update_res` tries to fix the resolution when `parent_certainty` is `Certain(Some(..))`.
+fn update_res(cx: &LateContext<'_>, parent_certainty: Certainty, path_segment: &PathSegment<'_>) -> Option<Res> {
+    if path_segment.res == Res::Err && let Some(def_id) = parent_certainty.to_def_id() {
+        let mut def_path = cx.get_def_path(def_id);
+        def_path.push(path_segment.ident.name);
+        let reses = def_path_res(cx, &def_path.iter().map(Symbol::as_str).collect::<Vec<_>>());
+        if let [res] = reses.as_slice() { Some(*res) } else { None }
+    } else {
+        None
+    }
+}
+
+#[allow(clippy::cast_possible_truncation)]
+fn type_is_inferrable_from_arguments(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
+    let Some(callee_def_id) = (match expr.kind {
+        ExprKind::Call(callee, _) => {
+            let callee_ty = cx.typeck_results().expr_ty(callee);
+            if let ty::FnDef(callee_def_id, _) = callee_ty.kind() {
+                Some(*callee_def_id)
+            } else {
+                None
+            }
+        },
+        ExprKind::MethodCall(_, _, _, _) => cx.typeck_results().type_dependent_def_id(expr.hir_id),
+        _ => None,
+    }) else {
+        return false;
+    };
+
+    let generics = cx.tcx.generics_of(callee_def_id);
+    let fn_sig = cx.tcx.fn_sig(callee_def_id).skip_binder();
+
+    // Check that all type parameters appear in the functions input types.
+    (0..(generics.parent_count + generics.params.len()) as u32).all(|index| {
+        fn_sig
+            .inputs()
+            .iter()
+            .any(|input_ty| contains_param(*input_ty.skip_binder(), index))
+    })
+}
+
+fn self_ty<'tcx>(cx: &LateContext<'tcx>, method_def_id: DefId) -> Ty<'tcx> {
+    cx.tcx.fn_sig(method_def_id).skip_binder().inputs().skip_binder()[0]
+}
+
+fn adt_def_id(ty: Ty<'_>) -> Option<DefId> {
+    ty.peel_refs().ty_adt_def().map(AdtDef::did)
+}
+
+fn contains_param(ty: Ty<'_>, index: u32) -> bool {
+    ty.walk()
+        .any(|arg| matches!(arg.unpack(), GenericArgKind::Type(ty) if ty.is_param(index)))
+}
diff --git a/clippy_utils/src/usage.rs b/clippy_utils/src/usage.rs
index 20993398d1b..8a7ea1e6cad 100644
--- a/clippy_utils/src/usage.rs
+++ b/clippy_utils/src/usage.rs
@@ -1,5 +1,6 @@
 use crate::visitors::{for_each_expr, for_each_expr_with_closures, Descend};
 use core::ops::ControlFlow;
+use hir::def::Res;
 use rustc_hir::intravisit::{self, Visitor};
 use rustc_hir::{Expr, ExprKind, HirId, HirIdSet, Node};
 use rustc_hir_typeck::expr_use_visitor::{Delegate, ExprUseVisitor, PlaceBase, PlaceWithHirId};
@@ -127,7 +128,7 @@ impl<'a, 'tcx> intravisit::Visitor<'tcx> for BindingUsageFinder<'a, 'tcx> {
     }
 
     fn visit_path(&mut self, path: &hir::Path<'tcx>, _: hir::HirId) {
-        if let hir::def::Res::Local(id) = path.res {
+        if let Res::Local(id) = path.res {
             if self.binding_ids.contains(&id) {
                 self.usage_found = true;
             }