JdtLoopExtractor.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 org.eclipse.jdt.core.dom.*;
import org.sandbox.functional.core.builder.LoopModelBuilder;
import org.sandbox.functional.core.model.*;
import org.sandbox.functional.core.model.SourceDescriptor.SourceType;
/**
* Extracts a LoopModel from JDT AST nodes.
* This bridges the JDT world with the abstract ULR model.
*/
public class JdtLoopExtractor {
/**
* Extracts a LoopModel from an EnhancedForStatement.
*/
/**
* Enhanced wrapper that includes both the abstract LoopModel and the original AST nodes.
* This allows the renderer to use the actual AST nodes instead of parsing strings.
*/
public static class ExtractedLoop {
public final LoopModel model;
public final Statement originalBody;
public ExtractedLoop(LoopModel model, Statement originalBody) {
this.model = model;
this.originalBody = originalBody;
}
}
public ExtractedLoop extract(EnhancedForStatement forStatement) {
return extract(forStatement, null);
}
public ExtractedLoop extract(EnhancedForStatement forStatement, CompilationUnit compilationUnit) {
Expression iterable = forStatement.getExpression();
SingleVariableDeclaration parameter = forStatement.getParameter();
Statement body = forStatement.getBody();
// Determine source type
SourceType sourceType = determineSourceType(iterable);
String sourceExpression = iterable.toString();
String elementType = parameter.getType().toString();
// Element info
String varName = parameter.getName().getIdentifier();
boolean isFinal = Modifier.isFinal(parameter.getModifiers());
// Analyze metadata — track truly unconvertible control flow and patterns.
LoopBodyAnalyzer analyzer = new LoopBodyAnalyzer();
// Issue #670: Enable collection modification detection
if (iterable instanceof SimpleName) {
analyzer.setIteratedCollectionName(((SimpleName) iterable).getIdentifier());
}
body.accept(analyzer);
// Issue #670: Check if the iterated collection is a concurrent collection type
boolean isConcurrentCollection = checkConcurrentCollection(iterable);
// If the body contains unconvertible patterns, don't even try to build operations
// Note: external variable modifications (for reduce) and if-statements (for filter/match)
// are handled by analyzeAndAddOperations() and should NOT be blocked here
boolean hasUnconvertiblePatterns = analyzer.hasBreak()
|| analyzer.hasLabeledContinue()
|| analyzer.hasTryCatch()
|| analyzer.hasSynchronized()
|| analyzer.hasNestedLoop()
|| analyzer.hasVoidReturn()
|| analyzer.modifiesIteratedCollection()
|| isConcurrentCollection;
// Build model — mark as unconvertible if any patterns detected
LoopModelBuilder builder = new LoopModelBuilder()
.source(sourceType, sourceExpression, elementType)
.element(varName, elementType, isFinal)
.metadata(analyzer.hasBreak(), analyzer.hasLabeledContinue(),
false, analyzer.modifiesIteratedCollection(), true);
// Only analyze body and add operations/terminal if no unconvertible patterns
if (!hasUnconvertiblePatterns) {
analyzeAndAddOperations(body, builder, varName, compilationUnit);
}
// If hasUnconvertiblePatterns, no terminal is set → NOT_CONVERTIBLE
LoopModel model = builder.build();
return new ExtractedLoop(model, body);
}
private SourceType determineSourceType(Expression iterable) {
ITypeBinding binding = iterable.resolveTypeBinding();
if (binding != null) {
if (binding.isArray()) {
return SourceType.ARRAY;
}
// Check for Collection
if (isCollection(binding)) {
return SourceType.COLLECTION;
}
}
return SourceType.ITERABLE;
}
private boolean isCollection(ITypeBinding binding) {
if (binding == null) {
return false;
}
ITypeBinding erasure = binding.getErasure();
if (erasure == null) {
return false;
}
String qualifiedName = erasure.getQualifiedName();
if ("java.util.Collection".equals(qualifiedName)
|| "java.util.List".equals(qualifiedName)
|| "java.util.Set".equals(qualifiedName)
|| "java.util.Queue".equals(qualifiedName)
|| "java.util.Deque".equals(qualifiedName)) {
return true;
}
// Check interfaces
for (ITypeBinding iface : erasure.getInterfaces()) {
if (isCollection(iface)) {
return true;
}
}
// Check superclass
ITypeBinding superclass = erasure.getSuperclass();
if (superclass != null && isCollection(superclass)) {
return true;
}
return false;
}
/**
* Checks if the iterated expression refers to a concurrent collection type.
* Concurrent collections (e.g., CopyOnWriteArrayList, ConcurrentHashMap) have
* iteration semantics that may not translate correctly to stream operations.
*
* @param iterable the iterable expression from the enhanced for loop
* @return true if the iterable is a concurrent collection type
* @see ConcurrentCollectionDetector
*/
private boolean checkConcurrentCollection(Expression iterable) {
ITypeBinding typeBinding = resolveIteratedCollectionType(iterable);
if (typeBinding != null) {
return ConcurrentCollectionDetector.isConcurrentCollection(typeBinding);
}
return false;
}
/**
* Resolves the type binding of the iterated collection, unwrapping map view
* methods (entrySet, keySet, values) to get the underlying map type.
*/
private ITypeBinding resolveIteratedCollectionType(Expression iterable) {
if (iterable instanceof MethodInvocation methodInvocation) {
SimpleName name = methodInvocation.getName();
if (name != null) {
String identifier = name.getIdentifier();
if (("entrySet".equals(identifier) || "keySet".equals(identifier) || "values".equals(identifier)) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
&& methodInvocation.arguments().isEmpty()) {
Expression qualifier = methodInvocation.getExpression();
if (qualifier != null) {
ITypeBinding qualifierBinding = qualifier.resolveTypeBinding();
if (qualifierBinding != null) {
return qualifierBinding;
}
}
}
}
}
return iterable.resolveTypeBinding();
}
/**
* Analyzes the loop body and populates the builder with operations and terminal.
*
* <p>Detects the following patterns (conservatively — if no pattern matches,
* the builder gets no terminal, and the loop will NOT be converted):</p>
* <ul>
* <li>Simple body (single method call, no control flow) → ForEachTerminal</li>
* <li>IF with continue + remaining body → FilterOp + remaining patterns</li>
* <li>IF guard (no else, last statement) → FilterOp + nested body</li>
* <li>Variable declaration + remaining → MapOp + remaining patterns</li>
* <li>collection.add(expr) → CollectTerminal</li>
* <li>Accumulator patterns (+=, ++, etc.) → ReduceTerminal</li>
* <li>if (cond) return true/false (last) → MatchTerminal</li>
* </ul>
*
* <p>If the body contains unconvertible patterns (bare return, throw,
* assignments to external variables, etc.), no terminal is set and the
* loop is left unchanged.</p>
*/
private void analyzeAndAddOperations(Statement body, LoopModelBuilder builder, String varName, CompilationUnit compilationUnit) {
java.util.List<Statement> statements = new java.util.ArrayList<>();
if (body instanceof Block block) {
for (Object stmt : block.statements()) {
statements.add((Statement) stmt);
}
} else {
statements.add(body);
}
// Guard: empty body → no conversion
if (statements.isEmpty()) {
return; // No terminal → NOT_CONVERTIBLE
}
// Guard: reject bodies with throw statements or bare returns (not boolean returns)
if (containsUnconvertibleStatements(statements)) {
return; // No terminal → NOT_CONVERTIBLE
}
// Guard: if body has multiple variable declarations, use simple forEach
// instead of trying to decompose into map chains (which can be incorrect)
int varDeclCount = countVariableDeclarations(statements);
if (varDeclCount > 1) {
// Convert directly to simple forEach with body as-is
addSimpleForEachTerminal(statements, builder);
return;
}
// Guard: if body has multiple side-effect statements (e.g., multiple println),
// use simple forEach instead of trying to chain as map operations
if (statements.size() > 1 && allSideEffects(statements)) {
addSimpleForEachTerminal(statements, builder);
return;
}
// Try to analyze the statements into operations + terminal
analyzeStatements(statements, builder, varName, compilationUnit, false);
}
/**
* Checks if all statements are simple side effects (method calls).
*/
private boolean allSideEffects(java.util.List<Statement> statements) {
for (Statement stmt : statements) {
if (!isSimpleSideEffect(stmt)) {
return false;
}
}
return true;
}
/**
* Counts the number of variable declaration statements in the list.
*/
private int countVariableDeclarations(java.util.List<Statement> statements) {
int count = 0;
for (Statement stmt : statements) {
if (stmt instanceof VariableDeclarationStatement) {
count++;
}
}
return count;
}
/**
* Adds a simple forEach terminal with all statements in the body.
* Used when the body is too complex to decompose into map/filter chains.
*/
private void addSimpleForEachTerminal(java.util.List<Statement> statements, LoopModelBuilder builder) {
java.util.List<String> bodyStmts = new java.util.ArrayList<>();
for (Statement stmt : statements) {
String stmtStr = stmt.toString();
// Don't remove trailing semicolons for block statements
bodyStmts.add(stmtStr);
}
builder.forEach(bodyStmts, false);
}
/**
* Recursively analyzes a list of statements, building operations and setting the terminal.
* Implements V1-equivalent pattern detection for all supported transformations.
*
* @param statements the statements to analyze
* @param builder the model builder
* @param varName the current loop/pipeline variable name
* @param compilationUnit the compilation unit (for type resolution)
* @param hasOperations whether any intermediate operations (filter/map) have been added
*/
private void analyzeStatements(java.util.List<Statement> statements, LoopModelBuilder builder,
String varName, CompilationUnit compilationUnit, boolean hasOperations) {
String currentVarName = varName;
for (int i = 0; i < statements.size(); i++) {
Statement stmt = statements.get(i);
boolean isLast = (i == statements.size() - 1);
// === IF STATEMENT patterns ===
if (stmt instanceof IfStatement ifStmt && ifStmt.getElseStatement() == null) {
Statement thenStmt = ifStmt.getThenStatement();
// Pattern: if (cond) continue; → filter(x -> !(cond))
if (isContinueStatement(thenStmt)) {
String condition = ifStmt.getExpression().toString();
builder.filter("!(" + condition + ")");
attachComments(builder.getLastOperation(), stmt, compilationUnit);
hasOperations = true;
continue;
}
// Pattern: if (cond) return true/false; (last stmt) → MatchTerminal
if (isLast && isEarlyReturnIf(ifStmt, statements)) {
addMatchTerminal(ifStmt, builder, currentVarName);
return; // terminal set
}
// Pattern: if (cond) { body } (last stmt) → filter(cond) + body operations
if (isLast) {
String condition = ifStmt.getExpression().toString();
builder.filter("(" + condition + ")");
attachComments(builder.getLastOperation(), stmt, compilationUnit);
hasOperations = true;
analyzeAndAddOperations(ifStmt.getThenStatement(), builder, currentVarName, compilationUnit);
return; // terminal set by recursion
}
// Pattern: if (cond) { ... } (NOT last) → side-effect MAP that wraps the if
// V1 NON_TERMINAL: wrap intermediate if-statements as map(var -> { if(...){...} return var; })
builder.sideEffectMap(stmt.toString(), currentVarName);
attachComments(builder.getLastOperation(), stmt, compilationUnit);
hasOperations = true;
continue;
}
// === VARIABLE DECLARATION pattern ===
// Type x = expr; → map(var -> expr) with renamed pipeline variable
if (stmt instanceof VariableDeclarationStatement varDecl && !isLast) {
@SuppressWarnings("unchecked")
java.util.List<VariableDeclarationFragment> fragments = varDecl.fragments();
if (fragments.size() == 1) {
VariableDeclarationFragment frag = fragments.get(0);
Expression init = frag.getInitializer();
if (init != null) {
String newVarName = frag.getName().getIdentifier();
String mapExpr = init.toString();
// Check if remaining non-terminal statements need wrapping
// (V1 VARIABLE_DECLARATION.shouldWrapRemaining pattern)
if (shouldWrapRemainingInMap(statements, i, newVarName)) {
builder.map(mapExpr, varDecl.getType().toString(), newVarName);
attachComments(builder.getLastOperation(), stmt, compilationUnit);
currentVarName = newVarName;
hasOperations = true;
// Wrap remaining non-terminal statements as a single side-effect MAP
i = wrapRemainingNonTerminals(statements, i + 1, builder, currentVarName);
// Now i points to the last statement (terminal) — continue loop to process it
continue;
}
builder.map(mapExpr, varDecl.getType().toString(), newVarName);
attachComments(builder.getLastOperation(), stmt, compilationUnit);
currentVarName = newVarName;
hasOperations = true;
continue;
}
}
}
// === ASSIGNMENT MAP pattern ===
// x = expr; where x is the current pipeline variable → map(x -> expr)
if (stmt instanceof ExpressionStatement assignExprStmt && !isLast) {
Expression assignExpr = assignExprStmt.getExpression();
if (assignExpr instanceof Assignment assign
&& assign.getOperator() == Assignment.Operator.ASSIGN) {
Expression lhs = assign.getLeftHandSide();
if (lhs instanceof SimpleName name
&& name.getIdentifier().equals(currentVarName)) {
String mapExpr = assign.getRightHandSide().toString();
builder.map(mapExpr, null, currentVarName);
attachComments(builder.getLastOperation(), stmt, compilationUnit);
hasOperations = true;
continue;
}
}
}
// === TERMINAL patterns (last statement only) ===
if (isLast) {
// Collect: collection.add(expr)
if (isCollectPattern(stmt)) {
addCollectTerminal(stmt, builder, currentVarName);
return; // terminal set
}
// Reduce: += , ++, *=, count = count + 1, etc.
if (isReducePattern(stmt)) {
addReduceTerminal(stmt, builder, currentVarName);
return; // terminal set
}
// ForEach: everything else at the end
java.util.List<String> bodyStmts = new java.util.ArrayList<>();
String stmtStr = stmt.toString();
if (stmtStr.endsWith(";\n") || stmtStr.endsWith(";")) {
stmtStr = stmtStr.replaceAll(";\\s*$", "").trim();
}
bodyStmts.add(stmtStr);
builder.forEach(bodyStmts, hasOperations || builder.hasOperations());
return; // terminal set
}
// === NON-TERMINAL side-effect pattern ===
// Intermediate statements that don't match any pattern above are wrapped as
// side-effect MAP: map(var -> { stmt; return var; })
// This is the V1 NON_TERMINAL handler equivalent
if (isCollectPattern(stmt) || isReducePattern(stmt) || isSimpleSideEffect(stmt)) {
builder.sideEffectMap(stmt.toString(), currentVarName);
attachComments(builder.getLastOperation(), stmt, compilationUnit);
hasOperations = true;
continue;
}
// Unknown intermediate statement → cannot convert
return; // No terminal → NOT_CONVERTIBLE
}
}
/**
* Checks if remaining non-terminal statements after a variable declaration
* should be wrapped in a single MAP block.
*
* <p>V1 wraps remaining statements when they contain intermediate if-statements
* or other side-effect patterns that need to be combined into a single map block.</p>
*/
private boolean shouldWrapRemainingInMap(java.util.List<Statement> statements, int currentIndex, String newVarName) {
// Look through remaining non-terminal statements
for (int j = currentIndex + 1; j < statements.size() - 1; j++) {
Statement stmt = statements.get(j);
// If there's an intermediate if-statement (not continue, not return), wrap
if (stmt instanceof IfStatement ifStmt && ifStmt.getElseStatement() == null) {
Statement thenStmt = ifStmt.getThenStatement();
if (!isContinueStatement(thenStmt) && !isEarlyReturnIf(ifStmt, statements)) {
return true;
}
}
// Don't wrap if the statement is an assignment to the produced variable
// (handled by ASSIGNMENT_MAP pattern)
if (stmt instanceof ExpressionStatement es) {
Expression expr = es.getExpression();
if (expr instanceof Assignment assign
&& assign.getOperator() == Assignment.Operator.ASSIGN
&& assign.getLeftHandSide() instanceof SimpleName sn
&& sn.getIdentifier().equals(newVarName)) {
return false;
}
}
}
return false;
}
/**
* Wraps remaining non-terminal statements (from startIndex to size-2) in a single
* side-effect MAP operation, then returns the index of the last statement (terminal).
*
* @return the index of the last statement (for the caller to process as terminal)
*/
private int wrapRemainingNonTerminals(java.util.List<Statement> statements, int startIndex,
LoopModelBuilder builder, String currentVarName) {
int lastIndex = statements.size() - 1;
// Collect non-terminal statements into a block string
StringBuilder blockStr = new StringBuilder();
for (int j = startIndex; j < lastIndex; j++) {
blockStr.append(statements.get(j).toString());
}
if (blockStr.length() > 0) {
builder.sideEffectMap(blockStr.toString(), currentVarName);
}
// Return the index right before the last statement so the loop will process it next
return lastIndex - 1;
}
/**
* Checks if the statements contain patterns that are always unconvertible.
*/
private boolean containsUnconvertibleStatements(java.util.List<Statement> statements) {
for (Statement stmt : statements) {
// Bare return (not inside an if-return-boolean pattern) → unconvertible
if (stmt instanceof ReturnStatement returnStmt) {
Expression expr = returnStmt.getExpression();
// return; (void) → unconvertible
if (expr == null) return true;
// return someExpr; (not boolean literal) → unconvertible
// Note: if-return-boolean inside an IfStatement is handled as a match pattern
if (!(expr instanceof BooleanLiteral)) return true;
}
// Throw → unconvertible
if (stmt instanceof ThrowStatement) {
return true;
}
}
return false;
}
/**
* Checks if a statement is a simple side effect (method call, etc.)
* that can be safely included in a forEach body.
*/
private boolean isSimpleSideEffect(Statement stmt) {
if (stmt instanceof ExpressionStatement exprStmt) {
Expression expr = exprStmt.getExpression();
// Method invocations like System.out.println(x) are simple side effects
if (expr instanceof MethodInvocation) {
return true;
}
}
return false;
}
private boolean isContinueStatement(Statement stmt) {
if (stmt instanceof ContinueStatement) {
return true;
}
if (stmt instanceof Block block) {
@SuppressWarnings("unchecked")
java.util.List<Statement> stmts = block.statements();
return stmts.size() == 1 && stmts.get(0) instanceof ContinueStatement;
}
return false;
}
/**
* Detects early return patterns for anyMatch/noneMatch/allMatch.
* Pattern: if (condition) return true/false; (as last statement)
*/
private boolean isEarlyReturnIf(IfStatement ifStmt, java.util.List<Statement> statements) {
Statement thenStmt = ifStmt.getThenStatement();
if (thenStmt instanceof ReturnStatement returnStmt) {
return returnStmt.getExpression() instanceof BooleanLiteral;
}
if (thenStmt instanceof Block block) {
@SuppressWarnings("unchecked")
java.util.List<Statement> stmts = block.statements();
if (stmts.size() == 1 && stmts.get(0) instanceof ReturnStatement returnStmt) {
return returnStmt.getExpression() instanceof BooleanLiteral;
}
}
return false;
}
private void addMatchTerminal(IfStatement ifStmt, LoopModelBuilder builder, String varName) {
String condition = ifStmt.getExpression().toString();
// Determine match type from the return value
Statement thenStmt = ifStmt.getThenStatement();
ReturnStatement returnStmt;
if (thenStmt instanceof ReturnStatement rs) {
returnStmt = rs;
} else {
@SuppressWarnings("unchecked")
java.util.List<Statement> stmts = ((Block) thenStmt).statements();
returnStmt = (ReturnStatement) stmts.get(0);
}
boolean returnsTrue = ((BooleanLiteral) returnStmt.getExpression()).booleanValue();
if (returnsTrue) {
// if (cond) return true; → anyMatch(cond)
builder.anyMatch(condition);
} else {
// if (cond) return false;
// Check if the condition is already negated → allMatch with inner condition
Expression condExpr = ifStmt.getExpression();
if (condExpr instanceof PrefixExpression prefix
&& prefix.getOperator() == PrefixExpression.Operator.NOT) {
// if (!innerCond) return false; → allMatch(innerCond)
String innerCondition = prefix.getOperand().toString();
builder.allMatch(innerCondition);
} else if (condition.startsWith("!(") && condition.endsWith(")")) {
// if (!(innerCond)) return false; → allMatch(innerCond)
String innerCondition = condition.substring(2, condition.length() - 1);
builder.allMatch(innerCondition);
} else {
// if (cond) return false; → noneMatch(cond)
builder.noneMatch(condition);
}
}
}
/**
* Detects collection.add(expr) patterns.
*/
private boolean isCollectPattern(Statement stmt) {
if (stmt instanceof ExpressionStatement exprStmt) {
Expression expr = exprStmt.getExpression();
if (expr instanceof MethodInvocation mi) {
String methodName = mi.getName().getIdentifier();
return "add".equals(methodName) && mi.arguments().size() == 1;
}
}
return false;
}
private void addCollectTerminal(Statement stmt, LoopModelBuilder builder, String varName) {
ExpressionStatement exprStmt = (ExpressionStatement) stmt;
MethodInvocation mi = (MethodInvocation) exprStmt.getExpression();
// Determine collector type from the target collection type
Expression target = mi.getExpression();
ITypeBinding targetType = target != null ? target.resolveTypeBinding() : null;
org.sandbox.functional.core.terminal.CollectTerminal.CollectorType collectorType =
org.sandbox.functional.core.terminal.CollectTerminal.CollectorType.TO_LIST;
if (targetType != null) {
String typeName = targetType.getErasure() != null ? targetType.getErasure().getQualifiedName() : "";
if (typeName.contains("Set")) {
collectorType = org.sandbox.functional.core.terminal.CollectTerminal.CollectorType.TO_SET;
}
}
String targetVar = target != null ? target.toString() : "result";
// Check if the added expression is not identity (needs a map before collect)
Expression addedExpr = (Expression) mi.arguments().get(0);
if (addedExpr instanceof SimpleName sn && sn.getIdentifier().equals(varName)) {
// Identity mapping - just collect
builder.collect(collectorType, targetVar);
} else {
// Non-identity: add map before collect, infer type from expression
String mapTargetType = null;
ITypeBinding exprType = addedExpr.resolveTypeBinding();
if (exprType != null) {
mapTargetType = exprType.getName();
}
builder.map(addedExpr.toString(), mapTargetType);
builder.collect(collectorType, targetVar);
}
}
/**
* Detects accumulator patterns like sum += x, count++, count = count + 1,
* max = Math.max(max, x), etc.
*/
private boolean isReducePattern(Statement stmt) {
if (stmt instanceof ExpressionStatement exprStmt) {
Expression expr = exprStmt.getExpression();
// Compound assignment: sum += x, product *= x
if (expr instanceof Assignment assign) {
Assignment.Operator op = assign.getOperator();
if (op == Assignment.Operator.PLUS_ASSIGN
|| op == Assignment.Operator.MINUS_ASSIGN
|| op == Assignment.Operator.TIMES_ASSIGN) {
return true;
}
// Plain assignment with infix accumulator: count = count + 1
if (op == Assignment.Operator.ASSIGN && assign.getLeftHandSide() instanceof SimpleName) {
if (isInfixAccumulator(assign)) return true;
if (isMathMaxMinPattern(assign)) return true;
}
}
// Postfix: count++, count--
if (expr instanceof PostfixExpression) {
return true;
}
// Prefix: ++count, --count
if (expr instanceof PrefixExpression prefix) {
PrefixExpression.Operator op = prefix.getOperator();
return op == PrefixExpression.Operator.INCREMENT
|| op == PrefixExpression.Operator.DECREMENT;
}
}
return false;
}
/**
* Checks if a plain assignment is an infix accumulator pattern:
* count = count + 1, result = result * value, etc.
*/
private boolean isInfixAccumulator(Assignment assign) {
String varName = ((SimpleName) assign.getLeftHandSide()).getIdentifier();
Expression rhs = assign.getRightHandSide();
if (rhs instanceof InfixExpression infix) {
Expression left = infix.getLeftOperand();
if (left instanceof SimpleName sn && sn.getIdentifier().equals(varName)) {
InfixExpression.Operator op = infix.getOperator();
return op == InfixExpression.Operator.PLUS
|| op == InfixExpression.Operator.MINUS
|| op == InfixExpression.Operator.TIMES;
}
}
return false;
}
private void addReduceTerminal(Statement stmt, LoopModelBuilder builder, String varName) {
ExpressionStatement exprStmt = (ExpressionStatement) stmt;
Expression expr = exprStmt.getExpression();
if (expr instanceof Assignment assign) {
Assignment.Operator op = assign.getOperator();
String accumVar = assign.getLeftHandSide().toString();
String valueExpr = assign.getRightHandSide().toString();
String accumType = resolveReducerType(assign.getLeftHandSide());
if (op == Assignment.Operator.PLUS_ASSIGN) {
// sum += value → .map(var -> value).reduce(sum, Integer/Long/Double::sum)
if (!valueExpr.equals(varName)) {
builder.map(valueExpr, null);
}
builder.terminal(new org.sandbox.functional.core.terminal.ReduceTerminal(
accumVar, accumType + "::sum", null,
org.sandbox.functional.core.terminal.ReduceTerminal.ReduceType.SUM, accumVar));
} else if (op == Assignment.Operator.TIMES_ASSIGN) {
if (!valueExpr.equals(varName)) {
builder.map(valueExpr, null);
}
builder.terminal(new org.sandbox.functional.core.terminal.ReduceTerminal(
accumVar, "(a, b) -> a * b", null,
org.sandbox.functional.core.terminal.ReduceTerminal.ReduceType.PRODUCT, accumVar));
} else if (op == Assignment.Operator.MINUS_ASSIGN) {
if (!valueExpr.equals(varName)) {
builder.map(valueExpr, null);
}
builder.terminal(new org.sandbox.functional.core.terminal.ReduceTerminal(
accumVar, "(a, b) -> a - b", null,
org.sandbox.functional.core.terminal.ReduceTerminal.ReduceType.CUSTOM, accumVar));
} else if (op == Assignment.Operator.ASSIGN) {
if (assign.getRightHandSide() instanceof InfixExpression infix) {
addInfixReduceTerminal(infix, builder, varName, accumVar);
} else if (isMathMaxMinPattern(assign)) {
addMathMaxMinReduce(assign, builder, accumVar);
}
}
} else if (expr instanceof PostfixExpression postfix) {
// i++ → .map(var -> 1).reduce(i, Integer::sum)
String accumVar = postfix.getOperand().toString();
String accumType = resolveReducerType(postfix.getOperand());
String mapValue = isLongType(accumType) ? "1L" : "1";
builder.map(mapValue, null);
builder.terminal(new org.sandbox.functional.core.terminal.ReduceTerminal(
accumVar, accumType + "::sum", null,
org.sandbox.functional.core.terminal.ReduceTerminal.ReduceType.COUNT, accumVar));
} else if (expr instanceof PrefixExpression prefix) {
String accumVar = prefix.getOperand().toString();
String accumType = resolveReducerType(prefix.getOperand());
String mapValue = isLongType(accumType) ? "1L" : "1";
if (prefix.getOperator() == PrefixExpression.Operator.INCREMENT) {
builder.map(mapValue, null);
builder.terminal(new org.sandbox.functional.core.terminal.ReduceTerminal(
accumVar, accumType + "::sum", null,
org.sandbox.functional.core.terminal.ReduceTerminal.ReduceType.COUNT, accumVar));
} else {
builder.map(mapValue, null);
builder.terminal(new org.sandbox.functional.core.terminal.ReduceTerminal(
accumVar, "(a, b) -> a - b", null,
org.sandbox.functional.core.terminal.ReduceTerminal.ReduceType.CUSTOM, accumVar));
}
}
}
/**
* Resolves the boxed type name for a reducer variable (Integer, Long, Double, etc.)
* Falls back to "Integer" if type cannot be resolved.
*/
private String resolveReducerType(Expression operand) {
ITypeBinding binding = operand.resolveTypeBinding();
if (binding != null) {
String name = binding.isPrimitive() ? getBoxedTypeName(binding.getName()) : binding.getName();
if (name != null) return name;
}
return "Integer";
}
private String getBoxedTypeName(String primitiveName) {
return switch (primitiveName) {
case "int" -> "Integer";
case "long" -> "Long";
case "double" -> "Double";
case "float" -> "Float";
default -> "Integer";
};
}
private boolean isLongType(String typeName) {
return "Long".equals(typeName) || "long".equals(typeName);
}
/**
* Checks if an assignment is a Math.max or Math.min pattern.
* Pattern: max = Math.max(max, x) or min = Math.min(min, x)
*/
private boolean isMathMaxMinPattern(Assignment assign) {
Expression rhs = assign.getRightHandSide();
if (rhs instanceof MethodInvocation mi) {
String methodName = mi.getName().getIdentifier();
Expression expr = mi.getExpression();
if (expr instanceof SimpleName sn && "Math".equals(sn.getIdentifier())) {
return ("max".equals(methodName) || "min".equals(methodName)) && mi.arguments().size() == 2;
}
}
return false;
}
/**
* Adds Math.max/min reduce terminal.
* Pattern: max = Math.max(max, num) → .reduce(max, Math::max)
*/
private void addMathMaxMinReduce(Assignment assign, LoopModelBuilder builder, String accumVar) {
MethodInvocation mi = (MethodInvocation) assign.getRightHandSide();
String methodName = mi.getName().getIdentifier();
org.sandbox.functional.core.terminal.ReduceTerminal.ReduceType type =
"max".equals(methodName) ? org.sandbox.functional.core.terminal.ReduceTerminal.ReduceType.MAX
: org.sandbox.functional.core.terminal.ReduceTerminal.ReduceType.MIN;
builder.terminal(new org.sandbox.functional.core.terminal.ReduceTerminal(
accumVar, "Math::" + methodName, null, type, accumVar));
}
private void addInfixReduceTerminal(InfixExpression infix, LoopModelBuilder builder,
String varName, String accumVar) {
String rightOperand = infix.getRightOperand().toString();
InfixExpression.Operator op = infix.getOperator();
String accumType = resolveReducerType(infix.getLeftOperand());
// Only add a MAP operation if the right operand is NOT the identity (loop variable)
// e.g., count = count + 1 → .map(item -> 1).reduce(count, Integer::sum)
// but result = result + item → .reduce(result, (a, b) -> a + b) [no map needed]
if (!rightOperand.equals(varName)) {
builder.map(rightOperand, null);
}
if (op == InfixExpression.Operator.PLUS) {
// For String types, check @NotNull annotation to decide between String::concat and lambda
if ("String".equals(accumType)) {
// Check if the accumulator variable has @NotNull annotation
boolean isNullSafe = org.sandbox.jdt.internal.corext.util.VariableResolver
.hasNotNullAnnotation(infix, accumVar);
if (isNullSafe) {
// @NotNull present: safe to use String::concat method reference
builder.terminal(new org.sandbox.functional.core.terminal.ReduceTerminal(
accumVar, "String::concat", null,
org.sandbox.functional.core.terminal.ReduceTerminal.ReduceType.STRING_CONCAT, accumVar));
} else {
// No @NotNull: use null-safe lambda (a, b) -> a + b
builder.terminal(new org.sandbox.functional.core.terminal.ReduceTerminal(
accumVar, "(a, b) -> a + b", null,
org.sandbox.functional.core.terminal.ReduceTerminal.ReduceType.CUSTOM, accumVar));
}
} else {
builder.terminal(new org.sandbox.functional.core.terminal.ReduceTerminal(
accumVar, accumType + "::sum", null,
org.sandbox.functional.core.terminal.ReduceTerminal.ReduceType.SUM, accumVar));
}
} else if (op == InfixExpression.Operator.TIMES) {
builder.terminal(new org.sandbox.functional.core.terminal.ReduceTerminal(
accumVar, "(a, b) -> a * b", null,
org.sandbox.functional.core.terminal.ReduceTerminal.ReduceType.PRODUCT, accumVar));
} else {
builder.terminal(new org.sandbox.functional.core.terminal.ReduceTerminal(
accumVar, "(a, b) -> a - b", null,
org.sandbox.functional.core.terminal.ReduceTerminal.ReduceType.CUSTOM, accumVar));
}
}
/**
* Attaches extracted comments from an AST node to an Operation.
* If the operation is a FilterOp or MapOp and has associated comments
* in the source, they are stored in the operation for preservation
* during rendering.
*
* @param operation the operation to attach comments to (can be null)
* @param node the AST node to extract comments from
* @param cu the compilation unit (can be null)
*/
private void attachComments(org.sandbox.functional.core.operation.Operation operation,
ASTNode node, CompilationUnit cu) {
if (operation == null || cu == null) {
return;
}
java.util.List<String> comments = extractComments(node, cu);
if (comments.isEmpty()) {
return;
}
if (operation instanceof org.sandbox.functional.core.operation.FilterOp filterOp) {
filterOp.addComments(comments);
} else if (operation instanceof org.sandbox.functional.core.operation.MapOp mapOp) {
mapOp.addComments(comments);
}
}
/**
* Extracts comments associated with an AST node.
*
* @param node the AST node to extract comments from
* @param cu the compilation unit (can be null)
* @return list of comment strings, never null
*/
@SuppressWarnings("unchecked")
private java.util.List<String> extractComments(ASTNode node, CompilationUnit cu) {
java.util.List<String> comments = new java.util.ArrayList<>();
if (cu == null || node == null) {
return comments;
}
java.util.List<Comment> commentList = cu.getCommentList();
if (commentList == null || commentList.isEmpty()) {
return comments;
}
int nodeStart = node.getStartPosition();
int nodeEnd = nodeStart + node.getLength();
int nodeStartLine = cu.getLineNumber(nodeStart);
int nodeEndLine = cu.getLineNumber(nodeEnd);
for (Comment comment : commentList) {
int commentStart = comment.getStartPosition();
int commentEnd = commentStart + comment.getLength();
int commentStartLine = cu.getLineNumber(commentStart);
int commentEndLine = cu.getLineNumber(commentEnd);
// Associate comments that are:
// 1. On the line immediately before the node, OR
// 2. On the same line as the node (leading: before node start, trailing: after node end), OR
// 3. Within the node's span (embedded comment)
boolean isLeadingComment = commentEndLine == nodeStartLine - 1 ||
(commentEndLine == nodeStartLine && commentEnd <= nodeStart);
boolean isTrailingComment = commentStartLine == nodeEndLine && commentStart >= nodeEnd;
boolean isEmbeddedComment = commentStart >= nodeStart && commentEnd <= nodeEnd;
if (isLeadingComment || isTrailingComment || isEmbeddedComment) {
String commentText = extractCommentText(comment, cu);
if (commentText != null && !commentText.isEmpty()) {
comments.add(commentText);
}
}
}
return comments;
}
/**
* Extracts the text content from a Comment node.
*
* @param comment the comment node
* @param cu the compilation unit
* @return the comment text without delimiters, or null if not available
*/
private String extractCommentText(Comment comment, CompilationUnit cu) {
try {
// Get the original source from the compilation unit's type root
org.eclipse.jdt.core.ICompilationUnit javaElement =
(org.eclipse.jdt.core.ICompilationUnit) cu.getJavaElement();
if (javaElement == null) {
return null;
}
String source = javaElement.getSource();
if (source == null) {
return null;
}
int start = comment.getStartPosition();
int length = comment.getLength();
if (start < 0 || start + length > source.length()) {
return null;
}
String commentStr = source.substring(start, start + length);
// Remove comment delimiters
if (comment.isLineComment()) {
// Remove leading //
commentStr = commentStr.replaceFirst("^//\\s*", "");
} else if (comment.isBlockComment()) {
// Remove /* and */
commentStr = commentStr.replaceFirst("^/\\*\\s*", "");
commentStr = commentStr.replaceFirst("\\s*\\*/$", "");
} else if (comment instanceof Javadoc) {
// Remove /** and */
commentStr = commentStr.replaceFirst("^/\\*\\*\\s*", "");
commentStr = commentStr.replaceFirst("\\s*\\*/$", "");
}
return commentStr.trim();
} catch (Exception e) {
// If extraction fails, return null
return null;
}
}
/**
* Visitor to analyze loop body for truly unconvertible control flow.
*
* <p>Tracks patterns that CANNOT be converted to stream operations:
* <ul>
* <li>break statements</li>
* <li>labeled continue statements</li>
* <li>try-catch statements (checked exceptions require Try-with-resources or explicit handling)</li>
* <li>synchronized statements (synchronization semantics differ in streams)</li>
* <li>traditional for loops (complex control flow)</li>
* <li>while loops (complex control flow)</li>
* <li>do-while loops (complex control flow)</li>
* </ul>
* </p>
*
* <p>Note: unlabeled continue, return, and add() calls are potentially
* convertible as filter, match, and collect patterns respectively.
* Those are handled by analyzeAndAddOperations().</p>
*/
private static class LoopBodyAnalyzer extends ASTVisitor {
private boolean hasBreak = false;
private boolean hasLabeledContinue = false;
private boolean hasTryCatch = false;
private boolean hasSynchronized = false;
private boolean hasNestedLoop = false;
private boolean hasVoidReturn = false;
private boolean hasIfElse = false;
private boolean modifiesIteratedCollection = false;
private String iteratedCollectionName;
@Override
public boolean visit(BreakStatement node) {
hasBreak = true;
return false;
}
@Override
public boolean visit(ContinueStatement node) {
// Only labeled continue is truly unconvertible
// Unlabeled continue can be converted to a negated filter
if (node.getLabel() != null) {
hasLabeledContinue = true;
}
return false;
}
@Override
public boolean visit(IfStatement node) {
// If-else patterns don't map cleanly to stream operations
// e.g., if (cond) list1.add(x) else list2.add(x)
if (node.getElseStatement() != null) {
hasIfElse = true;
}
return true; // Continue visiting to detect nested patterns
}
@Override
public boolean visit(ReturnStatement node) {
// Void return exits the enclosing method, not the loop
// This cannot be converted to stream operations
if (node.getExpression() == null) {
hasVoidReturn = true;
}
// Note: boolean returns (return true/false) are potentially convertible
// as match patterns — handled separately in analyzeStatements()
return false;
}
@Override
public boolean visit(TryStatement node) {
// Try-catch cannot be easily converted to stream operations
// due to checked exception handling requirements
hasTryCatch = true;
return false;
}
@Override
public boolean visit(SynchronizedStatement node) {
// Synchronized blocks have different semantics in streams
// (parallelStream vs sequential processing)
hasSynchronized = true;
return false;
}
@Override
public boolean visit(ForStatement node) {
// Traditional for loops inside the body → unconvertible
hasNestedLoop = true;
return false;
}
@Override
public boolean visit(WhileStatement node) {
// While loops inside the body → unconvertible
hasNestedLoop = true;
return false;
}
@Override
public boolean visit(DoStatement node) {
// Do-while loops inside the body → unconvertible
hasNestedLoop = true;
return false;
}
@Override
public boolean visit(MethodInvocation node) {
// Issue #670: Detect structural modifications on the iterated collection
if (iteratedCollectionName != null
&& CollectionModificationDetector.isModification(node, iteratedCollectionName)) {
modifiesIteratedCollection = true;
}
return true;
}
public boolean hasBreak() { return hasBreak; }
public boolean hasLabeledContinue() { return hasLabeledContinue; }
public boolean hasTryCatch() { return hasTryCatch; }
public boolean hasSynchronized() { return hasSynchronized; }
public boolean hasNestedLoop() { return hasNestedLoop; }
public boolean hasVoidReturn() { return hasVoidReturn; }
public boolean hasIfElse() { return hasIfElse; }
public boolean modifiesIteratedCollection() { return modifiesIteratedCollection; }
/**
* Sets the name of the iterated collection for modification detection.
* Must be called before accept() to enable collection modification checking.
*
* @see <a href="https://github.com/carstenartur/sandbox/issues/670">Issue #670</a>
*/
public void setIteratedCollectionName(String name) {
this.iteratedCollectionName = name;
}
}
}