IteratorLoopAnalyzer.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.List;
import java.util.Set;
import org.eclipse.jdt.core.dom.ArrayAccess;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.BreakStatement;
import org.eclipse.jdt.core.dom.ContinueStatement;
import org.eclipse.jdt.core.dom.DoStatement;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.ForStatement;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.PostfixExpression;
import org.eclipse.jdt.core.dom.PrefixExpression;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.SuperFieldAccess;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.WhileStatement;
/** Analyzes iterator loops for safety and convertibility. */
public class IteratorLoopAnalyzer {
/**
* @param isSafe whether the loop is safe for stream conversion
* @param reason reason when unsafe
* @param hasRemove whether {@code iterator.remove()} is used
* @param hasMultipleNext whether multiple {@code next()} calls are used
* @param hasBreak whether a break is present
* @param hasLabeledContinue whether a labeled continue is present
*/
public record SafetyAnalysis(boolean isSafe, String reason, boolean hasRemove,
boolean hasMultipleNext, boolean hasBreak, boolean hasLabeledContinue) {
public static SafetyAnalysis safe() {
return new SafetyAnalysis(true, null, false, false, false, false);
}
public static SafetyAnalysis unsafe(String reason, boolean hasRemove,
boolean hasMultipleNext, boolean hasBreak, boolean hasLabeledContinue) {
return new SafetyAnalysis(false, reason, hasRemove, hasMultipleNext, hasBreak, hasLabeledContinue);
}
}
/**
* Rejects external state mutation except for one supported reduction expression
* that is the final top-level body statement. The rewrite still has to prove a
* matching fresh local accumulator declaration before applying that reduction.
*/
public SafetyAnalysis analyze(Statement loopBody, String iteratorVarName) {
IteratorUsageVisitor visitor= new IteratorUsageVisitor(iteratorVarName);
loopBody.accept(visitor);
if (visitor.hasRemove) {
return SafetyAnalysis.unsafe("Iterator.remove() is not supported in stream operations", //$NON-NLS-1$
true, visitor.hasMultipleNext, visitor.hasBreak, visitor.hasLabeledContinue);
}
if (visitor.hasMultipleNext) {
return SafetyAnalysis.unsafe("Multiple iterator.next() calls detected", //$NON-NLS-1$
false, true, visitor.hasBreak, visitor.hasLabeledContinue);
}
if (visitor.hasLabeledContinue) {
return SafetyAnalysis.unsafe("Labeled continue statement is not supported", //$NON-NLS-1$
false, false, visitor.hasBreak, true);
}
if (visitor.hasBreak) {
return SafetyAnalysis.unsafe("break statements in the loop body are not yet supported for stream conversion", //$NON-NLS-1$
false, false, true, false);
}
if (visitor.externalStateMutationCount > 0
&& !isSingleTerminalReduction(loopBody, visitor.externalStateMutationCount, visitor.externalStateMutation)) {
return SafetyAnalysis.unsafe("The iterator loop mutates state declared outside its body", //$NON-NLS-1$
false, false, false, false);
}
return SafetyAnalysis.safe();
}
private static boolean isSingleTerminalReduction(Statement loopBody, int count, ASTNode mutation) {
if (count != 1 || mutation == null || !(loopBody instanceof Block block)) {
return false;
}
@SuppressWarnings("unchecked") //$NON-NLS-1$
List<Statement> statements= block.statements();
if (statements.isEmpty()) {
return false;
}
ASTNode current= mutation;
while (current != null && current.getParent() != block) {
current= current.getParent();
}
if (current != statements.get(statements.size() - 1) || !(current instanceof ExpressionStatement expressionStatement)) {
return false;
}
Expression expression= expressionStatement.getExpression();
if (expression instanceof PostfixExpression) {
return true;
}
if (expression instanceof PrefixExpression prefix) {
return prefix.getOperator() == PrefixExpression.Operator.INCREMENT
|| prefix.getOperator() == PrefixExpression.Operator.DECREMENT;
}
if (!(expression instanceof Assignment assignment)) {
return false;
}
Assignment.Operator operator= assignment.getOperator();
if (operator == Assignment.Operator.PLUS_ASSIGN || operator == Assignment.Operator.MINUS_ASSIGN
|| operator == Assignment.Operator.TIMES_ASSIGN) {
return true;
}
if (operator != Assignment.Operator.ASSIGN) {
return false;
}
Expression right= assignment.getRightHandSide();
if (right instanceof InfixExpression infix) {
InfixExpression.Operator infixOperator= infix.getOperator();
return infixOperator == InfixExpression.Operator.PLUS || infixOperator == InfixExpression.Operator.MINUS
|| infixOperator == InfixExpression.Operator.TIMES;
}
return right instanceof MethodInvocation invocation
&& ("max".equals(invocation.getName().getIdentifier()) //$NON-NLS-1$
|| "min".equals(invocation.getName().getIdentifier())); //$NON-NLS-1$
}
private static final class IteratorUsageVisitor extends ASTVisitor {
private final String iteratorVarName;
private final Set<String> variablesDeclaredInBody= new HashSet<>();
private boolean hasRemove;
private int nextCallCount;
private boolean hasMultipleNext;
private boolean hasBreak;
private boolean hasLabeledContinue;
private int externalStateMutationCount;
private ASTNode externalStateMutation;
IteratorUsageVisitor(String iteratorVarName) {
this.iteratorVarName= iteratorVarName;
}
@Override
public boolean visit(VariableDeclarationFragment node) {
IVariableBinding binding= node.resolveBinding();
if (binding != null) {
variablesDeclaredInBody.add(binding.getVariableDeclaration().getKey());
}
return true;
}
@Override
public boolean visit(MethodInvocation node) {
Expression expression= node.getExpression();
if (expression instanceof SimpleName name && name.getIdentifier().equals(iteratorVarName)) {
String methodName= node.getName().getIdentifier();
if ("remove".equals(methodName)) { //$NON-NLS-1$
hasRemove= true;
} else if ("next".equals(methodName)) { //$NON-NLS-1$
nextCallCount++;
hasMultipleNext= nextCallCount > 1;
}
}
return true;
}
@Override
public boolean visit(Assignment node) {
recordExternalMutation(node.getLeftHandSide(), node);
return true;
}
@Override
public boolean visit(PostfixExpression node) {
recordExternalMutation(node.getOperand(), node);
return true;
}
@Override
public boolean visit(PrefixExpression node) {
PrefixExpression.Operator operator= node.getOperator();
if (operator == PrefixExpression.Operator.INCREMENT || operator == PrefixExpression.Operator.DECREMENT) {
recordExternalMutation(node.getOperand(), node);
}
return true;
}
private void recordExternalMutation(Expression expression, ASTNode mutation) {
if (mutatesExternalState(expression)) {
externalStateMutationCount++;
externalStateMutation= mutation;
}
}
private boolean mutatesExternalState(Expression expression) {
if (expression instanceof ArrayAccess) {
return true;
}
IVariableBinding binding= variableBinding(expression);
if (binding == null) {
return true;
}
IVariableBinding declaration= binding.getVariableDeclaration();
return declaration.isField() || !variablesDeclaredInBody.contains(declaration.getKey());
}
private IVariableBinding variableBinding(Expression expression) {
IBinding binding= null;
if (expression instanceof SimpleName name) {
binding= name.resolveBinding();
} else if (expression instanceof FieldAccess fieldAccess) {
binding= fieldAccess.resolveFieldBinding();
} else if (expression instanceof QualifiedName qualifiedName) {
binding= qualifiedName.resolveBinding();
} else if (expression instanceof SuperFieldAccess superFieldAccess) {
binding= superFieldAccess.resolveFieldBinding();
}
return binding instanceof IVariableBinding variableBinding ? variableBinding : null;
}
@Override
public boolean visit(BreakStatement node) {
hasBreak= true;
return false;
}
@Override
public boolean visit(ContinueStatement node) {
if (node.getLabel() != null) {
hasLabeledContinue= true;
}
return false;
}
@Override
public boolean visit(WhileStatement node) {
return false;
}
@Override
public boolean visit(ForStatement node) {
return false;
}
@Override
public boolean visit(EnhancedForStatement node) {
return false;
}
@Override
public boolean visit(DoStatement node) {
return false;
}
}
}