use crate::ast::{AstNode, CallExpr, Expr, HasArgList};
use crate::linter::diagnostic::{Applicability, Diagnostic, Fix};
use crate::linter::rules::matchers::{self, CallShape};
use crate::linter::rules::rewrite;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::syntax::{SyntaxElement, SyntaxKind};
pub struct LengthFindall;
impl Rule for LengthFindall {
fn id(&self) -> &'static str {
"length-findall"
}
fn description(&self) -> &'static str {
"Flag `length(findall(p, x))`, which allocates a vector of every \
matching index just to ask how many there are. `count(p, x)` answers \
with a counter and no allocation, and covers the one-argument form \
too: `length(findall(mask))` is `count(mask)`.\n\n\
Both calls must be plain — no keyword arguments, no splats — and both \
`length` and `findall` must be confirmed to be Base's, so a local \
shadow, a qualified `Base.findall`, or a file whose imports cannot be \
resolved reports nothing.\n\n\
The fix rewrites the pair to a `count` call carrying `findall`'s own \
argument list, but needs `--unsafe-fixes`: `findall` walks a \
collection's keys where `count` iterates its elements, which is the \
same walk for an array and a different one for a `Dict`, whose values \
`findall` tests and whose `key => value` pairs `count` does. It is \
withheld — the finding still stands — when the file's `count` is not \
Base's, or when a comment sits in the rewritten span outside the \
argument list."
}
fn examples(&self) -> &'static [Example] {
&[
Example {
caption: "The index vector is built only to be measured:",
source: "n = length(findall(isodd, xs))\n",
},
Example {
caption: "The one-argument form counts a mask:",
source: "hits = length(findall(mask))\n",
},
]
}
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((outer, mut args)) = matchers::plain_call(node, "length", 1) else {
return;
};
let Some(Expr::CallExpr(inner)) = args.pop() else {
return;
};
let Some(findall) = matchers::call_named(inner.syntax(), "findall") else {
return;
};
let shape = CallShape::of(&findall);
if !(shape.is_plain(1) || shape.is_plain(2)) {
return;
}
if !ctx.resolves_to_base(&outer) || !ctx.resolves_to_base(&findall) {
return;
}
let inner_args = shape
.positional
.iter()
.map(|arg| arg.syntax().text().to_string())
.collect::<Vec<_>>()
.join(", ");
let message = if inner_args.contains('\n') {
"call `count` instead of measuring `findall`'s result, which builds \
an index vector just to count it"
.to_string()
} else {
format!(
"call `count({inner_args})` instead of `length(findall({inner_args}))`, \
which builds an index vector just to count it"
)
};
let mut diag = Diagnostic::new(self.id(), outer.syntax().text_range(), message);
if let Some(fix) = count_rewrite(ctx, &outer, &findall) {
diag.fixes.push(fix);
}
sink.push(diag);
}
}
fn count_rewrite(ctx: &RuleContext<'_>, outer: &CallExpr, findall: &CallExpr) -> Option<Fix> {
let span = outer.syntax().text_range();
if !ctx.name_resolves_to_base("count", span.start()) {
return None;
}
let args = findall.arg_list()?;
if rewrite::drops_a_comment(outer.syntax(), &[args.syntax().text_range()]) {
return None;
}
Some(Fix {
description: "Count the matches directly with `count`".to_string(),
content: format!("count{}", args.syntax().text()),
start: span.start().into(),
end: span.end().into(),
applicability: Applicability::Unsafe,
})
}