use crate::ast::{AstNode, AstToken, BinaryExpr, Condition, Expr};
use crate::linter::diagnostic::Diagnostic;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode};
pub struct ConstantCondition;
fn in_exempt_context(node: &SyntaxNode) -> bool {
node.ancestors().any(|a| match a.kind() {
SyntaxKind::QUOTE_EXPR | SyntaxKind::QUOTE_SYM => true,
SyntaxKind::MACRO_CALL => is_static_macro_call(&a),
_ => false,
})
}
fn is_static_macro_call(call: &SyntaxNode) -> bool {
call.children()
.find(|c| c.kind() == SyntaxKind::MACRO_NAME)
.is_some_and(|name| {
name.descendants_with_tokens()
.filter_map(|e| e.into_token())
.filter(|t| t.kind() == SyntaxKind::IDENT)
.last()
.is_some_and(|t| t.text() == "static")
})
}
impl Rule for ConstantCondition {
fn id(&self) -> &'static str {
"constant-condition"
}
fn description(&self) -> &'static str {
"Flag a `true`/`false` literal used as an `if`/`elseif`/`while` test or \
as an operand of the short-circuit `&&`/`||`. The branch or \
short-circuit is decided before the code runs, so the literal is \
usually leftover debugging or a mistyped name. `while true` is exempt \
as Julia's idiomatic infinite loop. No fix: removing the constant \
means restructuring the branch."
}
fn examples(&self) -> &'static [Example] {
&[
Example {
caption: "A literal `if` test always takes the branch:",
source: "if true\n println(\"always\")\nend\n",
},
Example {
caption: "A literal operand decides `&&` at parse time:",
source: "ok = false && check(x)\n",
},
]
}
fn interests(&self) -> &'static [SyntaxKind] {
&[SyntaxKind::CONDITION, SyntaxKind::BINARY_EXPR]
}
fn check(&self, el: &SyntaxElement, _ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(node) = el.as_node() else { return };
if in_exempt_context(node) {
return;
}
if let Some(cond) = Condition::cast(node.clone()) {
let Some(Expr::Literal(lit)) = cond.expr() else {
return;
};
let Some(tok) = lit.bool_token() else { return };
if tok.kind() == SyntaxKind::TRUE_KW
&& cond
.syntax()
.parent()
.is_some_and(|p| p.kind() == SyntaxKind::WHILE_EXPR)
{
return;
}
sink.push(Diagnostic::new(
self.id(),
lit.syntax().text_range(),
format!("this condition is always `{}`", tok.text()),
));
} else if let Some(bin) = BinaryExpr::cast(node.clone()) {
let Some(op) = bin
.op()
.filter(|op| matches!(op.syntax().kind(), SyntaxKind::AND_AND | SyntaxKind::OR_OR))
else {
return;
};
for operand in [bin.lhs(), bin.rhs()] {
let Some(Expr::Literal(lit)) = operand else {
continue;
};
let Some(tok) = lit.bool_token() else {
continue;
};
sink.push(Diagnostic::new(
self.id(),
lit.syntax().text_range(),
format!("`{}` has a constant `{}` operand", op.text(), tok.text()),
));
}
}
}
}