use crate::ast_util::{purge_trivia, range};
use super::*;
use std::convert::Infallible;
use full_moon::{
ast::{self, Ast, BinOp},
visitors::Visitor,
};
pub struct ConstantTableComparisonLint;
impl Lint for ConstantTableComparisonLint {
type Config = ();
type Error = Infallible;
const SEVERITY: Severity = Severity::Error;
const LINT_TYPE: LintType = LintType::Correctness;
fn new(_: Self::Config) -> Result<Self, Self::Error> {
Ok(ConstantTableComparisonLint)
}
fn pass(&self, ast: &Ast, _: &Context, _: &AstContext) -> Vec<Diagnostic> {
let mut visitor = ConstantTableComparisonVisitor {
comparisons: Vec::new(),
};
visitor.visit_ast(ast);
visitor
.comparisons
.iter()
.map(|comparison| {
Diagnostic::new_complete(
"constant_table_comparison",
"comparing to a constant table will always fail".to_owned(),
Label::new(comparison.range),
if let Some(empty_side) = comparison.empty_side {
vec![format!(
"try: `next({}) {} nil`",
match empty_side {
EmptyComparison::CheckEmpty(side) => match side {
EmptyComparisonSide::Left => &comparison.rhs,
EmptyComparisonSide::Right => &comparison.lhs,
},
EmptyComparison::CheckNotEmpty(side) => match side {
EmptyComparisonSide::Left => &comparison.rhs,
EmptyComparisonSide::Right => &comparison.lhs,
},
},
match empty_side {
EmptyComparison::CheckEmpty(_) => "==",
EmptyComparison::CheckNotEmpty(_) => "~=",
}
)]
} else {
Vec::new()
},
Vec::new(),
)
})
.collect()
}
}
struct ConstantTableComparisonVisitor {
comparisons: Vec<Comparison>,
}
#[derive(Clone, Copy)]
enum EmptyComparisonSide {
Left,
Right,
}
#[derive(Clone, Copy)]
enum EmptyComparison {
CheckEmpty(EmptyComparisonSide),
CheckNotEmpty(EmptyComparisonSide),
}
struct Comparison {
lhs: String,
rhs: String,
empty_side: Option<EmptyComparison>,
range: (usize, usize),
}
enum ConstantTableMatch {
Empty,
NotEmpty,
}
fn constant_table_match(expression: &ast::Expression) -> Option<ConstantTableMatch> {
if let ast::Expression::TableConstructor(table_constructor) = expression {
return if table_constructor.fields().is_empty() {
Some(ConstantTableMatch::Empty)
} else {
Some(ConstantTableMatch::NotEmpty)
};
}
None
}
impl Visitor for ConstantTableComparisonVisitor {
fn visit_expression(&mut self, node: &ast::Expression) {
if let ast::Expression::BinaryOperator {
lhs,
binop:
binop @ (BinOp::TwoEqual(_)
| BinOp::TildeEqual(_)
| BinOp::GreaterThan(_)
| BinOp::LessThan(_)
| BinOp::GreaterThanEqual(_)
| BinOp::LessThanEqual(_)),
rhs,
} = node
{
match (constant_table_match(lhs), constant_table_match(rhs)) {
(Some(_), Some(_))
| (Some(ConstantTableMatch::NotEmpty), _)
| (_, Some(ConstantTableMatch::NotEmpty)) => {
self.comparisons.push(Comparison {
lhs: purge_trivia(lhs).to_string(),
rhs: purge_trivia(rhs).to_string(),
empty_side: None,
range: range(node),
});
}
empty_checks @ ((Some(ConstantTableMatch::Empty), None)
| (None, Some(ConstantTableMatch::Empty))) => {
let side = match empty_checks.0.is_some() {
true => EmptyComparisonSide::Left,
false => EmptyComparisonSide::Right,
};
self.comparisons.push(Comparison {
lhs: purge_trivia(lhs).to_string(),
rhs: purge_trivia(rhs).to_string(),
empty_side: match binop {
ast::BinOp::TwoEqual(_) => Some(EmptyComparison::CheckEmpty(side)),
ast::BinOp::TildeEqual(_) => Some(EmptyComparison::CheckNotEmpty(side)),
_ => None,
},
range: range(node),
});
}
(None, None) => {}
}
}
}
}
#[cfg(test)]
mod tests {
use super::{super::test_util::test_lint, *};
#[test]
fn test_constant_table_comparison() {
test_lint(
ConstantTableComparisonLint::new(()).unwrap(),
"constant_table_comparison",
"constant_table_comparison",
);
}
}