IfStatementAnalyzer.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.List;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.BooleanLiteral;
import org.eclipse.jdt.core.dom.BreakStatement;
import org.eclipse.jdt.core.dom.ContinueStatement;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.Statement;
import org.sandbox.jdt.internal.corext.util.ExpressionHelper;
/**
* Analyzes IF statements to detect patterns that can be converted to stream operations.
*
* <p>This class identifies various IF statement patterns:</p>
* <ul>
* <li><b>Early Return Patterns</b>: anyMatch, noneMatch, allMatch</li>
* <li><b>Continue Patterns</b>: Can be converted to negated filters</li>
* <li><b>Break/Labeled Continue</b>: Cannot be converted (rejects conversion)</li>
* </ul>
*
* <p><b>Pattern Examples:</b></p>
* <pre>{@code
* // anyMatch: if (condition) return true; ... return false;
* // noneMatch: if (condition) return false; ... return true;
* // allMatch: if (!condition) return false; ... return true;
* // filter: if (condition) continue; → .filter(x -> !condition)
* }</pre>
*
* <p><b>Usage:</b></p>
* <pre>{@code
* IfStatementAnalyzer analyzer = new IfStatementAnalyzer(forLoop);
* if (analyzer.isEarlyReturnIf(ifStmt)) {
* MatchPatternType pattern = analyzer.getMatchPatternType();
* // Create appropriate stream operation
* }
* }</pre>
*
* @see StreamPipelineBuilder
* @see OperationType
*/
public final class IfStatementAnalyzer {
/**
* Represents the type of match pattern detected in an IF statement.
*/
public enum MatchPatternType {
/** {@code if (condition) return true;} followed by {@code return false;} */
ANY_MATCH,
/** {@code if (condition) return false;} followed by {@code return true;} */
NONE_MATCH,
/** {@code if (!condition) return false;} followed by {@code return true;} */
ALL_MATCH,
/** No match pattern detected */
NONE
}
private final EnhancedForStatement forLoop;
private MatchPatternType detectedPattern = MatchPatternType.NONE;
/**
* Creates a new IfStatementAnalyzer for the given for-loop context.
*
* @param forLoop the enhanced for-loop being analyzed
* @throws IllegalArgumentException if forLoop is null
*/
public IfStatementAnalyzer(EnhancedForStatement forLoop) {
if (forLoop == null) {
throw new IllegalArgumentException("forLoop cannot be null");
}
this.forLoop = forLoop;
}
/**
* Returns the match pattern type detected by the last call to
* {@link #isEarlyReturnIf(IfStatement)}.
*
* @return the detected pattern type, or {@link MatchPatternType#NONE} if none detected
*/
public MatchPatternType getDetectedPattern() {
return detectedPattern;
}
/**
* Checks if an IF statement contains an unlabeled continue statement.
* This pattern can be converted to a negated filter.
*
* <p>Pattern: {@code if (condition) continue;} → {@code .filter(x -> !condition)}</p>
*
* @param ifStatement the IF statement to check
* @return true if the then branch contains an unlabeled continue statement
*/
public boolean isIfWithContinue(IfStatement ifStatement) {
if (ifStatement == null) {
return false;
}
Statement thenStatement = ifStatement.getThenStatement();
if (thenStatement instanceof ContinueStatement) {
ContinueStatement continueStmt = (ContinueStatement) thenStatement;
return continueStmt.getLabel() == null;
}
if (thenStatement instanceof Block) {
Block block = (Block) thenStatement;
if (block.statements().size() == 1 && block.statements().get(0) instanceof ContinueStatement) {
ContinueStatement continueStmt = (ContinueStatement) block.statements().get(0);
return continueStmt.getLabel() == null;
}
}
return false;
}
/**
* Checks if the IF statement contains a labeled continue statement.
* Labeled continues cannot be converted to stream operations.
*
* @param ifStatement the IF statement to check
* @return true if the IF contains a labeled continue statement
*/
public boolean isIfWithLabeledContinue(IfStatement ifStatement) {
if (ifStatement == null) {
return false;
}
Statement thenStatement = ifStatement.getThenStatement();
if (thenStatement instanceof ContinueStatement) {
ContinueStatement continueStmt = (ContinueStatement) thenStatement;
return continueStmt.getLabel() != null;
}
if (thenStatement instanceof Block) {
Block block = (Block) thenStatement;
if (block.statements().size() == 1 && block.statements().get(0) instanceof ContinueStatement) {
ContinueStatement continueStmt = (ContinueStatement) block.statements().get(0);
return continueStmt.getLabel() != null;
}
}
return false;
}
/**
* Checks if the IF statement contains a break statement.
* Break statements cannot be converted to stream operations.
*
* @param ifStatement the IF statement to check
* @return true if the IF contains a break statement
*/
public boolean isIfWithBreak(IfStatement ifStatement) {
if (ifStatement == null) {
return false;
}
Statement thenStatement = ifStatement.getThenStatement();
if (thenStatement instanceof BreakStatement) {
return true;
}
if (thenStatement instanceof Block) {
Block block = (Block) thenStatement;
if (block.statements().size() == 1 && block.statements().get(0) instanceof BreakStatement) {
return true;
}
}
return false;
}
/**
* Checks if the IF statement contains a return statement that cannot be converted.
* Returns that are not boolean literals (like return null, return someValue)
* cannot be converted to stream operations because they would lose the return semantics.
*
* @param ifStatement the IF statement to check
* @return true if the IF contains a non-convertible return statement
*/
public boolean isIfWithUnconvertibleReturn(IfStatement ifStatement) {
if (ifStatement == null) {
return false;
}
Statement thenStatement = ifStatement.getThenStatement();
ReturnStatement returnStmt = extractReturnStatement(thenStatement);
if (returnStmt == null) {
return false;
}
// Check if it's a boolean literal - those can be converted to anyMatch/noneMatch/allMatch
Expression returnExpr = returnStmt.getExpression();
if (returnExpr instanceof BooleanLiteral) {
// This could be part of a match pattern - let other methods handle it
return false;
}
// Any other return (null, variable, method call, etc.) cannot be converted
return true;
}
/**
* Checks if the IF statement body contains an assignment to an external variable.
* Such assignments cannot be safely converted to stream operations because
* they would either be lost or would break stream semantics.
*
* <p><b>Note:</b> Compound assignments (+=, -=, *=, etc.) are allowed because they
* represent accumulator/reducer patterns that CAN be converted to reduce() operations.</p>
*
* @param ifStatement the IF statement to check
* @param loopVarName the loop variable name (assignments to this are internal)
* @return true if the IF body contains a simple assignment to an external variable
*/
public boolean isIfWithExternalAssignment(IfStatement ifStatement, String loopVarName) {
if (ifStatement == null) {
return false;
}
Statement thenStatement = ifStatement.getThenStatement();
return containsExternalSimpleAssignment(thenStatement, loopVarName);
}
/**
* Recursively checks if a statement contains a simple assignment (=) to an external variable.
* Compound assignments (+=, -=, *=, etc.) are NOT flagged because they are accumulator patterns.
*/
private boolean containsExternalSimpleAssignment(Statement stmt, String loopVarName) {
if (stmt == null) {
return false;
}
if (stmt instanceof org.eclipse.jdt.core.dom.ExpressionStatement) {
org.eclipse.jdt.core.dom.ExpressionStatement exprStmt = (org.eclipse.jdt.core.dom.ExpressionStatement) stmt;
Expression expr = exprStmt.getExpression();
if (expr instanceof org.eclipse.jdt.core.dom.Assignment) {
org.eclipse.jdt.core.dom.Assignment assignment = (org.eclipse.jdt.core.dom.Assignment) expr;
// Compound assignments (+=, -=, *=, etc.) are accumulator patterns - allow them
if (assignment.getOperator() != org.eclipse.jdt.core.dom.Assignment.Operator.ASSIGN) {
return false; // Not a simple assignment, it's an accumulator pattern - this single statement is OK
}
// Simple assignment (=) to something other than the loop variable
Expression lhs = assignment.getLeftHandSide();
if (lhs instanceof org.eclipse.jdt.core.dom.SimpleName) {
String varName = ((org.eclipse.jdt.core.dom.SimpleName) lhs).getIdentifier();
// If simple-assigning to something other than the loop variable, it's problematic
if (!varName.equals(loopVarName)) {
return true;
}
}
}
// Other expression statements (method calls, etc.) are OK
return false;
} else if (stmt instanceof Block) {
Block block = (Block) stmt;
for (Object obj : block.statements()) {
if (containsExternalSimpleAssignment((Statement) obj, loopVarName)) {
return true;
}
}
return false;
} else if (stmt instanceof IfStatement) {
IfStatement nested = (IfStatement) stmt;
if (containsExternalSimpleAssignment(nested.getThenStatement(), loopVarName)) {
return true;
}
if (nested.getElseStatement() != null && containsExternalSimpleAssignment(nested.getElseStatement(), loopVarName)) {
return true;
}
return false;
}
// Other statement types (variable declarations, etc.) are OK
return false;
}
/**
* Checks if the IF statement contains an early return pattern that can be
* converted to anyMatch/noneMatch/allMatch.
*
* <p>This method detects patterns by examining:</p>
* <ol>
* <li>The IF statement structure (no else, returns boolean literal)</li>
* <li>The return statement following the loop</li>
* <li>Whether the condition is negated (for allMatch)</li>
* </ol>
*
* <p>After calling this method, use {@link #getDetectedPattern()} to determine
* which pattern was detected.</p>
*
* @param ifStatement the IF statement to check
* @return true if an early return pattern is detected
*/
public boolean isEarlyReturnIf(IfStatement ifStatement) {
detectedPattern = MatchPatternType.NONE;
if (ifStatement == null || ifStatement.getElseStatement() != null) {
return false;
}
Statement thenStmt = ifStatement.getThenStatement();
ReturnStatement returnStmt = extractReturnStatement(thenStmt);
if (returnStmt == null || !(returnStmt.getExpression() instanceof BooleanLiteral)) {
return false;
}
BooleanLiteral returnValue = (BooleanLiteral) returnStmt.getExpression();
BooleanLiteral followingReturn = getReturnAfterLoop();
if (followingReturn == null) {
return false;
}
// Detect pattern based on return values
if (returnValue.booleanValue() && !followingReturn.booleanValue()) {
// if (condition) return true; ... return false; → anyMatch
detectedPattern = MatchPatternType.ANY_MATCH;
return true;
} else if (!returnValue.booleanValue() && followingReturn.booleanValue()) {
// if (condition) return false; ... return true; → noneMatch or allMatch
Expression condition = ifStatement.getExpression();
if (ExpressionHelper.isNegatedExpression(condition)) {
// if (!condition) return false; ... return true; → allMatch
detectedPattern = MatchPatternType.ALL_MATCH;
} else {
// if (condition) return false; ... return true; → noneMatch
detectedPattern = MatchPatternType.NONE_MATCH;
}
return true;
}
return false;
}
/**
* Checks if this IF statement is an early return pattern based on precomputed flags.
* This is used when the pattern type has already been determined by PreconditionsChecker.
*
* @param ifStatement the IF statement to check
* @param isAnyMatch true if this is an anyMatch pattern
* @param isNoneMatch true if this is a noneMatch pattern
* @param isAllMatch true if this is an allMatch pattern
* @return true if the IF matches the specified pattern
*/
public boolean isEarlyReturnIf(IfStatement ifStatement, boolean isAnyMatch, boolean isNoneMatch, boolean isAllMatch) {
if (ifStatement == null || ifStatement.getElseStatement() != null) {
return false;
}
Statement thenStmt = ifStatement.getThenStatement();
ReturnStatement returnStmt = extractReturnStatement(thenStmt);
if (returnStmt == null || !(returnStmt.getExpression() instanceof BooleanLiteral)) {
return false;
}
BooleanLiteral returnValue = (BooleanLiteral) returnStmt.getExpression();
// Check against precomputed flags
if (isAnyMatch && returnValue.booleanValue()) {
detectedPattern = MatchPatternType.ANY_MATCH;
return true;
} else if (isNoneMatch && !returnValue.booleanValue()) {
detectedPattern = MatchPatternType.NONE_MATCH;
return true;
} else if (isAllMatch && !returnValue.booleanValue()) {
detectedPattern = MatchPatternType.ALL_MATCH;
return true;
}
// Fall back to auto-detection
return isEarlyReturnIf(ifStatement);
}
/**
* Creates a match operation from an IF statement based on the detected pattern.
*
* @param ifStmt the IF statement containing the early return pattern
* @return a ProspectiveOperation for the match, or null if no pattern detected
*/
public ProspectiveOperation createMatchOperation(IfStatement ifStmt) {
if (detectedPattern == MatchPatternType.NONE) {
return null;
}
OperationType opType;
switch (detectedPattern) {
case ANY_MATCH:
opType = OperationType.ANYMATCH;
break;
case NONE_MATCH:
opType = OperationType.NONEMATCH;
break;
case ALL_MATCH:
opType = OperationType.ALLMATCH;
break;
default:
return null;
}
Expression condition = ifStmt.getExpression();
// For allMatch, strip the negation since the pattern is "if (!condition) return false"
if (detectedPattern == MatchPatternType.ALL_MATCH) {
condition = ExpressionHelper.stripNegation(condition);
}
return new ProspectiveOperation(condition, opType);
}
/**
* Extracts a return statement from an IF's then-branch.
* Handles both direct return statements and blocks containing a single return.
*/
private ReturnStatement extractReturnStatement(Statement thenStmt) {
if (thenStmt instanceof ReturnStatement) {
return (ReturnStatement) thenStmt;
}
if (thenStmt instanceof Block) {
Block block = (Block) thenStmt;
if (block.statements().size() == 1 && block.statements().get(0) instanceof ReturnStatement) {
return (ReturnStatement) block.statements().get(0);
}
}
return null;
}
/**
* Gets the boolean return value from the statement immediately following the loop.
*
* @return the BooleanLiteral returned after the loop, or null if not found
*/
private BooleanLiteral getReturnAfterLoop() {
ASTNode parent = forLoop.getParent();
if (!(parent instanceof Block)) {
return null;
}
Block block = (Block) parent;
List<?> statements = block.statements();
int loopIndex = statements.indexOf(forLoop);
if (loopIndex == -1 || loopIndex >= statements.size() - 1) {
return null;
}
Statement nextStmt = (Statement) statements.get(loopIndex + 1);
if (nextStmt instanceof ReturnStatement) {
ReturnStatement returnStmt = (ReturnStatement) nextStmt;
Expression expr = returnStmt.getExpression();
if (expr instanceof BooleanLiteral) {
return (BooleanLiteral) expr;
}
}
return null;
}
}