use rowan::NodeOrToken;
use rowan::ast::AstNode as _;
use crate::ast::CallExpr;
use crate::linter::diagnostic::{Diagnostic, Fix, Severity, ViolationData};
use crate::linter::rules::matchers;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken};
pub struct OuterNegation;
const EXAMPLES: &[Example] = &[Example {
caption: "Negating every element of an aggregation:",
source: "if (any(!ok)) stop()\n",
}];
impl Rule for OuterNegation {
fn id(&self) -> &'static str {
"outer-negation"
}
fn description(&self) -> &'static str {
"Flag `any(!x)` / `all(!x)`, which by De Morgan's law read more clearly \
with the negation pulled outside: `!all(x)` and `!any(x)`.\n\nThe rule \
fires only when *every* positional argument is negated (a `na.rm` \
argument is allowed and preserved). The fix is withheld when the call \
sits in a context that binds tighter than `!`, where the rewrite would \
need parentheses."
}
fn examples(&self) -> &'static [Example] {
EXAMPLES
}
fn interests(&self) -> &'static [SyntaxKind] {
&[SyntaxKind::CALL_EXPR]
}
fn check(&self, el: &SyntaxElement, _ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(node) = el.as_node() else {
return;
};
let Some(call) = CallExpr::cast(node.clone()) else {
return;
};
let other = match matchers::callee_name(&call).as_deref() {
Some("any") => "all",
Some("all") => "any",
_ => return,
};
let args = matchers::args(&call);
let mut negated = 0usize;
for arg in &args {
match arg.name.as_deref() {
Some("na.rm") => {}
Some(_) => return,
None => {
let Some(value) = &arg.value else { return };
if bang_of(value).is_none() {
return;
}
negated += 1;
}
}
}
if negated == 0 {
return;
}
let r = call.syntax().text_range();
let fix = if is_safe_context(call.syntax()) {
build_replacement(&call, other).map(|content| {
Fix::safe(
usize::from(r.start()),
usize::from(r.end()),
content,
format!("Rewrite as `!{other}(...)`"),
)
})
} else {
None
};
sink.push(Diagnostic {
rule: "outer-negation",
severity: Severity::Warning,
path: Default::default(),
range: r,
message: ViolationData::new(
"outer-negation",
"negating an aggregation is clearer with the negation outside",
)
.with_suggestion("`any(!x)` is `!all(x)`; `all(!x)` is `!any(x)`."),
fix,
});
}
}
fn bang_of(value: &SyntaxElement) -> Option<SyntaxToken> {
let node = value.as_node()?;
if node.kind() != SyntaxKind::UNARY_EXPR {
return None;
}
let tok = node.children_with_tokens().find_map(|e| e.into_token())?;
(tok.kind() == SyntaxKind::BANG).then_some(tok)
}
fn build_replacement(call: &CallExpr, other: &str) -> Option<String> {
let callee = call.callee_token()?;
let mut out = String::from("!");
for child in call.syntax().children_with_tokens() {
match child {
NodeOrToken::Token(t) if t.text_range() == callee.text_range() => out.push_str(other),
NodeOrToken::Node(n) if n.kind() == SyntaxKind::ARG_LIST => {
out.push_str(&render_arg_list(&n));
}
el => out.push_str(&matchers::element_text(&el)),
}
}
Some(out)
}
fn render_arg_list(list: &SyntaxNode) -> String {
let mut out = String::new();
for child in list.children_with_tokens() {
match child {
NodeOrToken::Node(n) if n.kind() == SyntaxKind::ARG => out.push_str(&render_arg(&n)),
el => out.push_str(&matchers::element_text(&el)),
}
}
out
}
fn render_arg(arg: &SyntaxNode) -> String {
let text = arg.text().to_string();
let named = arg
.children_with_tokens()
.any(|e| e.kind() == SyntaxKind::ASSIGN_EQ);
if !named
&& let Some(unary) = arg.children().next()
&& unary.kind() == SyntaxKind::UNARY_EXPR
&& let Some(bang) = unary.children_with_tokens().find_map(|e| e.into_token())
&& bang.kind() == SyntaxKind::BANG
{
let rel = usize::from(bang.text_range().start()) - usize::from(arg.text_range().start());
return format!("{}{}", &text[..rel], &text[rel + 1..]);
}
text
}
fn is_safe_context(node: &SyntaxNode) -> bool {
let Some(parent) = node.parent() else {
return true;
};
match parent.kind() {
SyntaxKind::ROOT
| SyntaxKind::BLOCK_EXPR
| SyntaxKind::PAREN_EXPR
| SyntaxKind::ARG
| SyntaxKind::IF_EXPR
| SyntaxKind::WHILE_EXPR
| SyntaxKind::FOR_EXPR
| SyntaxKind::REPEAT_EXPR
| SyntaxKind::ASSIGNMENT_EXPR => true,
SyntaxKind::BINARY_EXPR => matchers::binary_parts(&parent).is_some_and(|(_, op, _)| {
matches!(
op.kind(),
SyntaxKind::AND
| SyntaxKind::AND2
| SyntaxKind::OR
| SyntaxKind::OR2
| SyntaxKind::TILDE
)
}),
SyntaxKind::UNARY_EXPR => parent
.children_with_tokens()
.find_map(|e| e.into_token())
.is_some_and(|t| t.kind() == SyntaxKind::BANG),
_ => false,
}
}