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