]> git.proxmox.com Git - rustc.git/blame - src/tools/clippy/clippy_lints/src/suspicious_operation_groupings.rs
New upstream version 1.53.0+dfsg1
[rustc.git] / src / tools / clippy / clippy_lints / src / suspicious_operation_groupings.rs
CommitLineData
cdc7bbd5
XL
1use clippy_utils::ast_utils::{eq_id, is_useless_with_eq_exprs, IdentIter};
2use clippy_utils::diagnostics::span_lint_and_sugg;
3use clippy_utils::source::snippet_with_applicability;
f20569fa
XL
4use core::ops::{Add, AddAssign};
5use if_chain::if_chain;
6use rustc_ast::ast::{BinOpKind, Expr, ExprKind, StmtKind};
7use rustc_data_structures::fx::FxHashSet;
8use rustc_errors::Applicability;
9use rustc_lint::{EarlyContext, EarlyLintPass};
10use rustc_session::{declare_lint_pass, declare_tool_lint};
11use rustc_span::source_map::Spanned;
12use rustc_span::symbol::Ident;
13use rustc_span::Span;
14
15declare_clippy_lint! {
16 /// **What it does:**
17 /// Checks for unlikely usages of binary operators that are almost
18 /// certainly typos and/or copy/paste errors, given the other usages
19 /// of binary operators nearby.
20 /// **Why is this bad?**
21 /// They are probably bugs and if they aren't then they look like bugs
22 /// and you should add a comment explaining why you are doing such an
23 /// odd set of operations.
24 /// **Known problems:**
25 /// There may be some false positives if you are trying to do something
26 /// unusual that happens to look like a typo.
27 ///
28 /// **Example:**
29 ///
30 /// ```rust
31 /// struct Vec3 {
32 /// x: f64,
33 /// y: f64,
34 /// z: f64,
35 /// }
36 ///
37 /// impl Eq for Vec3 {}
38 ///
39 /// impl PartialEq for Vec3 {
40 /// fn eq(&self, other: &Self) -> bool {
41 /// // This should trigger the lint because `self.x` is compared to `other.y`
42 /// self.x == other.y && self.y == other.y && self.z == other.z
43 /// }
44 /// }
45 /// ```
46 /// Use instead:
47 /// ```rust
48 /// # struct Vec3 {
49 /// # x: f64,
50 /// # y: f64,
51 /// # z: f64,
52 /// # }
53 /// // same as above except:
54 /// impl PartialEq for Vec3 {
55 /// fn eq(&self, other: &Self) -> bool {
56 /// // Note we now compare other.x to self.x
57 /// self.x == other.x && self.y == other.y && self.z == other.z
58 /// }
59 /// }
60 /// ```
61 pub SUSPICIOUS_OPERATION_GROUPINGS,
62 style,
63 "groupings of binary operations that look suspiciously like typos"
64}
65
66declare_lint_pass!(SuspiciousOperationGroupings => [SUSPICIOUS_OPERATION_GROUPINGS]);
67
68impl EarlyLintPass for SuspiciousOperationGroupings {
69 fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) {
70 if expr.span.from_expansion() {
71 return;
72 }
73
74 if let Some(binops) = extract_related_binops(&expr.kind) {
75 check_binops(cx, &binops.iter().collect::<Vec<_>>());
76
77 let mut op_types = Vec::with_capacity(binops.len());
78 // We could use a hashmap, etc. to avoid being O(n*m) here, but
79 // we want the lints to be emitted in a consistent order. Besides,
80 // m, (the number of distinct `BinOpKind`s in `binops`)
81 // will often be small, and does have an upper limit.
82 binops.iter().map(|b| b.op).for_each(|op| {
83 if !op_types.contains(&op) {
84 op_types.push(op);
85 }
86 });
87
88 for op_type in op_types {
89 let ops: Vec<_> = binops.iter().filter(|b| b.op == op_type).collect();
90
91 check_binops(cx, &ops);
92 }
93 }
94 }
95}
96
97fn check_binops(cx: &EarlyContext<'_>, binops: &[&BinaryOp<'_>]) {
98 let binop_count = binops.len();
99 if binop_count < 2 {
100 // Single binary operation expressions would likely be false
101 // positives.
102 return;
103 }
104
105 let mut one_ident_difference_count = 0;
106 let mut no_difference_info = None;
107 let mut double_difference_info = None;
108 let mut expected_ident_loc = None;
109
110 let mut paired_identifiers = FxHashSet::default();
111
112 for (i, BinaryOp { left, right, op, .. }) in binops.iter().enumerate() {
113 match ident_difference_expr(left, right) {
114 IdentDifference::NoDifference => {
115 if is_useless_with_eq_exprs(*op) {
116 // The `eq_op` lint should catch this in this case.
117 return;
118 }
119
120 no_difference_info = Some(i);
121 },
122 IdentDifference::Single(ident_loc) => {
123 one_ident_difference_count += 1;
124 if let Some(previous_expected) = expected_ident_loc {
125 if previous_expected != ident_loc {
126 // This expression doesn't match the form we're
127 // looking for.
128 return;
129 }
130 } else {
131 expected_ident_loc = Some(ident_loc);
132 }
133
134 // If there was only a single difference, all other idents
135 // must have been the same, and thus were paired.
136 for id in skip_index(IdentIter::from(*left), ident_loc.index) {
137 paired_identifiers.insert(id);
138 }
139 },
140 IdentDifference::Double(ident_loc1, ident_loc2) => {
141 double_difference_info = Some((i, ident_loc1, ident_loc2));
142 },
143 IdentDifference::Multiple | IdentDifference::NonIdent => {
144 // It's too hard to know whether this is a bug or not.
145 return;
146 },
147 }
148 }
149
150 let mut applicability = Applicability::MachineApplicable;
151
152 if let Some(expected_loc) = expected_ident_loc {
153 match (no_difference_info, double_difference_info) {
154 (Some(i), None) => attempt_to_emit_no_difference_lint(cx, binops, i, expected_loc),
155 (None, Some((double_difference_index, ident_loc1, ident_loc2))) => {
156 if_chain! {
157 if one_ident_difference_count == binop_count - 1;
158 if let Some(binop) = binops.get(double_difference_index);
159 then {
160 let changed_loc = if ident_loc1 == expected_loc {
161 ident_loc2
162 } else if ident_loc2 == expected_loc {
163 ident_loc1
164 } else {
165 // This expression doesn't match the form we're
166 // looking for.
167 return;
168 };
169
170 if let Some(sugg) = ident_swap_sugg(
171 cx,
172 &paired_identifiers,
173 binop,
174 changed_loc,
175 &mut applicability,
176 ) {
177 emit_suggestion(
178 cx,
179 binop.span,
180 sugg,
181 applicability,
182 );
183 }
184 }
185 }
186 },
187 _ => {},
188 }
189 }
190}
191
192fn attempt_to_emit_no_difference_lint(
193 cx: &EarlyContext<'_>,
194 binops: &[&BinaryOp<'_>],
195 i: usize,
196 expected_loc: IdentLocation,
197) {
cdc7bbd5 198 if let Some(binop) = binops.get(i).copied() {
f20569fa
XL
199 // We need to try and figure out which identifier we should
200 // suggest using instead. Since there could be multiple
201 // replacement candidates in a given expression, and we're
202 // just taking the first one, we may get some bad lint
203 // messages.
204 let mut applicability = Applicability::MaybeIncorrect;
205
206 // We assume that the correct ident is one used elsewhere in
207 // the other binops, in a place that there was a single
208 // difference between idents before.
209 let old_left_ident = get_ident(binop.left, expected_loc);
210 let old_right_ident = get_ident(binop.right, expected_loc);
211
212 for b in skip_index(binops.iter(), i) {
213 if_chain! {
214 if let (Some(old_ident), Some(new_ident)) =
215 (old_left_ident, get_ident(b.left, expected_loc));
216 if old_ident != new_ident;
217 if let Some(sugg) = suggestion_with_swapped_ident(
218 cx,
219 binop.left,
220 expected_loc,
221 new_ident,
222 &mut applicability,
223 );
224 then {
225 emit_suggestion(
226 cx,
227 binop.span,
cdc7bbd5 228 replace_left_sugg(cx, binop, &sugg, &mut applicability),
f20569fa
XL
229 applicability,
230 );
231 return;
232 }
233 }
234
235 if_chain! {
236 if let (Some(old_ident), Some(new_ident)) =
237 (old_right_ident, get_ident(b.right, expected_loc));
238 if old_ident != new_ident;
239 if let Some(sugg) = suggestion_with_swapped_ident(
240 cx,
241 binop.right,
242 expected_loc,
243 new_ident,
244 &mut applicability,
245 );
246 then {
247 emit_suggestion(
248 cx,
249 binop.span,
cdc7bbd5 250 replace_right_sugg(cx, binop, &sugg, &mut applicability),
f20569fa
XL
251 applicability,
252 );
253 return;
254 }
255 }
256 }
257 }
258}
259
260fn emit_suggestion(cx: &EarlyContext<'_>, span: Span, sugg: String, applicability: Applicability) {
261 span_lint_and_sugg(
262 cx,
263 SUSPICIOUS_OPERATION_GROUPINGS,
264 span,
265 "this sequence of operators looks suspiciously like a bug",
266 "did you mean",
267 sugg,
268 applicability,
269 )
270}
271
272fn ident_swap_sugg(
273 cx: &EarlyContext<'_>,
274 paired_identifiers: &FxHashSet<Ident>,
275 binop: &BinaryOp<'_>,
276 location: IdentLocation,
277 applicability: &mut Applicability,
278) -> Option<String> {
cdc7bbd5
XL
279 let left_ident = get_ident(binop.left, location)?;
280 let right_ident = get_ident(binop.right, location)?;
f20569fa
XL
281
282 let sugg = match (
283 paired_identifiers.contains(&left_ident),
284 paired_identifiers.contains(&right_ident),
285 ) {
286 (true, true) | (false, false) => {
287 // We don't have a good guess of what ident should be
288 // used instead, in these cases.
289 *applicability = Applicability::MaybeIncorrect;
290
291 // We arbitraily choose one side to suggest changing,
292 // since we don't have a better guess. If the user
293 // ends up duplicating a clause, the `logic_bug` lint
294 // should catch it.
295
cdc7bbd5 296 let right_suggestion = suggestion_with_swapped_ident(cx, binop.right, location, left_ident, applicability)?;
f20569fa
XL
297
298 replace_right_sugg(cx, binop, &right_suggestion, applicability)
299 },
300 (false, true) => {
301 // We haven't seen a pair involving the left one, so
302 // it's probably what is wanted.
303
cdc7bbd5 304 let right_suggestion = suggestion_with_swapped_ident(cx, binop.right, location, left_ident, applicability)?;
f20569fa
XL
305
306 replace_right_sugg(cx, binop, &right_suggestion, applicability)
307 },
308 (true, false) => {
309 // We haven't seen a pair involving the right one, so
310 // it's probably what is wanted.
cdc7bbd5 311 let left_suggestion = suggestion_with_swapped_ident(cx, binop.left, location, right_ident, applicability)?;
f20569fa
XL
312
313 replace_left_sugg(cx, binop, &left_suggestion, applicability)
314 },
315 };
316
317 Some(sugg)
318}
319
320fn replace_left_sugg(
321 cx: &EarlyContext<'_>,
322 binop: &BinaryOp<'_>,
323 left_suggestion: &str,
324 applicability: &mut Applicability,
325) -> String {
326 format!(
327 "{} {} {}",
328 left_suggestion,
329 binop.op.to_string(),
330 snippet_with_applicability(cx, binop.right.span, "..", applicability),
331 )
332}
333
334fn replace_right_sugg(
335 cx: &EarlyContext<'_>,
336 binop: &BinaryOp<'_>,
337 right_suggestion: &str,
338 applicability: &mut Applicability,
339) -> String {
340 format!(
341 "{} {} {}",
342 snippet_with_applicability(cx, binop.left.span, "..", applicability),
343 binop.op.to_string(),
344 right_suggestion,
345 )
346}
347
348#[derive(Clone, Debug)]
349struct BinaryOp<'exprs> {
350 op: BinOpKind,
351 span: Span,
352 left: &'exprs Expr,
353 right: &'exprs Expr,
354}
355
356impl BinaryOp<'exprs> {
357 fn new(op: BinOpKind, span: Span, (left, right): (&'exprs Expr, &'exprs Expr)) -> Self {
358 Self { op, span, left, right }
359 }
360}
361
362fn strip_non_ident_wrappers(expr: &Expr) -> &Expr {
363 let mut output = expr;
364 loop {
365 output = match &output.kind {
366 ExprKind::Paren(ref inner) | ExprKind::Unary(_, ref inner) => inner,
367 _ => {
368 return output;
369 },
370 };
371 }
372}
373
374fn extract_related_binops(kind: &ExprKind) -> Option<Vec<BinaryOp<'_>>> {
375 append_opt_vecs(chained_binops(kind), if_statment_binops(kind))
376}
377
378fn if_statment_binops(kind: &ExprKind) -> Option<Vec<BinaryOp<'_>>> {
379 match kind {
380 ExprKind::If(ref condition, _, _) => chained_binops(&condition.kind),
381 ExprKind::Paren(ref e) => if_statment_binops(&e.kind),
382 ExprKind::Block(ref block, _) => {
383 let mut output = None;
384 for stmt in &block.stmts {
385 match stmt.kind {
386 StmtKind::Expr(ref e) | StmtKind::Semi(ref e) => {
387 output = append_opt_vecs(output, if_statment_binops(&e.kind));
388 },
389 _ => {},
390 }
391 }
392 output
393 },
394 _ => None,
395 }
396}
397
398fn append_opt_vecs<A>(target_opt: Option<Vec<A>>, source_opt: Option<Vec<A>>) -> Option<Vec<A>> {
399 match (target_opt, source_opt) {
400 (Some(mut target), Some(mut source)) => {
401 target.reserve(source.len());
402 for op in source.drain(..) {
403 target.push(op);
404 }
405 Some(target)
406 },
407 (Some(v), None) | (None, Some(v)) => Some(v),
408 (None, None) => None,
409 }
410}
411
412fn chained_binops(kind: &ExprKind) -> Option<Vec<BinaryOp<'_>>> {
413 match kind {
414 ExprKind::Binary(_, left_outer, right_outer) => chained_binops_helper(left_outer, right_outer),
415 ExprKind::Paren(ref e) | ExprKind::Unary(_, ref e) => chained_binops(&e.kind),
416 _ => None,
417 }
418}
419
420fn chained_binops_helper(left_outer: &'expr Expr, right_outer: &'expr Expr) -> Option<Vec<BinaryOp<'expr>>> {
421 match (&left_outer.kind, &right_outer.kind) {
422 (
423 ExprKind::Paren(ref left_e) | ExprKind::Unary(_, ref left_e),
424 ExprKind::Paren(ref right_e) | ExprKind::Unary(_, ref right_e),
425 ) => chained_binops_helper(left_e, right_e),
426 (ExprKind::Paren(ref left_e) | ExprKind::Unary(_, ref left_e), _) => chained_binops_helper(left_e, right_outer),
427 (_, ExprKind::Paren(ref right_e) | ExprKind::Unary(_, ref right_e)) => {
428 chained_binops_helper(left_outer, right_e)
429 },
430 (
431 ExprKind::Binary(Spanned { node: left_op, .. }, ref left_left, ref left_right),
432 ExprKind::Binary(Spanned { node: right_op, .. }, ref right_left, ref right_right),
433 ) => match (
434 chained_binops_helper(left_left, left_right),
435 chained_binops_helper(right_left, right_right),
436 ) {
437 (Some(mut left_ops), Some(mut right_ops)) => {
438 left_ops.reserve(right_ops.len());
439 for op in right_ops.drain(..) {
440 left_ops.push(op);
441 }
442 Some(left_ops)
443 },
444 (Some(mut left_ops), _) => {
445 left_ops.push(BinaryOp::new(*right_op, right_outer.span, (right_left, right_right)));
446 Some(left_ops)
447 },
448 (_, Some(mut right_ops)) => {
449 right_ops.insert(0, BinaryOp::new(*left_op, left_outer.span, (left_left, left_right)));
450 Some(right_ops)
451 },
452 (None, None) => Some(vec![
453 BinaryOp::new(*left_op, left_outer.span, (left_left, left_right)),
454 BinaryOp::new(*right_op, right_outer.span, (right_left, right_right)),
455 ]),
456 },
457 _ => None,
458 }
459}
460
461#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
462struct IdentLocation {
463 index: usize,
464}
465
466impl Add for IdentLocation {
467 type Output = IdentLocation;
468
469 fn add(self, other: Self) -> Self::Output {
470 Self {
471 index: self.index + other.index,
472 }
473 }
474}
475
476impl AddAssign for IdentLocation {
477 fn add_assign(&mut self, other: Self) {
478 *self = *self + other
479 }
480}
481
482#[derive(Clone, Copy, Debug)]
483enum IdentDifference {
484 NoDifference,
485 Single(IdentLocation),
486 Double(IdentLocation, IdentLocation),
487 Multiple,
488 NonIdent,
489}
490
491impl Add for IdentDifference {
492 type Output = IdentDifference;
493
494 fn add(self, other: Self) -> Self::Output {
495 match (self, other) {
496 (Self::NoDifference, output) | (output, Self::NoDifference) => output,
497 (Self::Multiple, _)
498 | (_, Self::Multiple)
499 | (Self::Double(_, _), Self::Single(_))
500 | (Self::Single(_) | Self::Double(_, _), Self::Double(_, _)) => Self::Multiple,
501 (Self::NonIdent, _) | (_, Self::NonIdent) => Self::NonIdent,
502 (Self::Single(il1), Self::Single(il2)) => Self::Double(il1, il2),
503 }
504 }
505}
506
507impl AddAssign for IdentDifference {
508 fn add_assign(&mut self, other: Self) {
509 *self = *self + other
510 }
511}
512
513impl IdentDifference {
514 /// Returns true if learning about more differences will not change the value
515 /// of this `IdentDifference`, and false otherwise.
516 fn is_complete(&self) -> bool {
517 match self {
518 Self::NoDifference | Self::Single(_) | Self::Double(_, _) => false,
519 Self::Multiple | Self::NonIdent => true,
520 }
521 }
522}
523
524fn ident_difference_expr(left: &Expr, right: &Expr) -> IdentDifference {
525 ident_difference_expr_with_base_location(left, right, IdentLocation::default()).0
526}
527
528fn ident_difference_expr_with_base_location(
529 left: &Expr,
530 right: &Expr,
531 mut base: IdentLocation,
532) -> (IdentDifference, IdentLocation) {
533 // Ideally, this function should not use IdentIter because it should return
534 // early if the expressions have any non-ident differences. We want that early
535 // return because if without that restriction the lint would lead to false
536 // positives.
537 //
538 // But, we cannot (easily?) use a `rustc_ast::visit::Visitor`, since we need
539 // the two expressions to be walked in lockstep. And without a `Visitor`, we'd
540 // have to do all the AST traversal ourselves, which is a lot of work, since to
541 // do it properly we'd need to be able to handle more or less every possible
542 // AST node since `Item`s can be written inside `Expr`s.
543 //
544 // In practice, it seems likely that expressions, above a certain size, that
545 // happen to use the exact same idents in the exact same order, and which are
546 // not structured the same, would be rare. Therefore it seems likely that if
547 // we do only the first layer of matching ourselves and eventually fallback on
548 // IdentIter, then the output of this function will be almost always be correct
549 // in practice.
550 //
551 // If it turns out that problematic cases are more prelavent than we assume,
552 // then we should be able to change this function to do the correct traversal,
553 // without needing to change the rest of the code.
554
555 #![allow(clippy::enum_glob_use)]
556 use ExprKind::*;
557
558 match (
559 &strip_non_ident_wrappers(left).kind,
560 &strip_non_ident_wrappers(right).kind,
561 ) {
562 (Yield(_), Yield(_))
563 | (Try(_), Try(_))
564 | (Paren(_), Paren(_))
565 | (Repeat(_, _), Repeat(_, _))
566 | (Struct(_), Struct(_))
567 | (MacCall(_), MacCall(_))
568 | (LlvmInlineAsm(_), LlvmInlineAsm(_))
569 | (InlineAsm(_), InlineAsm(_))
570 | (Ret(_), Ret(_))
571 | (Continue(_), Continue(_))
572 | (Break(_, _), Break(_, _))
573 | (AddrOf(_, _, _), AddrOf(_, _, _))
574 | (Path(_, _), Path(_, _))
575 | (Range(_, _, _), Range(_, _, _))
576 | (Index(_, _), Index(_, _))
577 | (Field(_, _), Field(_, _))
578 | (AssignOp(_, _, _), AssignOp(_, _, _))
579 | (Assign(_, _, _), Assign(_, _, _))
580 | (TryBlock(_), TryBlock(_))
581 | (Await(_), Await(_))
582 | (Async(_, _, _), Async(_, _, _))
583 | (Block(_, _), Block(_, _))
584 | (Closure(_, _, _, _, _, _), Closure(_, _, _, _, _, _))
585 | (Match(_, _), Match(_, _))
586 | (Loop(_, _), Loop(_, _))
587 | (ForLoop(_, _, _, _), ForLoop(_, _, _, _))
588 | (While(_, _, _), While(_, _, _))
589 | (If(_, _, _), If(_, _, _))
590 | (Let(_, _), Let(_, _))
591 | (Type(_, _), Type(_, _))
592 | (Cast(_, _), Cast(_, _))
593 | (Lit(_), Lit(_))
594 | (Unary(_, _), Unary(_, _))
595 | (Binary(_, _, _), Binary(_, _, _))
596 | (Tup(_), Tup(_))
597 | (MethodCall(_, _, _), MethodCall(_, _, _))
598 | (Call(_, _), Call(_, _))
599 | (ConstBlock(_), ConstBlock(_))
600 | (Array(_), Array(_))
601 | (Box(_), Box(_)) => {
602 // keep going
603 },
604 _ => {
605 return (IdentDifference::NonIdent, base);
606 },
607 }
608
609 let mut difference = IdentDifference::NoDifference;
610
611 for (left_attr, right_attr) in left.attrs.iter().zip(right.attrs.iter()) {
612 let (new_difference, new_base) =
613 ident_difference_via_ident_iter_with_base_location(left_attr, right_attr, base);
614 base = new_base;
615 difference += new_difference;
616 if difference.is_complete() {
617 return (difference, base);
618 }
619 }
620
621 let (new_difference, new_base) = ident_difference_via_ident_iter_with_base_location(left, right, base);
622 base = new_base;
623 difference += new_difference;
624
625 (difference, base)
626}
627
628fn ident_difference_via_ident_iter_with_base_location<Iterable: Into<IdentIter>>(
629 left: Iterable,
630 right: Iterable,
631 mut base: IdentLocation,
632) -> (IdentDifference, IdentLocation) {
633 // See the note in `ident_difference_expr_with_base_location` about `IdentIter`
634 let mut difference = IdentDifference::NoDifference;
635
636 let mut left_iterator = left.into();
637 let mut right_iterator = right.into();
638
639 loop {
640 match (left_iterator.next(), right_iterator.next()) {
641 (Some(left_ident), Some(right_ident)) => {
642 if !eq_id(left_ident, right_ident) {
643 difference += IdentDifference::Single(base);
644 if difference.is_complete() {
645 return (difference, base);
646 }
647 }
648 },
649 (Some(_), None) | (None, Some(_)) => {
650 return (IdentDifference::NonIdent, base);
651 },
652 (None, None) => {
653 return (difference, base);
654 },
655 }
656 base += IdentLocation { index: 1 };
657 }
658}
659
660fn get_ident(expr: &Expr, location: IdentLocation) -> Option<Ident> {
661 IdentIter::from(expr).nth(location.index)
662}
663
664fn suggestion_with_swapped_ident(
665 cx: &EarlyContext<'_>,
666 expr: &Expr,
667 location: IdentLocation,
668 new_ident: Ident,
669 applicability: &mut Applicability,
670) -> Option<String> {
671 get_ident(expr, location).and_then(|current_ident| {
672 if eq_id(current_ident, new_ident) {
673 // We never want to suggest a non-change
674 return None;
675 }
676
677 Some(format!(
678 "{}{}{}",
679 snippet_with_applicability(cx, expr.span.with_hi(current_ident.span.lo()), "..", applicability),
680 new_ident.to_string(),
681 snippet_with_applicability(cx, expr.span.with_lo(current_ident.span.hi()), "..", applicability),
682 ))
683 })
684}
685
686fn skip_index<A, Iter>(iter: Iter, index: usize) -> impl Iterator<Item = A>
687where
688 Iter: Iterator<Item = A>,
689{
690 iter.enumerate()
691 .filter_map(move |(i, a)| if i == index { None } else { Some(a) })
692}