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};
pub struct AnyDuplicated;
const EXAMPLES: &[Example] = &[Example {
caption: "Testing for any duplicate value:",
source: "if (any(duplicated(x))) stop()\n",
}];
impl Rule for AnyDuplicated {
fn id(&self) -> &'static str {
"any-duplicated"
}
fn description(&self) -> &'static str {
"Flag `any(duplicated(x))`, which is the purpose-built `anyDuplicated(x) \
> 0` — 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 `duplicated` resolve \
to base R; a local redefinition of either is left alone. Because the \
replacement is a comparison, the fix is withheld in a context that binds \
tighter than a comparison, where the bare 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) = 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, "duplicated") 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 && is_safe_context(call.syntax())).then(|| {
Fix::safe(
usize::from(r.start()),
usize::from(r.end()),
format!("anyDuplicated({}) > 0", matchers::element_text(&arg)),
"Replace `any(duplicated(x))` with `anyDuplicated(x) > 0`",
)
});
sink.push(Diagnostic {
rule: "any-duplicated",
severity: Severity::Warning,
path: Default::default(),
range: r,
message: ViolationData::new(
"any-duplicated",
"`any(duplicated(x))` is the faster, clearer `anyDuplicated(x) > 0`",
)
.with_suggestion("Use `anyDuplicated(x) > 0`."),
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
}
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,
}
}