selene_lib/lints/
constant_table_comparison.rs

1use crate::ast_util::{purge_trivia, range};
2
3use super::*;
4use std::convert::Infallible;
5
6use full_moon::{
7    ast::{self, Ast, BinOp},
8    visitors::Visitor,
9};
10
11pub struct ConstantTableComparisonLint;
12
13impl Lint for ConstantTableComparisonLint {
14    type Config = ();
15    type Error = Infallible;
16
17    const SEVERITY: Severity = Severity::Error;
18    const LINT_TYPE: LintType = LintType::Correctness;
19
20    fn new(_: Self::Config) -> Result<Self, Self::Error> {
21        Ok(ConstantTableComparisonLint)
22    }
23
24    fn pass(&self, ast: &Ast, _: &Context, _: &AstContext) -> Vec<Diagnostic> {
25        let mut visitor = ConstantTableComparisonVisitor {
26            comparisons: Vec::new(),
27        };
28
29        visitor.visit_ast(ast);
30
31        visitor
32            .comparisons
33            .iter()
34            .map(|comparison| {
35                Diagnostic::new_complete(
36                    "constant_table_comparison",
37                    "comparing to a constant table will always fail".to_owned(),
38                    Label::new(comparison.range),
39                    if let Some(empty_side) = comparison.empty_side {
40                        vec![format!(
41                            "try: `next({}) {} nil`",
42                            match empty_side {
43                                EmptyComparison::CheckEmpty(side) => match side {
44                                    EmptyComparisonSide::Left => &comparison.rhs,
45                                    EmptyComparisonSide::Right => &comparison.lhs,
46                                },
47
48                                EmptyComparison::CheckNotEmpty(side) => match side {
49                                    EmptyComparisonSide::Left => &comparison.rhs,
50                                    EmptyComparisonSide::Right => &comparison.lhs,
51                                },
52                            },
53                            match empty_side {
54                                EmptyComparison::CheckEmpty(_) => "==",
55                                EmptyComparison::CheckNotEmpty(_) => "~=",
56                            }
57                        )]
58                    } else {
59                        Vec::new()
60                    },
61                    Vec::new(),
62                )
63            })
64            .collect()
65    }
66}
67
68struct ConstantTableComparisonVisitor {
69    comparisons: Vec<Comparison>,
70}
71
72#[derive(Clone, Copy)]
73enum EmptyComparisonSide {
74    Left,
75    Right,
76}
77
78#[derive(Clone, Copy)]
79enum EmptyComparison {
80    CheckEmpty(EmptyComparisonSide),
81    CheckNotEmpty(EmptyComparisonSide),
82}
83
84struct Comparison {
85    lhs: String,
86    rhs: String,
87    empty_side: Option<EmptyComparison>,
88    range: (usize, usize),
89}
90
91enum ConstantTableMatch {
92    Empty,
93    NotEmpty,
94}
95
96fn constant_table_match(expression: &ast::Expression) -> Option<ConstantTableMatch> {
97    if let ast::Expression::TableConstructor(table_constructor) = expression {
98        return if table_constructor.fields().is_empty() {
99            Some(ConstantTableMatch::Empty)
100        } else {
101            Some(ConstantTableMatch::NotEmpty)
102        };
103    }
104
105    None
106}
107
108impl Visitor for ConstantTableComparisonVisitor {
109    fn visit_expression(&mut self, node: &ast::Expression) {
110        if let ast::Expression::BinaryOperator {
111            lhs,
112            binop:
113                binop @ (BinOp::TwoEqual(_)
114                | BinOp::TildeEqual(_)
115                | BinOp::GreaterThan(_)
116                | BinOp::LessThan(_)
117                | BinOp::GreaterThanEqual(_)
118                | BinOp::LessThanEqual(_)),
119            rhs,
120        } = node
121        {
122            match (constant_table_match(lhs), constant_table_match(rhs)) {
123                // The (Some(_), Some(_)) case is rare, but also blatantly useless.
124                // `{} == {}` translating to `next({}) == nil` is clearly silly.
125                (Some(_), Some(_))
126                | (Some(ConstantTableMatch::NotEmpty), _)
127                | (_, Some(ConstantTableMatch::NotEmpty)) => {
128                    self.comparisons.push(Comparison {
129                        lhs: purge_trivia(lhs).to_string(),
130                        rhs: purge_trivia(rhs).to_string(),
131                        empty_side: None,
132                        range: range(node),
133                    });
134                }
135
136                empty_checks @ ((Some(ConstantTableMatch::Empty), None)
137                | (None, Some(ConstantTableMatch::Empty))) => {
138                    let side = match empty_checks.0.is_some() {
139                        true => EmptyComparisonSide::Left,
140                        false => EmptyComparisonSide::Right,
141                    };
142
143                    self.comparisons.push(Comparison {
144                        lhs: purge_trivia(lhs).to_string(),
145                        rhs: purge_trivia(rhs).to_string(),
146                        empty_side: match binop {
147                            ast::BinOp::TwoEqual(_) => Some(EmptyComparison::CheckEmpty(side)),
148                            ast::BinOp::TildeEqual(_) => Some(EmptyComparison::CheckNotEmpty(side)),
149                            _ => None,
150                        },
151                        range: range(node),
152                    });
153                }
154
155                (None, None) => {}
156            }
157        }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::{super::test_util::test_lint, *};
164
165    #[test]
166    fn test_constant_table_comparison() {
167        test_lint(
168            ConstantTableComparisonLint::new(()).unwrap(),
169            "constant_table_comparison",
170            "constant_table_comparison",
171        );
172    }
173}