selene_lib/lints/
almost_swapped.rs

1use super::*;
2use crate::ast_util::{purge_trivia, range, HasSideEffects};
3use std::convert::Infallible;
4
5use full_moon::{
6    ast::{self, Ast},
7    visitors::Visitor,
8};
9
10pub struct AlmostSwappedLint;
11
12impl Lint for AlmostSwappedLint {
13    type Config = ();
14    type Error = Infallible;
15
16    const SEVERITY: Severity = Severity::Error;
17    const LINT_TYPE: LintType = LintType::Correctness;
18
19    fn new(_: Self::Config) -> Result<Self, Self::Error> {
20        Ok(AlmostSwappedLint)
21    }
22
23    fn pass(&self, ast: &Ast, _: &Context, _: &AstContext) -> Vec<Diagnostic> {
24        let mut visitor = AlmostSwappedVisitor {
25            almost_swaps: Vec::new(),
26        };
27
28        visitor.visit_ast(ast);
29
30        visitor
31            .almost_swaps
32            .iter()
33            .map(|almost_swap| {
34                Diagnostic::new_complete(
35                    "almost_swapped",
36                    format!(
37                        "this looks like you are trying to swap `{}` and `{}`",
38                        (almost_swap.names.0),
39                        (almost_swap.names.1),
40                    ),
41                    Label::new(almost_swap.range),
42                    vec![format!(
43                        "try: `{name1}, {name2} = {name2}, {name1}`",
44                        name1 = almost_swap.names.0,
45                        name2 = almost_swap.names.1,
46                    )],
47                    Vec::new(),
48                )
49            })
50            .collect()
51    }
52}
53
54struct AlmostSwappedVisitor {
55    almost_swaps: Vec<AlmostSwap>,
56}
57
58struct AlmostSwap {
59    names: (String, String),
60    range: (usize, usize),
61}
62
63impl Visitor for AlmostSwappedVisitor {
64    fn visit_block(&mut self, block: &ast::Block) {
65        let mut last_swap: Option<AlmostSwap> = None;
66
67        for stmt in block.stmts() {
68            if let ast::Stmt::Assignment(assignment) = stmt {
69                let expressions = assignment.expressions();
70                let variables = assignment.variables();
71
72                if variables.len() == 1 && expressions.len() == 1 {
73                    let expr = expressions.into_iter().next().unwrap();
74                    let var = variables.into_iter().next().unwrap();
75
76                    if !var.has_side_effects() {
77                        let expr_end = range(expr).1;
78
79                        let expr_text = purge_trivia(expr).to_string().trim().to_owned();
80                        let var_text = purge_trivia(var).to_string().trim().to_owned();
81
82                        if let Some(last_swap) = last_swap.take() {
83                            if last_swap.names.0 == expr_text && last_swap.names.1 == var_text {
84                                self.almost_swaps.push(AlmostSwap {
85                                    names: last_swap.names.to_owned(),
86                                    range: (last_swap.range.0, expr_end),
87                                });
88                            }
89                        } else {
90                            last_swap = Some(AlmostSwap {
91                                names: (var_text, expr_text),
92                                range: range(stmt),
93                            });
94                        }
95
96                        continue;
97                    }
98                }
99            }
100
101            last_swap = None;
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::{super::test_util::test_lint, *};
109
110    #[test]
111    fn test_almost_swapped() {
112        test_lint(
113            AlmostSwappedLint::new(()).unwrap(),
114            "almost_swapped",
115            "almost_swapped",
116        );
117    }
118
119    #[test]
120    fn test_almost_swapped_panic() {
121        test_lint(
122            AlmostSwappedLint::new(()).unwrap(),
123            "almost_swapped",
124            "panic",
125        );
126    }
127}