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};
pub struct AnyIsNa;
const EXAMPLES: &[Example] = &[Example {
caption: "Testing for any missing value:",
source: "if (any(is.na(x))) stop()\n",
}];
impl Rule for AnyIsNa {
fn id(&self) -> &'static str {
"any-is-na"
}
fn description(&self) -> &'static str {
"Flag `any(is.na(x))`, which is the purpose-built `anyNA(x)` — faster \
(it short-circuits and builds no intermediate logical vector) and \
clearer.\n\nThe rule fires only on the clean single-argument shape and \
only when both `any` and `is.na` resolve to base R; a local \
redefinition of either is left alone."
}
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) = matchers::call_named(node, "any") else {
return;
};
let Some(outer_arg) = sole_positional(&call) else {
return;
};
let Some(inner_node) = outer_arg.as_node() else {
return;
};
let Some(inner) = matchers::call_named(inner_node, "is.na") else {
return;
};
let Some(arg) = sole_positional(&inner) else {
return;
};
if !ctx.resolves_to_base(&call) || !ctx.resolves_to_base(&inner) {
return;
}
let r = call.syntax().text_range();
let arg_range = arg.text_range();
let drops_comment = call
.syntax()
.descendants_with_tokens()
.any(|e| e.kind() == SyntaxKind::COMMENT && !arg_range.contains_range(e.text_range()));
let fix = (!drops_comment).then(|| {
Fix::safe(
usize::from(r.start()),
usize::from(r.end()),
format!("anyNA({})", matchers::element_text(&arg)),
"Replace `any(is.na(x))` with `anyNA(x)`",
)
});
sink.push(Diagnostic {
rule: "any-is-na",
severity: Severity::Warning,
path: Default::default(),
range: r,
message: ViolationData::new(
"any-is-na",
"`any(is.na(x))` is the faster, clearer `anyNA(x)`",
)
.with_suggestion("Use `anyNA(x)`."),
fix,
});
}
}
fn sole_positional(call: &CallExpr) -> Option<SyntaxElement> {
let mut valued = matchers::args(call)
.into_iter()
.filter(|a| a.value.is_some());
let only = valued.next()?;
if valued.next().is_some() || only.name.is_some() {
return None;
}
only.value
}