use crate::linter::diagnostic::Diagnostic;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::semantic::{Access, Binding, BindingId, BindingKind, ScopeId, ScopeKind, SemanticModel};
pub struct LoopVariableShadow;
impl Rule for LoopVariableShadow {
fn id(&self) -> &'static str {
"loop-variable-shadow"
}
fn description(&self) -> &'static str {
"Flag a `for` loop whose index variable is already an enclosing `for` \
loop's index, and an assignment to a loop variable inside its own \
loop. The nested `for` binds a fresh variable, so the outer index is \
unreachable inside it — usually a copy-pasted inner loop whose index \
was never renamed. An assignment to a loop variable is discarded at \
the next iteration, since `for` rebinds the variable from the \
iterator on every pass, so it can never steer the iteration. \
Comprehension and generator clauses are left alone, as is reuse \
across a function body, a closure, or a `do` block. No fix: renaming \
an index or dropping an assignment changes what the body computes."
}
fn examples(&self) -> &'static [Example] {
&[
Example {
caption: "A nested loop reusing the enclosing loop's index:",
source: "for i in 1:3\n for i in 1:2\n println(i)\n end\nend\n",
},
Example {
caption: "An assignment the next iteration discards:",
source: "for i in 1:10\n if isodd(i)\n i += 1\n end\n println(i)\nend\n",
},
]
}
fn check_file(&self, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let model = ctx.model;
for (index, binding) in model.bindings().iter().enumerate() {
if !is_loop_variable(model, binding) {
continue;
}
let id = BindingId(index as u32);
if let Some(outer) = enclosing_loop_variable(model, binding) {
sink.push(Diagnostic::new(
self.id(),
binding.def_range,
format!(
"loop variable `{}` shadows the enclosing loop's variable",
model.binding(outer).name
),
));
}
for occurrence in model.occurrences(id) {
if occurrence.is_def
|| !matches!(occurrence.access, Access::Write | Access::ReadWrite)
{
continue;
}
sink.push(Diagnostic::new(
self.id(),
occurrence.range,
format!(
"assignment to loop variable `{}` is discarded at the next iteration",
binding.name
),
));
}
}
}
}
fn is_loop_variable(model: &SemanticModel, binding: &Binding) -> bool {
binding.kind == BindingKind::ForVar && model.scope(binding.scope).kind == ScopeKind::For
}
fn enclosing_loop_variable(model: &SemanticModel, binding: &Binding) -> Option<BindingId> {
let mut cursor = model.scope(binding.scope).parent;
while let Some(id) = cursor {
let scope = model.scope(id);
if scope.kind.is_global() || scope.kind == ScopeKind::Function {
return None;
}
if scope.kind == ScopeKind::For
&& let Some(outer) = same_named_loop_variable(model, id, binding)
{
return Some(outer);
}
cursor = scope.parent;
}
None
}
fn same_named_loop_variable(
model: &SemanticModel,
scope: ScopeId,
binding: &Binding,
) -> Option<BindingId> {
model
.scope(scope)
.bindings
.iter()
.copied()
.find(|&candidate| {
let outer = model.binding(candidate);
outer.kind == BindingKind::ForVar && outer.name == binding.name
})
}