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;
	/**
	 * 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>
	 * </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>
	 * 
	 * @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
		return !throwsException && !containsBreak && !containsLabeledContinue && (!containsReturn || allowedReturn)
				&& !containsNEFs && !containsNestedLoop;
	}

	/**
	 * 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;
	}

	/**
	 * 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() {
		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 - mark as not convertible
			// We check that this is NOT the same node as the outer loop
			if (node != loop) {
				containsNestedLoop = true;
			}
			return true;
		}).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);
			}
			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 block = (Block) parent;
			@SuppressWarnings("unchecked")
			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;

		analyzeEffectivelyFinalVariables();
	}

	/**
	 * Checks if variables declared within the loop are effectively final.
	 * 
	 * <p>
	 * A variable is effectively final if it is never modified after its
	 * initialization. This is important for stream operations because lambda
	 * expressions can only capture effectively final variables from their enclosing
	 * scope.
	 * </p>
	 * 
	 * <p>
	 * Sets {@link #containsNEFs} to true if any non-effectively-final variable is
	 * found.
	 * </p>
	 * 
	 * <p>
	 * <b>Important:</b> Variables declared inside the loop body that are modified
	 * within the same loop iteration are NOT checked here. Such variables will be
	 * converted to map operations in the stream pipeline. Only variables that would
	 * need to be captured from an outer scope are relevant for this check.
	 * </p>
	 */
	private void analyzeEffectivelyFinalVariables() {
		// Variables declared INSIDE the loop body are allowed to be modified,
		// as they will be converted to map operations in the stream pipeline.
		// We need to check for variables declared OUTSIDE the loop that
		// are used inside the loop body - these must be effectively final
		// to be captured by a lambda.
		//
		// EXCEPTION: For reducer patterns (hasReducer == true), the accumulator
		// variable doesn't need to be effectively final because it will become
		// the reduce accumulator, not a captured variable in a lambda.
		
		if (!(loop instanceof EnhancedForStatement)) {
			return;
		}
		
		// If this is a reducer pattern, the accumulator variable is allowed
		// to be non-effectively-final since it will be part of the reduce operation
		// and won't need to be captured by a lambda.
		if (hasReducer) {
			// For reducers, we don't check effectively final because the
			// modified variable becomes the reduce accumulator, not a captured var
			return;
		}
		
		EnhancedForStatement enhancedFor = (EnhancedForStatement) loop;
		Statement body = enhancedFor.getBody();
		
		// Get the loop parameter name (the iteration variable)
		String loopParamName = enhancedFor.getParameter().getName().getIdentifier();
		
		// Collect all names of variables declared inside the loop
		Set<String> innerVariableNames = new HashSet<>();
		for (VariableDeclarationFragment frag : innerVariables) {
			innerVariableNames.add(frag.getName().getIdentifier());
		}
		
		// Visit all SimpleName references in the loop body
		body.accept(new ASTVisitor() {
			@Override
			public boolean visit(SimpleName node) {
				// Skip if this is a declaration, not a reference
				if (node.isDeclaration()) {
					return true;
				}
				
				// Skip if this is the loop parameter
				if (node.getIdentifier().equals(loopParamName)) {
					return true;
				}
				
				// Skip if this is a variable declared inside the loop
				if (innerVariableNames.contains(node.getIdentifier())) {
					return true;
				}
				
				// Resolve the binding
				IBinding binding = node.resolveBinding();
				if (binding == null) {
					// Cannot resolve binding - skip this check (don't block conversion)
					return true;
				}
				
				if (binding instanceof IVariableBinding) {
					IVariableBinding varBinding = (IVariableBinding) binding;
					
					// Skip fields - they're not captured as local variables
					if (varBinding.isField()) {
						return true;
					}
					
					// Skip method parameters - they're allowed to be captured
					if (varBinding.isParameter()) {
						return true;
					}
					
					// Check if the variable is effectively final
					// Note: final variables and effectively final variables both return true
					if (!varBinding.isEffectivelyFinal()) {
						// This variable is captured from an outer scope but is not effectively final
						// It cannot be used in a lambda
//						containsNEFs = true;
					}
				}
				return true;
			}
		});
	}

	/**
	 * 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) {
				collectTargetVariable = ((SimpleName) receiver).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)) {
			return;
		}

		EnhancedForStatement forLoop = (EnhancedForStatement) loop;
		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) {
			expr = ((ParenthesizedExpression) expr).getExpression();
		}
		
		return expr instanceof PrefixExpression
				&& ((PrefixExpression) expr).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)) {
			return null;
		}
		
		Block block = (Block) parent;
		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) {
			ReturnStatement returnStmt = (ReturnStatement) nextStmt;
			Expression expr = returnStmt.getExpression();
			if (expr instanceof BooleanLiteral) {
				return (BooleanLiteral) expr;
			}
		}
		
		return null;
	}
}