use rowan::ast::AstNode as _;
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 Lengths;
const EXAMPLES: &[Example] = &[Example {
caption: "Per-element lengths of a list:",
source: "n <- sapply(x, length)\n",
}];
impl Rule for Lengths {
fn id(&self) -> &'static str {
"lengths"
}
fn description(&self) -> &'static str {
"Flag `sapply(x, length)`, which is the purpose-built `lengths(x)`—\
faster (a single C pass instead of an R call per element) and clearer. \
Both return an integer vector and keep `x`'s names by default.\n\nThe \
rule fires only on the clean two-positional-argument shape and only \
when `sapply` resolves to base R and `length` is not locally rebound; \
a 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, "sapply") else {
return;
};
let valued: Vec<_> = matchers::args(&call)
.into_iter()
.filter(|a| a.value.is_some())
.collect();
if valued.len() != 2 || valued.iter().any(|a| a.name.is_some()) {
return;
}
let data = valued[0].value.clone().expect("value-bearing by filter");
let fun = valued[1].value.clone().expect("value-bearing by filter");
let Some(fun_token) = fun.as_token() else {
return;
};
if fun_token.kind() != SyntaxKind::IDENT || fun_token.text() != "length" {
return;
}
if !ctx.resolves_to_base(&call) || !ctx.read_resolves_to_base(fun_token) {
return;
}
let r = call.syntax().text_range();
let data_range = data.text_range();
let drops_comment = call
.syntax()
.descendants_with_tokens()
.any(|e| e.kind() == SyntaxKind::COMMENT && !data_range.contains_range(e.text_range()));
let fix = (!drops_comment).then(|| {
Fix::safe(
usize::from(r.start()),
usize::from(r.end()),
format!("lengths({})", matchers::element_text(&data)),
"Replace `sapply(x, length)` with `lengths(x)`",
)
});
sink.push(Diagnostic {
rule: "lengths",
severity: Default::default(),
path: Default::default(),
range: r,
message: ViolationData::new(
"lengths",
"`sapply(x, length)` is the faster, clearer `lengths(x)`",
)
.with_suggestion("Use `lengths(x)`."),
fix,
});
}
}