rustc_lint/
noop_method_call.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
use rustc_hir::def::DefKind;
use rustc_hir::{Expr, ExprKind};
use rustc_middle::ty;
use rustc_middle::ty::adjustment::Adjust;
use rustc_session::{declare_lint, declare_lint_pass};
use rustc_span::symbol::sym;

use crate::context::LintContext;
use crate::lints::{
    NoopMethodCallDiag, SuspiciousDoubleRefCloneDiag, SuspiciousDoubleRefDerefDiag,
};
use crate::{LateContext, LateLintPass};

declare_lint! {
    /// The `noop_method_call` lint detects specific calls to noop methods
    /// such as a calling `<&T as Clone>::clone` where `T: !Clone`.
    ///
    /// ### Example
    ///
    /// ```rust
    /// # #![allow(unused)]
    /// struct Foo;
    /// let foo = &Foo;
    /// let clone: &Foo = foo.clone();
    /// ```
    ///
    /// {{produces}}
    ///
    /// ### Explanation
    ///
    /// Some method calls are noops meaning that they do nothing. Usually such methods
    /// are the result of blanket implementations that happen to create some method invocations
    /// that end up not doing anything. For instance, `Clone` is implemented on all `&T`, but
    /// calling `clone` on a `&T` where `T` does not implement clone, actually doesn't do anything
    /// as references are copy. This lint detects these calls and warns the user about them.
    pub NOOP_METHOD_CALL,
    Warn,
    "detects the use of well-known noop methods"
}

declare_lint! {
    /// The `suspicious_double_ref_op` lint checks for usage of `.clone()`/`.borrow()`/`.deref()`
    /// on an `&&T` when `T: !Deref/Borrow/Clone`, which means the call will return the inner `&T`,
    /// instead of performing the operation on the underlying `T` and can be confusing.
    ///
    /// ### Example
    ///
    /// ```rust
    /// # #![allow(unused)]
    /// struct Foo;
    /// let foo = &&Foo;
    /// let clone: &Foo = foo.clone();
    /// ```
    ///
    /// {{produces}}
    ///
    /// ### Explanation
    ///
    /// Since `Foo` doesn't implement `Clone`, running `.clone()` only dereferences the double
    /// reference, instead of cloning the inner type which should be what was intended.
    pub SUSPICIOUS_DOUBLE_REF_OP,
    Warn,
    "suspicious call of trait method on `&&T`"
}

declare_lint_pass!(NoopMethodCall => [NOOP_METHOD_CALL, SUSPICIOUS_DOUBLE_REF_OP]);

impl<'tcx> LateLintPass<'tcx> for NoopMethodCall {
    fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
        // We only care about method calls.
        let ExprKind::MethodCall(call, receiver, _, call_span) = &expr.kind else {
            return;
        };

        if call_span.from_expansion() {
            return;
        }

        // We only care about method calls corresponding to the `Clone`, `Deref` and `Borrow`
        // traits and ignore any other method call.

        let Some((DefKind::AssocFn, did)) = cx.typeck_results().type_dependent_def(expr.hir_id)
        else {
            return;
        };

        let Some(trait_id) = cx.tcx.trait_of_item(did) else { return };

        let Some(trait_) = cx.tcx.get_diagnostic_name(trait_id) else { return };

        if !matches!(trait_, sym::Borrow | sym::Clone | sym::Deref) {
            return;
        };

        let args = cx
            .tcx
            .normalize_erasing_regions(cx.typing_env(), cx.typeck_results().node_args(expr.hir_id));
        // Resolve the trait method instance.
        let Ok(Some(i)) = ty::Instance::try_resolve(cx.tcx, cx.typing_env(), did, args) else {
            return;
        };
        // (Re)check that it implements the noop diagnostic.
        let Some(name) = cx.tcx.get_diagnostic_name(i.def_id()) else { return };
        if !matches!(
            name,
            sym::noop_method_borrow | sym::noop_method_clone | sym::noop_method_deref
        ) {
            return;
        }

        let receiver_ty = cx.typeck_results().expr_ty(receiver);
        let expr_ty = cx.typeck_results().expr_ty_adjusted(expr);
        let arg_adjustments = cx.typeck_results().expr_adjustments(receiver);

        // If there is any user defined auto-deref step, then we don't want to warn.
        // https://github.com/rust-lang/rust-clippy/issues/9272
        if arg_adjustments.iter().any(|adj| matches!(adj.kind, Adjust::Deref(Some(_)))) {
            return;
        }

        let expr_span = expr.span;
        let span = expr_span.with_lo(receiver.span.hi());

        let orig_ty = expr_ty.peel_refs();

        if receiver_ty == expr_ty {
            let suggest_derive = match orig_ty.kind() {
                ty::Adt(def, _) => Some(cx.tcx.def_span(def.did()).shrink_to_lo()),
                _ => None,
            };
            cx.emit_span_lint(NOOP_METHOD_CALL, span, NoopMethodCallDiag {
                method: call.ident.name,
                orig_ty,
                trait_,
                label: span,
                suggest_derive,
            });
        } else {
            match name {
                // If `type_of(x) == T` and `x.borrow()` is used to get `&T`,
                // then that should be allowed
                sym::noop_method_borrow => return,
                sym::noop_method_clone => cx.emit_span_lint(
                    SUSPICIOUS_DOUBLE_REF_OP,
                    span,
                    SuspiciousDoubleRefCloneDiag { ty: expr_ty },
                ),
                sym::noop_method_deref => cx.emit_span_lint(
                    SUSPICIOUS_DOUBLE_REF_OP,
                    span,
                    SuspiciousDoubleRefDerefDiag { ty: expr_ty },
                ),
                _ => return,
            }
        }
    }
}