Skip to content

Commit ee1d796

Browse files
committed
Make explicit_deref_methods check for multiple deref calls
Fix suggestion for `explicit_deref_methods`. Sometimes `&**` is needed, sometimes nothing is needed. Allow `explicit_deref_methods` to trigger in a few new contexts. `explicit_deref_methods` will now consider ufcs calls
1 parent d02ca3b commit ee1d796

File tree

8 files changed

+461
-123
lines changed

8 files changed

+461
-123
lines changed

clippy_lints/src/dereference.rs

+304-65
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
use crate::utils::{get_parent_expr, implements_trait, snippet, span_lint_and_sugg};
2-
use if_chain::if_chain;
3-
use rustc_ast::util::parser::{ExprPrecedence, PREC_POSTFIX, PREC_PREFIX};
1+
use crate::utils::{
2+
get_node_span, get_parent_node, in_macro, is_allowed, peel_mid_ty_refs, snippet_with_context, span_lint_and_sugg,
3+
};
4+
use rustc_ast::util::parser::PREC_PREFIX;
45
use rustc_errors::Applicability;
5-
use rustc_hir::{Expr, ExprKind};
6+
use rustc_hir::{BorrowKind, Destination, Expr, ExprKind, HirId, MatchSource, Mutability, Node, UnOp};
67
use rustc_lint::{LateContext, LateLintPass};
7-
use rustc_session::{declare_lint_pass, declare_tool_lint};
8-
use rustc_span::source_map::Span;
8+
use rustc_middle::ty::{self, adjustment::Adjustment, Ty, TyCtxt, TyS, TypeckResults};
9+
use rustc_session::{declare_tool_lint, impl_lint_pass};
10+
use rustc_span::{symbol::sym, Span};
911

1012
declare_clippy_lint! {
1113
/// **What it does:** Checks for explicit `deref()` or `deref_mut()` method calls.
@@ -34,76 +36,313 @@ declare_clippy_lint! {
3436
"Explicit use of deref or deref_mut method while not in a method chain."
3537
}
3638

37-
declare_lint_pass!(Dereferencing => [
38-
EXPLICIT_DEREF_METHODS
39+
impl_lint_pass!(Dereferencing => [
40+
EXPLICIT_DEREF_METHODS,
3941
]);
4042

43+
#[derive(Default)]
44+
pub struct Dereferencing {
45+
state: Option<(State, StateData)>,
46+
47+
// While parsing a `deref` method call in ufcs form, the path to the function is itself an
48+
// expression. This is to store the id of that expression so it can be skipped when
49+
// `check_expr` is called for it.
50+
skip_expr: Option<HirId>,
51+
}
52+
53+
struct StateData {
54+
/// Span of the top level expression
55+
span: Span,
56+
/// The required mutability
57+
target_mut: Mutability,
58+
}
59+
60+
enum State {
61+
// Any number of deref method calls.
62+
DerefMethod {
63+
// The number of calls in a sequence which changed the referenced type
64+
ty_changed_count: usize,
65+
is_final_ufcs: bool,
66+
},
67+
}
68+
69+
// A reference operation considered by this lint pass
70+
enum RefOp {
71+
Method,
72+
Deref,
73+
AddrOf,
74+
}
75+
4176
impl<'tcx> LateLintPass<'tcx> for Dereferencing {
4277
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
43-
if_chain! {
44-
if !expr.span.from_expansion();
45-
if let ExprKind::MethodCall(ref method_name, _, ref args, _) = &expr.kind;
46-
if args.len() == 1;
47-
48-
then {
49-
if let Some(parent_expr) = get_parent_expr(cx, expr) {
50-
// Check if we have the whole call chain here
51-
if let ExprKind::MethodCall(..) = parent_expr.kind {
52-
return;
53-
}
54-
// Check for Expr that we don't want to be linted
55-
let precedence = parent_expr.precedence();
56-
match precedence {
57-
// Lint a Call is ok though
58-
ExprPrecedence::Call | ExprPrecedence::AddrOf => (),
59-
_ => {
60-
if precedence.order() >= PREC_PREFIX && precedence.order() <= PREC_POSTFIX {
61-
return;
62-
}
63-
}
78+
// Skip path expressions from deref calls. e.g. `Deref::deref(e)`
79+
if Some(expr.hir_id) == self.skip_expr.take() {
80+
return;
81+
}
82+
83+
// Stop processing sub expressions when a macro call is seen
84+
if in_macro(expr.span) {
85+
if let Some((state, data)) = self.state.take() {
86+
report(cx, expr, state, data);
87+
}
88+
return;
89+
}
90+
91+
let typeck = cx.typeck_results();
92+
let (kind, sub_expr) = if let Some(x) = try_parse_ref_op(cx.tcx, typeck, expr) {
93+
x
94+
} else {
95+
// The whole chain of reference operations has been seen
96+
if let Some((state, data)) = self.state.take() {
97+
report(cx, expr, state, data);
98+
}
99+
return;
100+
};
101+
102+
match (self.state.take(), kind) {
103+
(None, kind) => {
104+
let parent = get_parent_node(cx.tcx, expr.hir_id);
105+
// This is an odd case. The expression is a macro argument, but the top level
106+
// address of expression is inserted by the compiler.
107+
if matches!(kind, RefOp::AddrOf) && parent.and_then(get_node_span).map_or(false, in_macro) {
108+
return;
109+
}
110+
111+
let expr_adjustments = find_adjustments(cx.tcx, typeck, expr);
112+
let expr_ty = typeck.expr_ty(expr);
113+
let target_mut =
114+
if let ty::Ref(_, _, mutability) = *expr_adjustments.last().map_or(expr_ty, |a| a.target).kind() {
115+
mutability
116+
} else {
117+
Mutability::Not
118+
};
119+
120+
match kind {
121+
RefOp::Method
122+
if !is_allowed(cx, EXPLICIT_DEREF_METHODS, expr.hir_id)
123+
&& is_linted_explicit_deref_position(parent, expr.hir_id) =>
124+
{
125+
self.state = Some((
126+
State::DerefMethod {
127+
ty_changed_count: if deref_method_same_type(expr_ty, typeck.expr_ty(sub_expr)) {
128+
0
129+
} else {
130+
1
131+
},
132+
is_final_ufcs: matches!(expr.kind, ExprKind::Call(..)),
133+
},
134+
StateData {
135+
span: expr.span,
136+
target_mut,
137+
},
138+
));
64139
}
140+
_ => (),
65141
}
66-
let name = method_name.ident.as_str();
67-
lint_deref(cx, &*name, &args[0], args[0].span, expr.span);
68-
}
142+
},
143+
(Some((State::DerefMethod { ty_changed_count, .. }, data)), RefOp::Method) => {
144+
self.state = Some((
145+
State::DerefMethod {
146+
ty_changed_count: if deref_method_same_type(typeck.expr_ty(expr), typeck.expr_ty(sub_expr)) {
147+
ty_changed_count
148+
} else {
149+
ty_changed_count + 1
150+
},
151+
is_final_ufcs: matches!(expr.kind, ExprKind::Call(..)),
152+
},
153+
data,
154+
));
155+
},
156+
157+
(Some((state, data)), _) => report(cx, expr, state, data),
69158
}
70159
}
71160
}
72161

73-
fn lint_deref(cx: &LateContext<'_>, method_name: &str, call_expr: &Expr<'_>, var_span: Span, expr_span: Span) {
74-
match method_name {
75-
"deref" => {
76-
let impls_deref_trait = cx.tcx.lang_items().deref_trait().map_or(false, |id| {
77-
implements_trait(cx, cx.typeck_results().expr_ty(&call_expr), id, &[])
78-
});
79-
if impls_deref_trait {
80-
span_lint_and_sugg(
81-
cx,
82-
EXPLICIT_DEREF_METHODS,
83-
expr_span,
84-
"explicit deref method call",
85-
"try this",
86-
format!("&*{}", &snippet(cx, var_span, "..")),
87-
Applicability::MachineApplicable,
88-
);
89-
}
162+
fn try_parse_ref_op(
163+
tcx: TyCtxt<'tcx>,
164+
typeck: &'tcx TypeckResults<'_>,
165+
expr: &'tcx Expr<'_>,
166+
) -> Option<(RefOp, &'tcx Expr<'tcx>)> {
167+
let (def_id, arg) = match expr.kind {
168+
ExprKind::MethodCall(_, _, [arg], _) => (typeck.type_dependent_def_id(expr.hir_id)?, arg),
169+
ExprKind::Call(
170+
Expr {
171+
kind: ExprKind::Path(path),
172+
hir_id,
173+
..
174+
},
175+
[arg],
176+
) => (typeck.qpath_res(path, *hir_id).opt_def_id()?, arg),
177+
ExprKind::Unary(UnOp::Deref, sub_expr) if !typeck.expr_ty(sub_expr).is_unsafe_ptr() => {
178+
return Some((RefOp::Deref, sub_expr));
90179
},
91-
"deref_mut" => {
92-
let impls_deref_mut_trait = cx.tcx.lang_items().deref_mut_trait().map_or(false, |id| {
93-
implements_trait(cx, cx.typeck_results().expr_ty(&call_expr), id, &[])
94-
});
95-
if impls_deref_mut_trait {
96-
span_lint_and_sugg(
97-
cx,
98-
EXPLICIT_DEREF_METHODS,
99-
expr_span,
100-
"explicit deref_mut method call",
101-
"try this",
102-
format!("&mut *{}", &snippet(cx, var_span, "..")),
103-
Applicability::MachineApplicable,
104-
);
105-
}
180+
ExprKind::AddrOf(BorrowKind::Ref, _, sub_expr) => return Some((RefOp::AddrOf, sub_expr)),
181+
_ => return None,
182+
};
183+
(tcx.is_diagnostic_item(sym::deref_method, def_id)
184+
|| tcx.trait_of_item(def_id)? == tcx.lang_items().deref_mut_trait()?)
185+
.then(|| (RefOp::Method, arg))
186+
}
187+
188+
// Checks whether the type for a deref call actually changed the type, not just the mutability of
189+
// the reference.
190+
fn deref_method_same_type(result_ty: Ty<'tcx>, arg_ty: Ty<'tcx>) -> bool {
191+
match (result_ty.kind(), arg_ty.kind()) {
192+
(ty::Ref(_, result_ty, _), ty::Ref(_, arg_ty, _)) => TyS::same_type(result_ty, arg_ty),
193+
194+
// The result type for a deref method is always a reference
195+
// Not matching the previous pattern means the argument type is not a reference
196+
// This means that the type did change
197+
_ => false,
198+
}
199+
}
200+
201+
// Adjustments are sometimes made in the parent block rather than the expression itself.
202+
fn find_adjustments(
203+
tcx: TyCtxt<'tcx>,
204+
typeck: &'tcx TypeckResults<'_>,
205+
expr: &'tcx Expr<'_>,
206+
) -> &'tcx [Adjustment<'tcx>] {
207+
let map = tcx.hir();
208+
let mut iter = map.parent_iter(expr.hir_id);
209+
let mut prev = expr;
210+
211+
loop {
212+
match typeck.expr_adjustments(prev) {
213+
[] => (),
214+
a => break a,
215+
};
216+
217+
match iter.next().map(|(_, x)| x) {
218+
Some(Node::Block(_)) => {
219+
if let Some((_, Node::Expr(e))) = iter.next() {
220+
prev = e;
221+
} else {
222+
// This shouldn't happen. Blocks are always contained in an expression.
223+
break &[];
224+
}
225+
},
226+
Some(Node::Expr(&Expr {
227+
kind: ExprKind::Break(Destination { target_id: Ok(id), .. }, _),
228+
..
229+
})) => {
230+
if let Some(Node::Expr(e)) = map.find(id) {
231+
prev = e;
232+
iter = map.parent_iter(id);
233+
continue;
234+
}
235+
// This shouldn't happen. The destination should definitely exist at this point.
236+
break &[];
237+
},
238+
_ => break &[],
239+
}
240+
}
241+
}
242+
243+
// Checks whether the parent node is a suitable context for switching from a deref method to the
244+
// deref operator.
245+
fn is_linted_explicit_deref_position(parent: Option<Node<'_>>, child_id: HirId) -> bool {
246+
let parent = match parent {
247+
Some(Node::Expr(e)) => e,
248+
_ => return true,
249+
};
250+
match parent.kind {
251+
// Leave deref calls in the middle of a method chain.
252+
// e.g. x.deref().foo()
253+
ExprKind::MethodCall(_, _, [self_arg, ..], _) if self_arg.hir_id == child_id => false,
254+
255+
// Leave deref calls resulting in a called function
256+
// e.g. (x.deref())()
257+
ExprKind::Call(func_expr, _) if func_expr.hir_id == child_id => false,
258+
259+
// Makes an ugly suggestion
260+
// e.g. *x.deref() => *&*x
261+
ExprKind::Unary(UnOp::Deref, _)
262+
// Postfix expressions would require parens
263+
| ExprKind::Match(_, _, MatchSource::TryDesugar | MatchSource::AwaitDesugar)
264+
| ExprKind::Field(..)
265+
| ExprKind::Index(..)
266+
| ExprKind::Err => false,
267+
268+
ExprKind::Box(..)
269+
| ExprKind::ConstBlock(..)
270+
| ExprKind::Array(_)
271+
| ExprKind::Call(..)
272+
| ExprKind::MethodCall(..)
273+
| ExprKind::Tup(..)
274+
| ExprKind::Binary(..)
275+
| ExprKind::Unary(..)
276+
| ExprKind::Lit(..)
277+
| ExprKind::Cast(..)
278+
| ExprKind::Type(..)
279+
| ExprKind::DropTemps(..)
280+
| ExprKind::If(..)
281+
| ExprKind::Loop(..)
282+
| ExprKind::Match(..)
283+
| ExprKind::Closure(..)
284+
| ExprKind::Block(..)
285+
| ExprKind::Assign(..)
286+
| ExprKind::AssignOp(..)
287+
| ExprKind::Path(..)
288+
| ExprKind::AddrOf(..)
289+
| ExprKind::Break(..)
290+
| ExprKind::Continue(..)
291+
| ExprKind::Ret(..)
292+
| ExprKind::InlineAsm(..)
293+
| ExprKind::LlvmInlineAsm(..)
294+
| ExprKind::Struct(..)
295+
| ExprKind::Repeat(..)
296+
| ExprKind::Yield(..) => true,
297+
}
298+
}
299+
300+
#[allow(clippy::needless_pass_by_value)]
301+
fn report(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, state: State, data: StateData) {
302+
match state {
303+
State::DerefMethod {
304+
ty_changed_count,
305+
is_final_ufcs,
306+
} => {
307+
let mut app = Applicability::MachineApplicable;
308+
let (expr_str, expr_is_macro_call) = snippet_with_context(cx, expr.span, data.span.ctxt(), "..", &mut app);
309+
let ty = cx.typeck_results().expr_ty(expr);
310+
let (_, ref_count) = peel_mid_ty_refs(ty);
311+
let deref_str = if ty_changed_count >= ref_count && ref_count != 0 {
312+
// a deref call changing &T -> &U requires two deref operators the first time
313+
// this occurs. One to remove the reference, a second to call the deref impl.
314+
"*".repeat(ty_changed_count + 1)
315+
} else {
316+
"*".repeat(ty_changed_count)
317+
};
318+
let addr_of_str = if ty_changed_count < ref_count {
319+
// Check if a reborrow from &mut T -> &T is required.
320+
if data.target_mut == Mutability::Not && matches!(ty.kind(), ty::Ref(_, _, Mutability::Mut)) {
321+
"&*"
322+
} else {
323+
""
324+
}
325+
} else if data.target_mut == Mutability::Mut {
326+
"&mut "
327+
} else {
328+
"&"
329+
};
330+
331+
let expr_str = if !expr_is_macro_call && is_final_ufcs && expr.precedence().order() < PREC_PREFIX {
332+
format!("({})", expr_str)
333+
} else {
334+
expr_str.into_owned()
335+
};
336+
337+
span_lint_and_sugg(
338+
cx,
339+
EXPLICIT_DEREF_METHODS,
340+
data.span,
341+
"explicit `deref` method call",
342+
"try this",
343+
format!("{}{}{}", addr_of_str, deref_str, expr_str),
344+
app,
345+
);
106346
},
107-
_ => (),
108347
}
109348
}

0 commit comments

Comments
 (0)