use crate::ast::{AstNode, AstToken, BinaryExpr, Expr};
use crate::linter::diagnostic::{Applicability, Diagnostic, Fix};
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::syntax::{SyntaxElement, SyntaxKind};
pub struct NothingComparison;
impl Rule for NothingComparison {
fn id(&self) -> &'static str {
"nothing-comparison"
}
fn description(&self) -> &'static str {
"Flag `x == nothing` / `x != nothing`, which compares against `nothing` \
by value. `nothing` is the singleton instance of `Nothing`, so an \
identity test (`===` / `!==`, or `isnothing`) is meant: it is faster and \
cannot be overloaded. The rule reports a safe fix rewriting `==` to \
`===` and `!=` to `!==`."
}
fn examples(&self) -> &'static [Example] {
&[Example {
caption: "Comparing against `nothing` by value:",
source: "if x == nothing\n 1\nend\n",
}]
}
fn interests(&self) -> &'static [SyntaxKind] {
&[SyntaxKind::BINARY_EXPR]
}
fn check(&self, el: &SyntaxElement, _ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(bin) = el.as_node().cloned().and_then(BinaryExpr::cast) else {
return;
};
let Some(op) = bin.op() else { return };
let replacement = match op.syntax().kind() {
SyntaxKind::EQ_EQ => "===",
SyntaxKind::NOT_EQ => "!==",
_ => return,
};
let is_nothing = |operand: Option<Expr>| {
matches!(operand, Some(Expr::Name(name))
if name.ident().is_some_and(|id| id.text() == "nothing"))
};
if !is_nothing(bin.lhs()) && !is_nothing(bin.rhs()) {
return;
}
let op_range = op.syntax().text_range();
let mut diag = Diagnostic::new(
self.id(),
bin.syntax().text_range(),
format!("comparison against `nothing` by value; use `{replacement}` or `isnothing`"),
);
diag.fixes.push(Fix {
description: format!("Replace `{}` with `{replacement}`", op.text()),
content: replacement.to_string(),
start: op_range.start().into(),
end: op_range.end().into(),
applicability: Applicability::Safe,
});
sink.push(diag);
}
}