use crate::ast::CallExpr;
use crate::linter::diagnostic::{Diagnostic, Fix, ViolationData};
use crate::linter::rules::matchers;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::syntax::{SyntaxElement, SyntaxKind};
pub struct IsNumeric;
const EXAMPLES: &[Example] = &[Example {
caption: "Testing for a numeric vector:",
source: "if (is.numeric(x) || is.integer(x)) mean(x)\n",
}];
impl Rule for IsNumeric {
fn id(&self) -> &'static str {
"is-numeric"
}
fn description(&self) -> &'static str {
"Flag `is.numeric(x) || is.integer(x)` (and the vectorized `|` \
spelling), which is just `is.numeric(x)`: `is.numeric()` already \
returns `TRUE` for integer vectors, so the disjunction adds nothing \
and suggests a misreading of what `is.numeric()` tests.\n\nThe rule \
fires only when both operands are single-argument calls on the same \
argument and both callees resolve to base R; a local redefinition of \
either is left alone."
}
fn examples(&self) -> &'static [Example] {
EXAMPLES
}
fn interests(&self) -> &'static [SyntaxKind] {
&[SyntaxKind::BINARY_EXPR]
}
fn check(&self, el: &SyntaxElement, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(node) = el.as_node() else {
return;
};
let Some((lhs, op, rhs)) = matchers::binary_parts(node) else {
return;
};
if !matches!(op.kind(), SyntaxKind::OR | SyntaxKind::OR2) {
return;
}
let (numeric, integer) = match (type_test(&lhs), type_test(&rhs)) {
(Some((a, a_arg)), Some((b, b_arg))) if a.0 != b.0 => {
if a.0 == "is.numeric" {
((a.1, a_arg), (b.1, b_arg))
} else {
((b.1, b_arg), (a.1, a_arg))
}
}
_ => return,
};
if matchers::element_text(&numeric.1) != matchers::element_text(&integer.1) {
return;
}
if !ctx.resolves_to_base(&numeric.0) || !ctx.resolves_to_base(&integer.0) {
return;
}
let r = node.text_range();
let arg_range = numeric.1.text_range();
let drops_comment = node
.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!("is.numeric({})", matchers::element_text(&numeric.1)),
"Replace the disjunction with `is.numeric(x)`",
)
});
sink.push(Diagnostic {
rule: "is-numeric",
severity: Default::default(),
path: Default::default(),
range: r,
message: ViolationData::new(
"is-numeric",
"`is.numeric(x) || is.integer(x)` is redundant—`is.numeric()` is already \
`TRUE` for integer vectors",
)
.with_suggestion("Use `is.numeric(x)`."),
fix,
});
}
}
fn type_test(el: &SyntaxElement) -> Option<((&'static str, CallExpr), SyntaxElement)> {
let node = el.as_node()?;
for name in ["is.numeric", "is.integer"] {
if let Some(call) = matchers::call_named(node, name) {
let arg = matchers::sole_positional(&call)?;
return Some(((name, call), arg));
}
}
None
}