PreconditionsChecker.java
/*******************************************************************************
* Copyright (c) 2025 Carsten Hammer and others.
*
* 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.Set;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.*;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.sandbox.jdt.internal.common.AstProcessorBuilder;
import org.sandbox.jdt.internal.common.ReferenceHolder;
import java.util.*;
/**
* Analyzes a loop statement to check various preconditions for safe refactoring
* to stream operations. Uses AstProcessorBuilder for cleaner AST traversal.
*
* <p>
* This class is final to prevent subclassing and potential finalizer attacks,
* since the constructor calls analysis methods that could potentially throw
* exceptions.
* </p>
*/
public final class PreconditionsChecker {
private final Statement loop;
// private final CompilationUnit compilationUnit;
private final Set<VariableDeclarationFragment> innerVariables = new HashSet<>();
private boolean containsBreak = false;
private boolean containsLabeledContinue = false;
private boolean containsReturn = false;
private boolean throwsException = false;
private boolean containsNEFs = false;
private boolean containsNestedLoop = false;
private boolean hasReducer = false;
private Statement reducerStatement = null;
private boolean isAnyMatchPattern = false;
private boolean isNoneMatchPattern = false;
private boolean isAllMatchPattern = false;
private boolean hasCollectPattern = false;
private Statement collectStatement = null;
private String collectTargetVariable = null;
private boolean modifiesIteratedCollection = false;
private boolean isConcurrentCollection = false;
/**
* Constructor for PreconditionsChecker.
*
* @param loop the statement containing the loop to analyze (must not
* be null)
* @param compilationUnit the compilation unit containing the loop
*/
public PreconditionsChecker(Statement loop, CompilationUnit compilationUnit) {
// Set loop field first - if null, we'll handle it gracefully in the catch block
this.loop = loop;
// this.compilationUnit = compilationUnit;
// Analyze the loop in a try-catch to prevent partial initialization
// if any exception occurs during analysis
try {
// Perform analysis only if loop is not null
if (loop != null) {
analyzeLoop();
} else {
// Null loop - treat as unsafe to refactor
this.containsBreak = true;
}
} catch (Exception e) {
// If analysis fails, treat loop as unsafe to refactor
// Set flags to prevent conversion
this.containsBreak = true; // Conservatively block conversion
}
}
/**
* Checks if the loop is safe to refactor to stream operations.
*
* <p>
* A loop is safe to refactor if it meets all of the following conditions:
* <ul>
* <li>Does not throw exceptions (throwsException == false)</li>
* <li>Does not contain break statements (containsBreak == false)</li>
* <li>Does not contain labeled continue statements (containsLabeledContinue ==
* false)</li>
* <li>Does not contain return statements OR contains only pattern-matching
* returns (anyMatch/noneMatch/allMatch)</li>
* <li>All variables are effectively final (containsNEFs == false)</li>
* <li>Does not iterate over concurrent collections (isConcurrentCollection == false)</li>
* </ul>
*
* <p>
* <b>Note on continue statements:</b> Unlabeled continue statements are allowed
* and will be converted to filter operations by StreamPipelineBuilder. Only
* labeled continues are rejected because they cannot be safely translated to
* stream operations.
* </p>
*
* <p>
* <b>Pattern-matching early returns:</b> Early returns matching anyMatch,
* noneMatch, or allMatch patterns are allowed because they can be converted to
* the corresponding terminal stream operations.
* </p>
*
* <p>
* <b>Concurrent collections:</b> Loops over concurrent collections (e.g.,
* {@code CopyOnWriteArrayList}, {@code ConcurrentHashMap}) are blocked
* conservatively to prevent unsafe conversions. Concurrent collections have
* iteration semantics that differ from standard collections (for example,
* snapshot-based iterators for {@code CopyOnWriteArrayList} and
* weakly-consistent iterators for {@code ConcurrentHashMap}) that may not
* translate correctly to stream operations.
* </p>
*
* @return true if the loop can be safely converted to stream operations, false
* otherwise
* @see StreamPipelineBuilder#parseLoopBody
*/
public boolean isSafeToRefactor() {
// Allow early returns if they match anyMatch/noneMatch/allMatch patterns
boolean allowedReturn = containsReturn && (isAnyMatchPattern || isNoneMatchPattern || isAllMatchPattern);
// Note: Unlabeled continues are allowed and will be converted to filters by
// StreamPipelineBuilder
// Only labeled continues are rejected here
// Issue #670: Block conversion when the iterated collection is modified (add/remove/put/clear)
// Issue #670: Block conversion when iterating over concurrent collections (conservative approach)
return !throwsException && !containsBreak && !containsLabeledContinue && (!containsReturn || allowedReturn)
&& !containsNEFs && !containsNestedLoop && !modifiesIteratedCollection && !isConcurrentCollection;
}
/**
* Checks if the loop contains a reducer pattern.
*
* <p>
* Scans loop body for accumulator patterns such as:
* <ul>
* <li>i++, i--, ++i, --i</li>
* <li>sum += x, product *= x, count -= 1</li>
* <li>Other compound assignments (|=, &=, etc.)</li>
* </ul>
*
* @return true if a reducer pattern is detected, false otherwise
*
* @see #getReducer()
*/
public boolean isReducer() {
return hasReducer;
}
/**
* Returns the statement containing the reducer pattern.
*
* <p>
* If multiple reducers exist in the loop, this returns only the first one
* encountered.
* </p>
*
* @return the statement containing the reducer, or null if no reducer was
* detected
*
* @see #isReducer()
*/
public Statement getReducer() {
return reducerStatement;
}
/**
* Checks if the loop matches the anyMatch pattern.
*
* <p>
* AnyMatch pattern: loop contains {@code if (condition) return true;}
* </p>
*
* @return true if anyMatch pattern is detected
*/
public boolean isAnyMatchPattern() {
return isAnyMatchPattern;
}
/**
* Checks if the loop matches the noneMatch pattern.
*
* <p>
* NoneMatch pattern: loop contains {@code if (condition) return false;}
* </p>
*
* @return true if noneMatch pattern is detected
*/
public boolean isNoneMatchPattern() {
return isNoneMatchPattern;
}
/**
* Checks if the loop matches the allMatch pattern.
*
* <p>
* AllMatch pattern: loop contains {@code if (condition) return false;} but the
* method returns true after the loop, or {@code if (!condition) return false;}
* checking all elements meet a condition.
* </p>
*
* @return true if allMatch pattern is detected
*/
public boolean isAllMatchPattern() {
return isAllMatchPattern;
}
/**
* Checks if the loop contains a collect pattern.
*
* <p>
* Scans loop body for collection accumulation patterns such as:
* <ul>
* <li>result.add(item)</li>
* <li>set.add(value)</li>
* </ul>
*
* @return true if a collect pattern is detected, false otherwise
*
* @see #getCollectStatement()
* @see #getCollectTarget()
*/
public boolean isCollectPattern() {
return hasCollectPattern;
}
/**
* Returns the statement containing the collect pattern.
*
* <p>
* If multiple collect statements exist in the loop, this returns only the first one
* encountered.
* </p>
*
* @return the statement containing the collect operation, or null if no collect was detected
*
* @see #isCollectPattern()
*/
public Statement getCollectStatement() {
return collectStatement;
}
/**
* Returns the target collection variable name for the collect pattern.
*
* @return the target variable name (e.g., "result"), or null if no collect was detected
*
* @see #isCollectPattern()
*/
public String getCollectTarget() {
return collectTargetVariable;
}
/**
* Checks if the loop modifies the collection being iterated over.
*
* <p>
* Detects calls to structural modification methods (remove, add, put, clear,
* set, addAll, removeAll, retainAll, removeIf, replaceAll, sort, and Map methods
* like putIfAbsent, compute, merge, replace) on the iterated collection. Such
* modifications cause ConcurrentModificationException with fail-fast iterators
* and change iteration semantics.
* </p>
*
* @return true if the iterated collection is modified in the loop body
*
* @see CollectionModificationDetector
* @see <a href="https://github.com/carstenartur/sandbox/issues/670">Issue #670</a>
*/
public boolean modifiesIteratedCollection() {
return modifiesIteratedCollection;
}
/**
* Checks if the iterated collection is a concurrent collection type.
*
* <p>
* Concurrent collections like {@code CopyOnWriteArrayList}, {@code ConcurrentHashMap},
* etc. have iteration semantics that differ from standard collections (for example,
* snapshot-based iterators for {@code CopyOnWriteArrayList} and
* weakly-consistent iterators for {@code ConcurrentHashMap}) that may not
* translate correctly to stream operations.
* Additionally, many concurrent collections do not support {@code iterator.remove()}.
* </p>
*
* <p><b>Current Integration:</b> Loops over concurrent collections are <b>blocked</b>
* from conversion by {@link #isSafeToRefactor()}. This conservative approach prevents
* potential issues with iterator semantics that may not translate
* correctly to stream operations.
* </p>
*
* <p><b>Rationale for Blocking:</b>
* <ul>
* <li>Concurrent collections and collections with snapshot-based iterators can
* have iterator semantics that may not translate correctly to stream
* operations (Issue #670).</li>
* <li>Iterator behavior differs significantly from standard collections</li>
* <li>Many concurrent collections don't support {@code iterator.remove()}</li>
* <li>Conservative approach ensures correctness over convenience</li>
* </ul>
* </p>
*
* @return true if the iterated collection is a concurrent collection
*
* @see ConcurrentCollectionDetector
* @see #isSafeToRefactor()
* @see <a href="https://github.com/carstenartur/sandbox/issues/670">Issue #670 - Point 2.4</a>
*/
public boolean isConcurrentCollection() {
return isConcurrentCollection;
}
/**
* Analyzes the loop statement to identify relevant elements for refactoring.
*
* <p>
* This method uses {@link AstProcessorBuilder} for cleaner and more
* maintainable AST traversal. It performs the following analysis:
* <ul>
* <li>Collects variable declarations within the loop</li>
* <li>Detects control flow statements (break, continue, return, throw)</li>
* <li>Identifies reducer patterns (i++, sum += x, etc.)</li>
* <li>Detects early return patterns (anyMatch, noneMatch, allMatch)</li>
* <li>Checks if variables are effectively final</li>
* </ul>
*
* <p>
* The analysis results are stored in instance variables that can be queried via
* getter methods like {@link #isSafeToRefactor()}, {@link #isReducer()}, etc.
* </p>
*/
private void analyzeLoop() {
// Extract the iterated collection name and type for modification detection (Issue #670)
String iteratedCollectionName = extractIteratedCollectionName();
ITypeBinding iteratedCollectionType = extractIteratedCollectionType();
// Check if iterating over a concurrent collection (Issue #670 - Point 2.4)
if (iteratedCollectionType != null) {
isConcurrentCollection = ConcurrentCollectionDetector.isConcurrentCollection(iteratedCollectionType);
}
AstProcessorBuilder<String, Object> builder = AstProcessorBuilder.with(new ReferenceHolder<String, Object>());
builder.onVariableDeclarationFragment((node, h) -> {
innerVariables.add(node);
return true;
}).onBreakStatement((node, h) -> {
containsBreak = true;
return true;
}).onLabeledContinue((node, h) -> {
// Labeled continue should prevent conversion (unlabeled continues are allowed)
containsLabeledContinue = true;
return true;
}).onReturnStatement((node, h) -> {
containsReturn = true;
return true;
}).onThrowStatement((node, h) -> {
throwsException = true;
return true;
}).onEnhancedForStatement((node, h) -> {
// If we encounter another EnhancedForStatement inside the loop body,
// it's a nested loop.
// PHASE 9: With LoopTree, nested loops are handled separately in the tree.
// We should NOT descend into them, as they'll be analyzed independently.
// Only mark as containsNestedLoop if this is the root loop we're analyzing.
if (node == loop) {
return true; // Continue analyzing this loop's body
}
// Found a nested loop - don't descend into it (return false)
// The LoopTree will handle this nested loop separately
return false;
}).onForStatement((node, h) -> {
// Traditional for loops inside the enhanced-for also prevent conversion
containsNestedLoop = true;
return true;
}).onWhileStatement((node, h) -> {
// While loops inside the enhanced-for also prevent conversion
containsNestedLoop = true;
return true;
}).onDoStatement((node, h) -> {
// Do-while loops inside the enhanced-for also prevent conversion
containsNestedLoop = true;
return true;
}).onTryStatement((node, h) -> {
// Try-catch blocks inside the loop prevent conversion
// (exception handling in lambdas is complex)
containsNEFs = true;
return true;
}).onSwitchStatement((node, h) -> {
// Switch statements inside the loop prevent conversion
containsNEFs = true;
return true;
}).onSynchronizedStatement((node, h) -> {
// Synchronized blocks inside the loop prevent conversion
containsNEFs = true;
return true;
}).onCompoundAssignment((node, h) -> {
// Compound assignments: +=, -=, *=, /=, |=, &=, etc.
markAsReducer(node);
return true;
}).onAssignment((node, h) -> {
// Check for Math.max/Math.min patterns: max = Math.max(max, x)
if (node.getOperator() == Assignment.Operator.ASSIGN && isMathMinMaxReducerPattern(node)) {
markAsReducer(node);
}
return true;
}).onPostfixIncrementOrDecrement((node, h) -> {
// Detect i++, i--
markAsReducer(node);
return true;
}).onPrefixIncrementOrDecrement((node, h) -> {
// Detect ++i, --i
markAsReducer(node);
return true;
}).onMethodInvocation((node, h) -> {
// Detect collection.add() patterns for collect operation
if (isCollectPattern(node)) {
markAsCollectPattern(node);
}
// Issue #670: Detect structural modifications on the iterated collection
if (iteratedCollectionName != null
&& CollectionModificationDetector.isModification(node, iteratedCollectionName)) {
modifiesIteratedCollection = true;
}
return true;
});
// First, analyze just the loop itself
builder.build(loop);
// Save the containsReturn flag state after analyzing only the loop body
// This is important because we want to distinguish between:
// 1. Returns INSIDE the loop (which may prevent conversion, except for match patterns)
// 2. Returns AFTER the loop (which are just part of the method and shouldn't prevent conversion)
boolean containsReturnInsideLoop = containsReturn;
// Then, if the loop is inside a Block, analyze only the immediately following
// statement (if any). This lets us detect patterns that depend on the statement
// right after the loop without pulling in unrelated statements.
ASTNode parent = loop.getParent();
if (parent instanceof Block block) {
List<Statement> statements = block.statements();
int loopIndex = statements.indexOf(loop);
if (loopIndex != -1 && loopIndex + 1 < statements.size()) {
Statement followingStatement = statements.get(loopIndex + 1);
builder.build(followingStatement);
}
}
// Detect anyMatch/noneMatch patterns
// This needs to see if there's a return statement after the loop,
// so containsReturn may be true from analyzing the following statement
detectEarlyReturnPatterns();
// Restore the containsReturn flag to only reflect returns INSIDE the loop
// This ensures that isSafeToRefactor() only rejects loops with returns inside,
// not loops followed by return statements (like reducers)
containsReturn = containsReturnInsideLoop;
// NOTE: Effectively-final variable checks for nested loops are now handled
// by EnhancedForHandler.endVisitLoop() using LoopTree/ScopeInfo, which properly
// tracks modifications in ancestor scopes while allowing reducer patterns.
}
/**
* Marks an AST node as a reducer pattern and records its statement.
*
* @param node the AST node that represents a reducer operation
*/
private void markAsReducer(ASTNode node) {
hasReducer = true;
if (reducerStatement == null) {
reducerStatement = ASTNodes.getFirstAncestorOrNull(node, Statement.class);
}
}
/**
* Marks an AST node as a collect pattern and records its statement.
*
* @param node the AST node that represents a collect operation (MethodInvocation)
*/
private void markAsCollectPattern(ASTNode node) {
hasCollectPattern = true;
if (collectStatement == null) {
collectStatement = ASTNodes.getFirstAncestorOrNull(node, Statement.class);
}
// Extract target variable from the MethodInvocation
if (node instanceof MethodInvocation methodInv && collectTargetVariable == null) {
Expression receiver = methodInv.getExpression();
if (receiver instanceof SimpleName simpleName) {
collectTargetVariable = simpleName.getIdentifier();
}
}
}
/**
* Checks if a method invocation represents a collect pattern.
*
* <p>Pattern: {@code result.add(item)} or {@code set.add(value)}</p>
*
* @param methodInv the method invocation to check
* @return true if this is a collect pattern
*/
private boolean isCollectPattern(MethodInvocation methodInv) {
// Check if method name is "add"
if (!"add".equals(methodInv.getName().getIdentifier())) {
return false;
}
// Check if invoked on a SimpleName (collection variable)
Expression receiver = methodInv.getExpression();
if (!(receiver instanceof SimpleName)) {
return false;
}
// Check if add() has one argument
if (methodInv.arguments().size() != 1) {
return false;
}
// Additional validation: check if the receiver is a collection type
// This is done in CollectPatternDetector, but for preconditions checking
// we'll allow it here and let the detector do the full validation
return true;
}
/**
* Checks if an assignment represents a Math.max or Math.min reducer pattern.
*
* <p>Pattern: {@code max = Math.max(max, x)} or {@code min = Math.min(min, x)}</p>
*
* @param assignment the assignment to check
* @return true if this is a Math.max/Math.min reducer pattern
*/
private boolean isMathMinMaxReducerPattern(Assignment assignment) {
Expression rhs = assignment.getRightHandSide();
if (!(rhs instanceof MethodInvocation methodInv)) {
return false;
}
if (!isMathMinMaxInvocation(methodInv)) {
return false;
}
return isLhsVariableInArguments(assignment.getLeftHandSide(), methodInv.arguments());
}
/**
* Checks if a method invocation is Math.max or Math.min.
*/
private boolean isMathMinMaxInvocation(MethodInvocation methodInv) {
Expression methodExpr = methodInv.getExpression();
if (!(methodExpr instanceof SimpleName className)) {
return false;
}
if (!"Math".equals(className.getIdentifier())) {
return false;
}
String methodName = methodInv.getName().getIdentifier();
return "max".equals(methodName) || "min".equals(methodName);
}
/**
* Checks if the LHS variable name appears in the method arguments.
*/
private boolean isLhsVariableInArguments(Expression lhs, List<?> arguments) {
if (!(lhs instanceof SimpleName lhsName)) {
return false;
}
if (arguments.size() != 2) {
return false;
}
String varName = lhsName.getIdentifier();
return arguments.stream()
.filter(SimpleName.class::isInstance)
.map(SimpleName.class::cast)
.anyMatch(arg -> varName.equals(arg.getIdentifier()));
}
/**
* Detects anyMatch, noneMatch, and allMatch patterns in the loop.
*
* <p>
* Patterns:
* <ul>
* <li>AnyMatch: {@code if (condition) return true;}</li>
* <li>NoneMatch: {@code if (condition) return false;}</li>
* <li>AllMatch: {@code if (!condition) return false;} or
* {@code if (condition) return false;} when negated</li>
* </ul>
*
* <p>
* These patterns must be the only statement with a return in the loop body.
*
* <p>
* AllMatch is typically used in patterns like:
*
* <pre>
* for (Item item : items) {
* if (!item.isValid())
* return false;
* }
* return true;
* </pre>
*/
private void detectEarlyReturnPatterns() {
if (!containsReturn || !(loop instanceof EnhancedForStatement forLoop)) {
return;
}
Statement body = forLoop.getBody();
// Find all IF statements with return statements in the loop
final List<IfStatement> ifStatementsWithReturn = new ArrayList<>();
// Use ASTVisitor to find IF statements
body.accept(new ASTVisitor() {
@Override
public boolean visit(IfStatement node) {
if (hasReturnInThenBranch(node)) {
ifStatementsWithReturn.add(node);
}
return true;
}
});
// For anyMatch/noneMatch/allMatch, we expect exactly one IF with return
if (ifStatementsWithReturn.size() != 1) {
return;
}
IfStatement ifStmt = ifStatementsWithReturn.get(0);
// The IF must not have an else branch for these patterns
if (ifStmt.getElseStatement() != null) {
return;
}
// Check if the IF returns a boolean literal
BooleanLiteral returnValue = getReturnValueFromIf(ifStmt);
if (returnValue == null) {
return;
}
// Check what statement follows the loop
BooleanLiteral followingReturn = getReturnAfterLoop(forLoop);
if (followingReturn == null) {
return;
}
// Determine pattern based on return values
determineMatchPattern(returnValue.booleanValue(), followingReturn.booleanValue(), ifStmt.getExpression());
}
/**
* Determines which match pattern (anyMatch, noneMatch, allMatch) applies based on
* the return values and condition.
*
* @param returnValueInLoop the boolean value returned inside the loop
* @param returnValueAfterLoop the boolean value returned after the loop
* @param condition the condition expression in the if statement
*/
private void determineMatchPattern(boolean returnValueInLoop, boolean returnValueAfterLoop, Expression condition) {
if (returnValueInLoop && !returnValueAfterLoop) {
// if (condition) return true; + return false; → anyMatch
isAnyMatchPattern = true;
} else if (!returnValueInLoop && returnValueAfterLoop) {
// if (condition) return false; + return true; → could be noneMatch OR allMatch
// Distinguish based on condition negation:
// - if (!condition) return false; + return true; → allMatch(condition)
// - if (condition) return false; + return true; → noneMatch(condition)
if (isNegatedCondition(condition)) {
isAllMatchPattern = true;
} else {
isNoneMatchPattern = true;
}
}
}
/**
* Checks if the IF statement has a return in its then branch.
*/
private boolean hasReturnInThenBranch(IfStatement ifStmt) {
return getReturnStatementFromThenBranch(ifStmt).isPresent();
}
/**
* Extracts the return statement from the then branch of an if statement.
* Handles both direct return statements and blocks with a single return statement.
*
* @param ifStmt the if statement to check
* @return Optional containing the ReturnStatement, or empty if not found
*/
private Optional<ReturnStatement> getReturnStatementFromThenBranch(IfStatement ifStmt) {
Statement thenStmt = ifStmt.getThenStatement();
if (thenStmt instanceof ReturnStatement returnStmt) {
return Optional.of(returnStmt);
}
if (thenStmt instanceof Block block) {
List<?> stmts = block.statements();
if (stmts.size() == 1 && stmts.get(0) instanceof ReturnStatement returnStmt) {
return Optional.of(returnStmt);
}
}
return Optional.empty();
}
/**
* Extracts the boolean literal value from a return statement in an IF.
*
* @return the BooleanLiteral if the IF returns a boolean literal, null
* otherwise
*/
private BooleanLiteral getReturnValueFromIf(IfStatement ifStmt) {
return getReturnStatementFromThenBranch(ifStmt)
.map(ReturnStatement::getExpression)
.filter(BooleanLiteral.class::isInstance)
.map(BooleanLiteral.class::cast)
.orElse(null);
}
/**
* Checks if an expression is a negated condition (starts with !).
* Handles ParenthesizedExpression wrapping.
*
* @param expr the expression to check
* @return true if the expression is a PrefixExpression with NOT operator (possibly wrapped in parentheses)
*/
private boolean isNegatedCondition(Expression expr) {
// Unwrap parentheses
while (expr instanceof ParenthesizedExpression parenthesized) {
expr = parenthesized.getExpression();
}
return expr instanceof PrefixExpression prefixExpr
&& prefixExpr.getOperator() == PrefixExpression.Operator.NOT;
}
/**
* Gets the boolean return value from the statement immediately following the loop.
*
* <p>
* For anyMatch/allMatch/noneMatch patterns, we expect a return statement with a
* boolean literal immediately after the loop. This method finds the loop's parent
* (usually a Block), locates the loop, and checks the next statement.
* </p>
*
* @param forLoop the EnhancedForStatement to check
* @return the BooleanLiteral returned after the loop, or null if not found
*/
private BooleanLiteral getReturnAfterLoop(EnhancedForStatement forLoop) {
ASTNode parent = forLoop.getParent();
// The loop must be in a Block (method body, if-then block, etc.)
if (!(parent instanceof Block block)) {
return null;
}
List<?> statements = block.statements();
// Find the loop in the block's statements
int loopIndex = statements.indexOf(forLoop);
if (loopIndex == -1 || loopIndex >= statements.size() - 1) {
// Loop not found or is the last statement
return null;
}
// Check the next statement
Statement nextStmt = (Statement) statements.get(loopIndex + 1);
// We expect a return statement with a boolean literal
if (nextStmt instanceof ReturnStatement returnStmt) {
Expression expr = returnStmt.getExpression();
if (expr instanceof BooleanLiteral boolLiteral) {
return boolLiteral;
}
}
return null;
}
/**
* Extracts the name of the iterated collection from the loop statement.
*
* <p>For enhanced for-loops: {@code for (String item : list)} → "list"</p>
*
* @return the collection variable name, or null if not determinable
*/
private String extractIteratedCollectionName() {
if (loop instanceof EnhancedForStatement enhancedFor) {
Expression expression = enhancedFor.getExpression();
if (expression instanceof SimpleName simpleName) {
return simpleName.getIdentifier();
}
// Unwrap common map view-producing calls like map.entrySet(), map.keySet(), map.values()
if (expression instanceof MethodInvocation methodInvocation) {
SimpleName name = methodInvocation.getName();
if (name != null) {
String identifier = name.getIdentifier();
if ("entrySet".equals(identifier) || "keySet".equals(identifier) || "values".equals(identifier)) {
if (methodInvocation.arguments().isEmpty()) {
Expression qualifier = methodInvocation.getExpression();
if (qualifier instanceof SimpleName qualifierName) {
return qualifierName.getIdentifier();
}
}
}
}
}
}
return null;
}
/**
* Extracts the type of the iterated collection from the loop statement.
*
* <p>For enhanced for-loops: {@code for (String item : list)} → type of list</p>
* <p>Unwraps common map view-producing calls like {@code map.entrySet()},
* {@code map.keySet()}, {@code map.values()} to detect the underlying map type.</p>
*
* @return the collection type binding, or null if not determinable
*/
private ITypeBinding extractIteratedCollectionType() {
if (loop instanceof EnhancedForStatement enhancedFor) {
Expression expression = enhancedFor.getExpression();
// Unwrap common map view-producing calls like map.entrySet(), map.keySet(), map.values()
if (expression instanceof MethodInvocation methodInvocation) {
SimpleName name = methodInvocation.getName();
if (name != null) {
String identifier = name.getIdentifier();
if ("entrySet".equals(identifier) || "keySet".equals(identifier) || "values".equals(identifier)) {
// Only consider the simple, no-arg variants
if (methodInvocation.arguments().isEmpty()) {
Expression qualifier = methodInvocation.getExpression();
if (qualifier != null) {
ITypeBinding qualifierBinding = qualifier.resolveTypeBinding();
if (qualifierBinding != null) {
return qualifierBinding;
}
}
}
}
}
}
return expression.resolveTypeBinding();
}
return null;
}
}