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};
pub struct ForLoopDupIndex;
const EXAMPLES: &[Example] = &[Example {
caption: "The inner loop overwrites the outer loop's counter:",
source: "for (i in 1:10) {\n for (i in 1:5) {\n print(i)\n }\n}\n",
}];
impl Rule for ForLoopDupIndex {
fn id(&self) -> &'static str {
"for-loop-dup-index"
}
fn description(&self) -> &'static str {
"Flag a nested `for` loop that reuses the index variable of an \
enclosing `for` loop. R loops introduce no scope, so the inner loop \
overwrites the outer index rather than shadowing it: the outer loop \
resumes with a corrupted counter and any later read of the name sees \
the inner loop's last value.\n\nA loop nested inside a *function* \
defined in the outer body is not flagged—it runs in its own frame and \
leaves the outer index alone. No fix is offered, since the repair is to \
invent a new index 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(inner) = ForExpr::cast(node.clone()) else {
return;
};
let Some(clause) = matchers::for_clause(&inner) else {
return;
};
let name = clause.index.text();
let mut shadowed = false;
for ancestor in node.ancestors().skip(1) {
if ancestor.kind() == SyntaxKind::FUNCTION_EXPR {
break;
}
let Some(candidate) = ForExpr::cast(ancestor.clone()) else {
continue;
};
let in_body = candidate
.body_element()
.is_some_and(|body| body.text_range().contains_range(node.text_range()));
if in_body && matchers::for_clause(&candidate).is_some_and(|c| c.index.text() == name) {
shadowed = true;
break;
}
}
if !shadowed {
return;
}
sink.push(Diagnostic {
rule: "for-loop-dup-index",
severity: Default::default(),
path: Default::default(),
range: clause.range(),
message: ViolationData::new(
"for-loop-dup-index",
format!("loop index `{name}` is already the index of an enclosing `for` loop"),
)
.with_suggestion(format!(
"Rename this loop index so it does not overwrite the enclosing loop's `{name}`."
)),
fix: None,
});
}
}