use rowan::ast::AstNode as _;
use crate::ast::ForExpr;
use crate::linter::diagnostic::{Diagnostic, ViolationData};
use crate::linter::rules::matchers;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode};
pub struct ForLoopIndex;
const EXAMPLES: &[Example] = &[
Example {
caption: "The loop index overwrites the vector being iterated over:",
source: "for (x in x) {\n print(x)\n}\n",
},
Example {
caption: "The same mistake one call deep:",
source: "for (i in seq_along(i)) {\n print(i)\n}\n",
},
];
impl Rule for ForLoopIndex {
fn id(&self) -> &'static str {
"for-loop-index"
}
fn description(&self) -> &'static str {
"Flag a `for` loop whose index symbol is also read in its own sequence \
expression, as in `for (x in x)` or `for (x in seq_along(x))`. R \
evaluates the sequence once and then binds the index over it, so the \
original value is destroyed by the first iteration and is not what a \
reader would expect after the loop.\n\nOnly a genuine *read* of the \
name counts: a field name (`for (x in df$x)`), an argument name \
(`for (x in list(x = 1))`), or a read belonging to a function literal \
inside the sequence is not a re-use and is not flagged. No fix is \
offered—the repair is to rename the index or the sequence, which \
means inventing a name."
}
fn examples(&self) -> &'static [Example] {
EXAMPLES
}
fn interests(&self) -> &'static [SyntaxKind] {
&[SyntaxKind::FOR_EXPR]
}
fn check(&self, el: &SyntaxElement, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(node) = el.as_node() else {
return;
};
let Some(for_expr) = ForExpr::cast(node.clone()) else {
return;
};
let Some(clause) = matchers::for_clause(&for_expr) else {
return;
};
let name = clause.index.text();
let reused = ctx.model.idents().iter().any(|ident| {
ident.name == name
&& clause.sequence.contains_range(ident.range)
&& !in_nested_function(node, ident.range)
});
if !reused {
return;
}
sink.push(Diagnostic {
rule: "for-loop-index",
severity: Default::default(),
path: Default::default(),
range: clause.range(),
message: ViolationData::new(
"for-loop-index",
format!("loop index `{name}` is also read in the loop's sequence"),
)
.with_suggestion(format!(
"Rename the loop index so iterating does not overwrite `{name}`."
)),
fix: None,
});
}
}
fn in_nested_function(for_node: &SyntaxNode, range: rowan::TextRange) -> bool {
for_node
.covering_element(range)
.ancestors()
.take_while(|anc| anc != for_node)
.any(|anc| anc.kind() == SyntaxKind::FUNCTION_EXPR)
}