LoopBodyScopeScanner.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Carsten Hammer
*******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.helper;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.DoStatement;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.ForStatement;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.PostfixExpression;
import org.eclipse.jdt.core.dom.PrefixExpression;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.WhileStatement;
import org.sandbox.functional.core.tree.ScopeInfo;
/**
* Lightweight scanner that analyzes a loop body to populate {@link ScopeInfo}.
*
* <p>This scanner identifies:
* <ul>
* <li>Variables accessed from the outer scope</li>
* <li>Variables modified within the loop (not effectively final)</li>
* <li>Local variables declared in the loop</li>
* </ul>
* </p>
*
* <p><b>Usage:</b></p>
* <pre>{@code
* LoopBodyScopeScanner scanner = new LoopBodyScopeScanner(loopStatement);
* scanner.scan();
* ScopeInfo scopeInfo = scanner.populateScopeInfo(parentScopeInfo);
* }</pre>
*
* @since 1.0.0
*/
public class LoopBodyScopeScanner {
private final EnhancedForStatement loop;
private final String loopParameterName;
private final Set<String> localVariables = new HashSet<>();
private final Set<String> modifiedVariables = new HashSet<>();
private final Set<String> referencedVariables = new HashSet<>();
/**
* Creates a new scope scanner for the given loop.
*
* @param loop the enhanced for statement to scan
*/
public LoopBodyScopeScanner(EnhancedForStatement loop) {
this.loop = loop;
this.loopParameterName = loop.getParameter().getName().getIdentifier();
}
/**
* Scans the loop body to collect variable information.
*/
public void scan() {
Statement body = loop.getBody();
if (body == null) {
return;
}
// First pass: collect variable declarations
body.accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
String varName = node.getName().getIdentifier();
localVariables.add(varName);
return true;
}
@Override
public boolean visit(SingleVariableDeclaration node) {
String varName = node.getName().getIdentifier();
localVariables.add(varName);
return true;
}
@Override
public boolean visit(EnhancedForStatement node) {
// Don't descend into nested loops - they'll be analyzed separately
return node == loop;
}
@Override
public boolean visit(ForStatement node) {
// Don't descend into nested loops
return false;
}
@Override
public boolean visit(WhileStatement node) {
// Don't descend into nested loops
return false;
}
@Override
public boolean visit(DoStatement node) {
// Don't descend into nested loops
return false;
}
});
// Second pass: collect variable references and modifications
body.accept(new ASTVisitor() {
@Override
public boolean visit(SimpleName node) {
String varName = node.getIdentifier();
// Skip if this is a declaration
if (node.isDeclaration()) {
return true;
}
// Skip the loop parameter itself
if (varName.equals(loopParameterName)) {
return true;
}
// Skip local variables declared in this loop
if (localVariables.contains(varName)) {
return true;
}
// Try to resolve binding to filter out fields
IBinding binding = node.resolveBinding();
if (binding instanceof IVariableBinding varBinding) {
// Skip fields - they're not in local scope
if (varBinding.isField()) {
return true;
}
}
// This is a reference to a variable from outer scope
referencedVariables.add(varName);
// Check if this variable is being modified
if (isModification(node)) {
modifiedVariables.add(varName);
}
return true;
}
@Override
public boolean visit(EnhancedForStatement node) {
// Don't descend into nested loops - they'll be analyzed separately
return node == loop;
}
@Override
public boolean visit(ForStatement node) {
// Don't descend into nested loops
return false;
}
@Override
public boolean visit(WhileStatement node) {
// Don't descend into nested loops
return false;
}
@Override
public boolean visit(DoStatement node) {
// Don't descend into nested loops
return false;
}
});
}
/**
* Checks if a SimpleName node represents a modification of the variable.
*
* <p>This method checks for direct modifications to the variable itself:
* <ul>
* <li>{@code x = 5} - MODIFICATION (x is directly the LHS)</li>
* <li>{@code x++} or {@code ++x} - MODIFICATION</li>
* <li>{@code arr[i] = 5} - NOT a modification of arr (only the element is modified)</li>
* <li>{@code obj.field = 5} - NOT a modification of obj (only the field is modified)</li>
* </ul>
*
* <p>This is correct for lambda capture purposes: modifying array elements or
* object fields doesn't make the variable non-effectively-final. Only direct
* reassignment of the variable itself does.</p>
*
* @param node the SimpleName node to check
* @return true if the variable itself is being modified
*/
private boolean isModification(SimpleName node) {
// Check if node is the DIRECT left-hand side of an assignment
// (not part of a complex LHS like arr[i] or obj.field)
if (node.getParent() instanceof Assignment assignment) {
if (assignment.getLeftHandSide() == node) {
return true;
}
}
// Check if node is part of increment/decrement expression
if (node.getParent() instanceof PostfixExpression postfix) {
PostfixExpression.Operator op = postfix.getOperator();
if (op == PostfixExpression.Operator.INCREMENT || op == PostfixExpression.Operator.DECREMENT) {
return true;
}
}
if (node.getParent() instanceof PrefixExpression prefix) {
PrefixExpression.Operator op = prefix.getOperator();
if (op == PrefixExpression.Operator.INCREMENT || op == PrefixExpression.Operator.DECREMENT) {
return true;
}
}
return false;
}
/**
* Populates the given ScopeInfo with the collected variable information.
*
* @param scopeInfo the ScopeInfo to populate
*/
public void populateScopeInfo(ScopeInfo scopeInfo) {
// Add local variables declared in this loop
for (String varName : localVariables) {
scopeInfo.addLocalVariable(varName);
}
// Add modified variables
for (String varName : modifiedVariables) {
scopeInfo.addModifiedVariable(varName);
}
// Note: outerScopeVariables are handled by ScopeInfo.createChildScope()
// which already propagates parent scope variables. We don't need to
// explicitly add them here, they're already in the ScopeInfo from the parent.
}
/**
* Gets the set of variables referenced from outer scope.
*
* @return unmodifiable set of referenced variable names
*/
public Set<String> getReferencedVariables() {
return Set.copyOf(referencedVariables);
}
/**
* Gets the set of variables modified in this loop.
*
* @return unmodifiable set of modified variable names
*/
public Set<String> getModifiedVariables() {
return Set.copyOf(modifiedVariables);
}
/**
* Gets the set of local variables declared in this loop.
*
* @return unmodifiable set of local variable names
*/
public Set<String> getLocalVariables() {
return Set.copyOf(localVariables);
}
}