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 Crossprod;
const EXAMPLES: &[Example] = &[Example {
caption: "Transposed matrix products:",
source: "a <- t(x) %*% y\nb <- x %*% t(y)\n",
}];
struct Rewrite {
fname: &'static str,
shape: &'static str,
replacement: &'static str,
}
const CROSSPROD: Rewrite = Rewrite {
fname: "crossprod",
shape: "t(x) %*% y",
replacement: "crossprod(x, y)",
};
const TCROSSPROD: Rewrite = Rewrite {
fname: "tcrossprod",
shape: "x %*% t(y)",
replacement: "tcrossprod(x, y)",
};
impl Rule for Crossprod {
fn id(&self) -> &'static str {
"crossprod"
}
fn description(&self) -> &'static str {
"Flag `t(x) %*% y` and `x %*% t(y)`, which are the purpose-built \
`crossprod(x, y)` and `tcrossprod(x, y)`—faster (they go straight to \
BLAS and never materialize the transpose that `t()` builds) and \
clearer.\n\nThe rule fires only when one operand is a single-argument \
`t()` call and that `t` resolves to base R; a local redefinition is \
left alone. When both operands are the same symbol the single-argument \
form (`crossprod(x)`) is used."
}
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 op.kind() != SyntaxKind::USER_OP || op.text() != "%*%" {
return;
}
let (rewrite, t_call, first, second) = if let Some((call, arg)) = transposed(&lhs) {
(&CROSSPROD, call, arg, rhs)
} else if let Some((call, arg)) = transposed(&rhs) {
(&TCROSSPROD, call, lhs, arg)
} else {
return;
};
if !ctx.resolves_to_base(&t_call) {
return;
}
let r = node.text_range();
let (first_range, second_range) = (first.text_range(), second.text_range());
let drops_comment = node.descendants_with_tokens().any(|e| {
e.kind() == SyntaxKind::COMMENT
&& !first_range.contains_range(e.text_range())
&& !second_range.contains_range(e.text_range())
});
let content = match (ident_text(&first), ident_text(&second)) {
(Some(a), Some(b)) if a == b => format!("{}({a})", rewrite.fname),
_ => format!(
"{}({}, {})",
rewrite.fname,
matchers::element_text(&first),
matchers::element_text(&second)
),
};
let fix = (!drops_comment).then(|| {
Fix::safe(
usize::from(r.start()),
usize::from(r.end()),
content,
format!("Replace `{}` with `{}`", rewrite.shape, rewrite.replacement),
)
});
sink.push(Diagnostic {
rule: "crossprod",
severity: Default::default(),
path: Default::default(),
range: r,
message: ViolationData::new(
"crossprod",
format!(
"`{}` is the faster, clearer `{}`",
rewrite.shape, rewrite.replacement
),
)
.with_suggestion(format!("Use `{}`.", rewrite.replacement)),
fix,
});
}
}
fn transposed(el: &SyntaxElement) -> Option<(crate::ast::CallExpr, SyntaxElement)> {
let node = el.as_node()?;
let call = matchers::call_named(node, "t")?;
let arg = sole_positional(&call)?;
Some((call, arg))
}
fn ident_text(el: &SyntaxElement) -> Option<String> {
el.as_token()
.filter(|t| t.kind() == SyntaxKind::IDENT)
.map(|t| t.text().to_string())
}
fn sole_positional(call: &crate::ast::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
}