1 use clippy_utils::ast_utils::{eq_id, is_useless_with_eq_exprs, IdentIter};
2 use clippy_utils::diagnostics::span_lint_and_sugg;
3 use clippy_utils::source::snippet_with_applicability;
4 use core::ops::{Add, AddAssign};
5 use if_chain::if_chain;
6 use rustc_ast::ast::{BinOpKind, Expr, ExprKind, StmtKind};
7 use rustc_data_structures::fx::FxHashSet;
8 use rustc_errors::Applicability;
9 use rustc_lint::{EarlyContext, EarlyLintPass};
10 use rustc_session::{declare_lint_pass, declare_tool_lint};
11 use rustc_span::source_map::Spanned;
12 use rustc_span::symbol::Ident;
15 declare_clippy_lint! {
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.
21 /// ### Why is this bad?
22 /// They are probably bugs and if they aren't then they look like bugs
23 /// and you should add a comment explaining why you are doing such an
24 /// odd set of operations.
26 /// ### Known problems
27 /// There may be some false positives if you are trying to do something
28 /// unusual that happens to look like a typo.
38 /// impl Eq for Vec3 {}
40 /// impl PartialEq for Vec3 {
41 /// fn eq(&self, other: &Self) -> bool {
42 /// // This should trigger the lint because `self.x` is compared to `other.y`
43 /// self.x == other.y && self.y == other.y && self.z == other.z
54 /// // same as above except:
55 /// impl PartialEq for Vec3 {
56 /// fn eq(&self, other: &Self) -> bool {
57 /// // Note we now compare other.x to self.x
58 /// self.x == other.x && self.y == other.y && self.z == other.z
62 #[clippy::version = "1.50.0"]
63 pub SUSPICIOUS_OPERATION_GROUPINGS,
65 "groupings of binary operations that look suspiciously like typos"
68 declare_lint_pass!(SuspiciousOperationGroupings => [SUSPICIOUS_OPERATION_GROUPINGS]);
70 impl EarlyLintPass for SuspiciousOperationGroupings {
71 fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) {
72 if expr.span.from_expansion() {
76 if let Some(binops) = extract_related_binops(&expr.kind) {
77 check_binops(cx, &binops.iter().collect::<Vec<_>>());
79 let mut op_types = Vec::with_capacity(binops.len());
80 // We could use a hashmap, etc. to avoid being O(n*m) here, but
81 // we want the lints to be emitted in a consistent order. Besides,
82 // m, (the number of distinct `BinOpKind`s in `binops`)
83 // will often be small, and does have an upper limit.
84 binops.iter().map(|b| b.op).for_each(|op| {
85 if !op_types.contains(&op) {
90 for op_type in op_types {
91 let ops: Vec<_> = binops.iter().filter(|b| b.op == op_type).collect();
93 check_binops(cx, &ops);
99 fn check_binops(cx: &EarlyContext<'_>, binops: &[&BinaryOp<'_>]) {
100 let binop_count = binops.len();
102 // Single binary operation expressions would likely be false
107 let mut one_ident_difference_count = 0;
108 let mut no_difference_info = None;
109 let mut double_difference_info = None;
110 let mut expected_ident_loc = None;
112 let mut paired_identifiers = FxHashSet::default();
114 for (i, BinaryOp { left, right, op, .. }) in binops.iter().enumerate() {
115 match ident_difference_expr(left, right) {
116 IdentDifference::NoDifference => {
117 if is_useless_with_eq_exprs(*op) {
118 // The `eq_op` lint should catch this in this case.
122 no_difference_info = Some(i);
124 IdentDifference::Single(ident_loc) => {
125 one_ident_difference_count += 1;
126 if let Some(previous_expected) = expected_ident_loc {
127 if previous_expected != ident_loc {
128 // This expression doesn't match the form we're
133 expected_ident_loc = Some(ident_loc);
136 // If there was only a single difference, all other idents
137 // must have been the same, and thus were paired.
138 for id in skip_index(IdentIter::from(*left), ident_loc.index) {
139 paired_identifiers.insert(id);
142 IdentDifference::Double(ident_loc1, ident_loc2) => {
143 double_difference_info = Some((i, ident_loc1, ident_loc2));
145 IdentDifference::Multiple | IdentDifference::NonIdent => {
146 // It's too hard to know whether this is a bug or not.
152 let mut applicability = Applicability::MachineApplicable;
154 if let Some(expected_loc) = expected_ident_loc {
155 match (no_difference_info, double_difference_info) {
156 (Some(i), None) => attempt_to_emit_no_difference_lint(cx, binops, i, expected_loc),
157 (None, Some((double_difference_index, ident_loc1, ident_loc2))) => {
159 if one_ident_difference_count == binop_count - 1;
160 if let Some(binop) = binops.get(double_difference_index);
162 let changed_loc = if ident_loc1 == expected_loc {
164 } else if ident_loc2 == expected_loc {
167 // This expression doesn't match the form we're
172 if let Some(sugg) = ident_swap_sugg(
194 fn attempt_to_emit_no_difference_lint(
195 cx: &EarlyContext<'_>,
196 binops: &[&BinaryOp<'_>],
198 expected_loc: IdentLocation,
200 if let Some(binop) = binops.get(i).copied() {
201 // We need to try and figure out which identifier we should
202 // suggest using instead. Since there could be multiple
203 // replacement candidates in a given expression, and we're
204 // just taking the first one, we may get some bad lint
206 let mut applicability = Applicability::MaybeIncorrect;
208 // We assume that the correct ident is one used elsewhere in
209 // the other binops, in a place that there was a single
210 // difference between idents before.
211 let old_left_ident = get_ident(binop.left, expected_loc);
212 let old_right_ident = get_ident(binop.right, expected_loc);
214 for b in skip_index(binops.iter(), i) {
216 if let (Some(old_ident), Some(new_ident)) =
217 (old_left_ident, get_ident(b.left, expected_loc));
218 if old_ident != new_ident;
219 if let Some(sugg) = suggestion_with_swapped_ident(
230 replace_left_sugg(cx, binop, &sugg, &mut applicability),
238 if let (Some(old_ident), Some(new_ident)) =
239 (old_right_ident, get_ident(b.right, expected_loc));
240 if old_ident != new_ident;
241 if let Some(sugg) = suggestion_with_swapped_ident(
252 replace_right_sugg(cx, binop, &sugg, &mut applicability),
262 fn emit_suggestion(cx: &EarlyContext<'_>, span: Span, sugg: String, applicability: Applicability) {
265 SUSPICIOUS_OPERATION_GROUPINGS,
267 "this sequence of operators looks suspiciously like a bug",
275 cx: &EarlyContext<'_>,
276 paired_identifiers: &FxHashSet<Ident>,
277 binop: &BinaryOp<'_>,
278 location: IdentLocation,
279 applicability: &mut Applicability,
280 ) -> Option<String> {
281 let left_ident = get_ident(binop.left, location)?;
282 let right_ident = get_ident(binop.right, location)?;
285 paired_identifiers.contains(&left_ident),
286 paired_identifiers.contains(&right_ident),
288 (true, true) | (false, false) => {
289 // We don't have a good guess of what ident should be
290 // used instead, in these cases.
291 *applicability = Applicability::MaybeIncorrect;
293 // We arbitrarily choose one side to suggest changing,
294 // since we don't have a better guess. If the user
295 // ends up duplicating a clause, the `logic_bug` lint
298 let right_suggestion = suggestion_with_swapped_ident(cx, binop.right, location, left_ident, applicability)?;
300 replace_right_sugg(cx, binop, &right_suggestion, applicability)
303 // We haven't seen a pair involving the left one, so
304 // it's probably what is wanted.
306 let right_suggestion = suggestion_with_swapped_ident(cx, binop.right, location, left_ident, applicability)?;
308 replace_right_sugg(cx, binop, &right_suggestion, applicability)
311 // We haven't seen a pair involving the right one, so
312 // it's probably what is wanted.
313 let left_suggestion = suggestion_with_swapped_ident(cx, binop.left, location, right_ident, applicability)?;
315 replace_left_sugg(cx, binop, &left_suggestion, applicability)
322 fn replace_left_sugg(
323 cx: &EarlyContext<'_>,
324 binop: &BinaryOp<'_>,
325 left_suggestion: &str,
326 applicability: &mut Applicability,
331 binop.op.to_string(),
332 snippet_with_applicability(cx, binop.right.span, "..", applicability),
336 fn replace_right_sugg(
337 cx: &EarlyContext<'_>,
338 binop: &BinaryOp<'_>,
339 right_suggestion: &str,
340 applicability: &mut Applicability,
344 snippet_with_applicability(cx, binop.left.span, "..", applicability),
345 binop.op.to_string(),
350 #[derive(Clone, Debug)]
351 struct BinaryOp<'exprs> {
358 impl<'exprs> BinaryOp<'exprs> {
359 fn new(op: BinOpKind, span: Span, (left, right): (&'exprs Expr, &'exprs Expr)) -> Self {
360 Self { op, span, left, right }
364 fn strip_non_ident_wrappers(expr: &Expr) -> &Expr {
365 let mut output = expr;
367 output = match &output.kind {
368 ExprKind::Paren(ref inner) | ExprKind::Unary(_, ref inner) => inner,
376 fn extract_related_binops(kind: &ExprKind) -> Option<Vec<BinaryOp<'_>>> {
377 append_opt_vecs(chained_binops(kind), if_statement_binops(kind))
380 fn if_statement_binops(kind: &ExprKind) -> Option<Vec<BinaryOp<'_>>> {
382 ExprKind::If(ref condition, _, _) => chained_binops(&condition.kind),
383 ExprKind::Paren(ref e) => if_statement_binops(&e.kind),
384 ExprKind::Block(ref block, _) => {
385 let mut output = None;
386 for stmt in &block.stmts {
388 StmtKind::Expr(ref e) | StmtKind::Semi(ref e) => {
389 output = append_opt_vecs(output, if_statement_binops(&e.kind));
400 fn append_opt_vecs<A>(target_opt: Option<Vec<A>>, source_opt: Option<Vec<A>>) -> Option<Vec<A>> {
401 match (target_opt, source_opt) {
402 (Some(mut target), Some(source)) => {
403 target.reserve(source.len());
409 (Some(v), None) | (None, Some(v)) => Some(v),
410 (None, None) => None,
414 fn chained_binops(kind: &ExprKind) -> Option<Vec<BinaryOp<'_>>> {
416 ExprKind::Binary(_, left_outer, right_outer) => chained_binops_helper(left_outer, right_outer),
417 ExprKind::Paren(ref e) | ExprKind::Unary(_, ref e) => chained_binops(&e.kind),
422 fn chained_binops_helper<'expr>(left_outer: &'expr Expr, right_outer: &'expr Expr) -> Option<Vec<BinaryOp<'expr>>> {
423 match (&left_outer.kind, &right_outer.kind) {
425 ExprKind::Paren(ref left_e) | ExprKind::Unary(_, ref left_e),
426 ExprKind::Paren(ref right_e) | ExprKind::Unary(_, ref right_e),
427 ) => chained_binops_helper(left_e, right_e),
428 (ExprKind::Paren(ref left_e) | ExprKind::Unary(_, ref left_e), _) => chained_binops_helper(left_e, right_outer),
429 (_, ExprKind::Paren(ref right_e) | ExprKind::Unary(_, ref right_e)) => {
430 chained_binops_helper(left_outer, right_e)
433 ExprKind::Binary(Spanned { node: left_op, .. }, ref left_left, ref left_right),
434 ExprKind::Binary(Spanned { node: right_op, .. }, ref right_left, ref right_right),
436 chained_binops_helper(left_left, left_right),
437 chained_binops_helper(right_left, right_right),
439 (Some(mut left_ops), Some(right_ops)) => {
440 left_ops.reserve(right_ops.len());
441 for op in right_ops {
446 (Some(mut left_ops), _) => {
447 left_ops.push(BinaryOp::new(*right_op, right_outer.span, (right_left, right_right)));
450 (_, Some(mut right_ops)) => {
451 right_ops.insert(0, BinaryOp::new(*left_op, left_outer.span, (left_left, left_right)));
454 (None, None) => Some(vec![
455 BinaryOp::new(*left_op, left_outer.span, (left_left, left_right)),
456 BinaryOp::new(*right_op, right_outer.span, (right_left, right_right)),
463 #[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
464 struct IdentLocation {
468 impl Add for IdentLocation {
469 type Output = IdentLocation;
471 fn add(self, other: Self) -> Self::Output {
473 index: self.index + other.index,
478 impl AddAssign for IdentLocation {
479 fn add_assign(&mut self, other: Self) {
480 *self = *self + other;
484 #[derive(Clone, Copy, Debug)]
485 enum IdentDifference {
487 Single(IdentLocation),
488 Double(IdentLocation, IdentLocation),
493 impl Add for IdentDifference {
494 type Output = IdentDifference;
496 fn add(self, other: Self) -> Self::Output {
497 match (self, other) {
498 (Self::NoDifference, output) | (output, Self::NoDifference) => output,
500 | (_, Self::Multiple)
501 | (Self::Double(_, _), Self::Single(_))
502 | (Self::Single(_) | Self::Double(_, _), Self::Double(_, _)) => Self::Multiple,
503 (Self::NonIdent, _) | (_, Self::NonIdent) => Self::NonIdent,
504 (Self::Single(il1), Self::Single(il2)) => Self::Double(il1, il2),
509 impl AddAssign for IdentDifference {
510 fn add_assign(&mut self, other: Self) {
511 *self = *self + other;
515 impl IdentDifference {
516 /// Returns true if learning about more differences will not change the value
517 /// of this `IdentDifference`, and false otherwise.
518 fn is_complete(&self) -> bool {
520 Self::NoDifference | Self::Single(_) | Self::Double(_, _) => false,
521 Self::Multiple | Self::NonIdent => true,
526 fn ident_difference_expr(left: &Expr, right: &Expr) -> IdentDifference {
527 ident_difference_expr_with_base_location(left, right, IdentLocation::default()).0
530 fn ident_difference_expr_with_base_location(
533 mut base: IdentLocation,
534 ) -> (IdentDifference, IdentLocation) {
535 // Ideally, this function should not use IdentIter because it should return
536 // early if the expressions have any non-ident differences. We want that early
537 // return because if without that restriction the lint would lead to false
540 // But, we cannot (easily?) use a `rustc_ast::visit::Visitor`, since we need
541 // the two expressions to be walked in lockstep. And without a `Visitor`, we'd
542 // have to do all the AST traversal ourselves, which is a lot of work, since to
543 // do it properly we'd need to be able to handle more or less every possible
544 // AST node since `Item`s can be written inside `Expr`s.
546 // In practice, it seems likely that expressions, above a certain size, that
547 // happen to use the exact same idents in the exact same order, and which are
548 // not structured the same, would be rare. Therefore it seems likely that if
549 // we do only the first layer of matching ourselves and eventually fallback on
550 // IdentIter, then the output of this function will be almost always be correct
553 // If it turns out that problematic cases are more prevalent than we assume,
554 // then we should be able to change this function to do the correct traversal,
555 // without needing to change the rest of the code.
557 #![allow(clippy::enum_glob_use)]
561 &strip_non_ident_wrappers(left).kind,
562 &strip_non_ident_wrappers(right).kind,
566 | (Paren(_), Paren(_))
567 | (Repeat(_, _), Repeat(_, _))
568 | (Struct(_), Struct(_))
569 | (MacCall(_), MacCall(_))
570 | (InlineAsm(_), InlineAsm(_))
572 | (Continue(_), Continue(_))
573 | (Break(_, _), Break(_, _))
574 | (AddrOf(_, _, _), AddrOf(_, _, _))
575 | (Path(_, _), Path(_, _))
576 | (Range(_, _, _), Range(_, _, _))
577 | (Index(_, _), Index(_, _))
578 | (Field(_, _), Field(_, _))
579 | (AssignOp(_, _, _), AssignOp(_, _, _))
580 | (Assign(_, _, _), Assign(_, _, _))
581 | (TryBlock(_), TryBlock(_))
582 | (Await(_), Await(_))
583 | (Async(_, _, _), Async(_, _, _))
584 | (Block(_, _), Block(_, _))
585 | (Closure(_, _, _, _, _, _, _), Closure(_, _, _, _, _, _, _))
586 | (Match(_, _), Match(_, _))
587 | (Loop(_, _), Loop(_, _))
588 | (ForLoop(_, _, _, _), ForLoop(_, _, _, _))
589 | (While(_, _, _), While(_, _, _))
590 | (If(_, _, _), If(_, _, _))
591 | (Let(_, _, _), Let(_, _, _))
592 | (Type(_, _), Type(_, _))
593 | (Cast(_, _), Cast(_, _))
595 | (Unary(_, _), Unary(_, _))
596 | (Binary(_, _, _), Binary(_, _, _))
598 | (MethodCall(_, _, _, _), MethodCall(_, _, _, _))
599 | (Call(_, _), Call(_, _))
600 | (ConstBlock(_), ConstBlock(_))
601 | (Array(_), Array(_))
602 | (Box(_), Box(_)) => {
606 return (IdentDifference::NonIdent, base);
610 let mut difference = IdentDifference::NoDifference;
612 for (left_attr, right_attr) in left.attrs.iter().zip(right.attrs.iter()) {
613 let (new_difference, new_base) =
614 ident_difference_via_ident_iter_with_base_location(left_attr, right_attr, base);
616 difference += new_difference;
617 if difference.is_complete() {
618 return (difference, base);
622 let (new_difference, new_base) = ident_difference_via_ident_iter_with_base_location(left, right, base);
624 difference += new_difference;
629 fn ident_difference_via_ident_iter_with_base_location<Iterable: Into<IdentIter>>(
632 mut base: IdentLocation,
633 ) -> (IdentDifference, IdentLocation) {
634 // See the note in `ident_difference_expr_with_base_location` about `IdentIter`
635 let mut difference = IdentDifference::NoDifference;
637 let mut left_iterator = left.into();
638 let mut right_iterator = right.into();
641 match (left_iterator.next(), right_iterator.next()) {
642 (Some(left_ident), Some(right_ident)) => {
643 if !eq_id(left_ident, right_ident) {
644 difference += IdentDifference::Single(base);
645 if difference.is_complete() {
646 return (difference, base);
650 (Some(_), None) | (None, Some(_)) => {
651 return (IdentDifference::NonIdent, base);
654 return (difference, base);
657 base += IdentLocation { index: 1 };
661 fn get_ident(expr: &Expr, location: IdentLocation) -> Option<Ident> {
662 IdentIter::from(expr).nth(location.index)
665 fn suggestion_with_swapped_ident(
666 cx: &EarlyContext<'_>,
668 location: IdentLocation,
670 applicability: &mut Applicability,
671 ) -> Option<String> {
672 get_ident(expr, location).and_then(|current_ident| {
673 if eq_id(current_ident, new_ident) {
674 // We never want to suggest a non-change
680 snippet_with_applicability(cx, expr.span.with_hi(current_ident.span.lo()), "..", applicability),
682 snippet_with_applicability(cx, expr.span.with_lo(current_ident.span.hi()), "..", applicability),
687 fn skip_index<A, Iter>(iter: Iter, index: usize) -> impl Iterator<Item = A>
689 Iter: Iterator<Item = A>,
692 .filter_map(move |(i, a)| if i == index { None } else { Some(a) })