TraditionalForHandler.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Carsten Hammer
 *******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.helper;

import java.util.List;
import java.util.Set;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.dom.*;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.functional.core.builder.LoopModelBuilder;
import org.sandbox.functional.core.model.ElementDescriptor;
import org.sandbox.functional.core.model.LoopModel;
import org.sandbox.functional.core.model.SourceDescriptor;
import org.sandbox.functional.core.terminal.ForEachTerminal;
import org.sandbox.functional.core.transformer.LoopModelTransformer;
import org.sandbox.jdt.internal.common.ReferenceHolder;
import org.sandbox.jdt.internal.corext.fix.UseFunctionalCallFixCore;

/**
 * Handler for converting traditional index-based for-loops to IntStream.range() operations.
 * 
 * <p>This handler converts classic for-loops with integer counters to functional IntStream operations:</p>
 * <pre>{@code
 * // Before:
 * for (int i = 0; i < 10; i++) {
 *     System.out.println(i);
 * }
 * 
 * // After:
 * IntStream.range(0, 10).forEach(i -> System.out.println(i));
 * }</pre>
 * 
 * <p><b>Supported Patterns:</b></p>
 * <ul>
 *   <li>Initializer: {@code int i = start} (single variable declaration with int type)</li>
 *   <li>Condition: {@code i < end} or {@code i <= end} (comparison with loop variable)</li>
 *   <li>Updater: {@code i++} or {@code ++i} (increment operator)</li>
 * </ul>
 * 
 * <p><b>Infrastructure:</b></p>
 * <ul>
 *   <li>Uses {@link SourceDescriptor.SourceType#EXPLICIT_RANGE} for range representation</li>
 *   <li>Leverages existing {@link ASTStreamRenderer} for IntStream.range() rendering</li>
 *   <li>Builds {@link LoopModel} with {@link ForEachTerminal} for body statements</li>
 * </ul>
 * 
 * <p><b>Thread-Safety Guards:</b></p>
 * <ul>
 *   <li><b>Nested loop guard</b>: {@code isNestedInsideLoop()} rejects loops inside other loops
 *       to avoid interfering with the {@code EnhancedForHandler}'s scope analysis and to
 *       prevent generating IntStream calls inside lambdas where mutable loop state could be captured</li>
 *   <li><b>Unconvertible statement detection</b>: {@code containsUnconvertibleStatements()} rejects
 *       loops with {@code break}, {@code continue}, or {@code return} which cannot be expressed
 *       in lambda bodies</li>
 *   <li><b>Sequential-only streams</b>: Always generates {@code IntStream.range()} (sequential),
 *       never parallel streams, avoiding complex synchronization requirements</li>
 *   <li><b>Synchronized block detection</b>: Handled upstream by {@code JdtLoopExtractor} and
 *       {@code PreconditionsChecker}, which reject loops containing synchronized statements
 *       before they reach this handler</li>
 * </ul>
 * 
 * <p><b>Naming Note:</b> This class is named after the <i>source</i> loop type (traditional for-loop),
 * not the target format. The architecture supports bidirectional transformations, so the name
 * describes what loop pattern this handler processes.</p>
 * 
 * @see SourceDescriptor.SourceType#EXPLICIT_RANGE
 * @see ASTStreamRenderer#renderSource(SourceDescriptor)
 */
public class TraditionalForHandler extends AbstractFunctionalCall<ForStatement> {
    
    @Override
    public void find(UseFunctionalCallFixCore fixcore, CompilationUnit compilationUnit,
                     Set<CompilationUnitRewriteOperation> operations, Set<ASTNode> nodesprocessed) {
        
        compilationUnit.accept(new ASTVisitor() {
            @Override
            public boolean visit(ForStatement node) {
                if (nodesprocessed.contains(node)) {
                    return false;
                }
                
                // Analyze the for-loop structure
                ForLoopPattern pattern = analyzeForLoop(node);
                if (pattern == null) {
                    return true; // Not a convertible pattern, continue visiting
                }
                
                // Mark as processed and add operation
                nodesprocessed.add(node);
                ReferenceHolder<ASTNode, Object> holder = ReferenceHolder.create();
                holder.put(node, pattern);
                operations.add(fixcore.rewrite(node, holder));
                
                return false; // Don't visit children of convertible loops
            }
        });
    }
    
    /**
     * Analyzes a ForStatement to determine if it follows the convertible pattern.
     * 
     * @param forStmt the ForStatement to analyze
     * @return a ForLoopPattern if convertible, null otherwise
     */
    private ForLoopPattern analyzeForLoop(ForStatement forStmt) {
        // Skip for-loops nested inside other loops to avoid converting
        // inner loops that the EnhancedForHandler expects to remain unchanged
        if (isNestedInsideLoop(forStmt)) {
            return null;
        }
        
        // Check initializer: must be single variable declaration of type int
        List<?> initializers = forStmt.initializers();
        if (initializers.size() != 1) {
            return null;
        }
        
        Object initObj = initializers.get(0);
        if (!(initObj instanceof VariableDeclarationExpression)) {
            return null;
        }
        
        VariableDeclarationExpression varDecl = (VariableDeclarationExpression) initObj;
        Type type = varDecl.getType();
        if (!isPrimitiveInt(type)) {
            return null;
        }
        
        List<?> fragments = varDecl.fragments();
        if (fragments.size() != 1) {
            return null;
        }
        
        VariableDeclarationFragment fragment = (VariableDeclarationFragment) fragments.get(0);
        String loopVarName = fragment.getName().getIdentifier();
        Expression startExpr = fragment.getInitializer();
        
        if (startExpr == null) {
            return null;
        }
        
        // Check condition: must be infix comparison with loop variable
        Expression condition = forStmt.getExpression();
        if (!(condition instanceof InfixExpression)) {
            return null;
        }
        
        InfixExpression infixCond = (InfixExpression) condition;
        InfixExpression.Operator operator = infixCond.getOperator();
        
        // Check that left operand is the loop variable
        Expression leftOperand = infixCond.getLeftOperand();
        if (!(leftOperand instanceof SimpleName)) {
            return null;
        }
        
        SimpleName leftName = (SimpleName) leftOperand;
        if (!leftName.getIdentifier().equals(loopVarName)) {
            return null;
        }
        
        Expression endExpr = infixCond.getRightOperand();
        if (endExpr == null) {
            return null;
        }
        
        boolean inclusive = false;
        if (operator == InfixExpression.Operator.LESS) {
            // i < end  →  IntStream.range(start, end)
            inclusive = false;
        } else if (operator == InfixExpression.Operator.LESS_EQUALS) {
            // i <= end  →  IntStream.rangeClosed(start, end) or range(start, end+1)
            inclusive = true;
        } else {
            return null; // Unsupported operator
        }
        
        // Check updater: must be i++ or ++i
        List<?> updaters = forStmt.updaters();
        if (updaters.size() != 1) {
            return null;
        }
        
        Object updaterObj = updaters.get(0);
        boolean isIncrement = false;
        
        if (updaterObj instanceof PostfixExpression) {
            PostfixExpression postfix = (PostfixExpression) updaterObj;
            if (postfix.getOperator() == PostfixExpression.Operator.INCREMENT) {
                Expression operand = postfix.getOperand();
                if (operand instanceof SimpleName && ((SimpleName) operand).getIdentifier().equals(loopVarName)) {
                    isIncrement = true;
                }
            }
        } else if (updaterObj instanceof PrefixExpression) {
            PrefixExpression prefix = (PrefixExpression) updaterObj;
            if (prefix.getOperator() == PrefixExpression.Operator.INCREMENT) {
                Expression operand = prefix.getOperand();
                if (operand instanceof SimpleName && ((SimpleName) operand).getIdentifier().equals(loopVarName)) {
                    isIncrement = true;
                }
            }
        }
        
        if (!isIncrement) {
            return null;
        }
        
        // Extract body
        Statement body = forStmt.getBody();
        
        // Check if body contains unconvertible statements (break, continue, return)
        if (containsUnconvertibleStatements(body)) {
            return null;
        }
        
        // Issue #670: Check if the index variable is used in the body for complex
        // patterns like a[i+1], a[i-1], or i%2 that indicate neighbor access or
        // non-trivial index semantics. These patterns suggest the loop relies on
        // index arithmetic that may not be safely convertible.
        if (usesIndexBeyondSimpleAccess(body, loopVarName)) {
            return null;
        }
        
        return new ForLoopPattern(loopVarName, startExpr, endExpr, inclusive, body);
    }
    
    /**
     * Checks if a Type represents primitive int.
     */
    private boolean isPrimitiveInt(Type type) {
        if (type instanceof PrimitiveType) {
            PrimitiveType primType = (PrimitiveType) type;
            return primType.getPrimitiveTypeCode() == PrimitiveType.INT;
        }
        return false;
    }
    
    /**
     * Checks if the for-loop is nested inside another loop (enhanced-for, while, for, do-while).
     * Nested traditional for-loops are skipped to avoid interfering with the EnhancedForHandler's
     * analysis of outer enhanced-for loops.
     */
    private boolean isNestedInsideLoop(ForStatement forStmt) {
        ASTNode parent = forStmt.getParent();
        while (parent != null) {
            if (parent instanceof EnhancedForStatement
                    || parent instanceof ForStatement
                    || parent instanceof WhileStatement
                    || parent instanceof DoStatement) {
                return true;
            }
            if (parent instanceof MethodDeclaration || parent instanceof TypeDeclaration) {
                break;
            }
            parent = parent.getParent();
        }
        return false;
    }
    
    /**
     * Checks if the loop body contains unconvertible statements.
     * Statements containing break, continue, or return cannot be converted to lambda.
     */
    private boolean containsUnconvertibleStatements(Statement body) {
        final boolean[] hasUnconvertible = {false};
        
        body.accept(new ASTVisitor() {
            @Override
            public boolean visit(BreakStatement node) {
                hasUnconvertible[0] = true;
                return false;
            }
            
            @Override
            public boolean visit(ContinueStatement node) {
                hasUnconvertible[0] = true;
                return false;
            }
            
            @Override
            public boolean visit(ReturnStatement node) {
                hasUnconvertible[0] = true;
                return false;
            }
        });
        
        return hasUnconvertible[0];
    }
    
    /**
     * Checks if the index variable is used in the loop body for complex patterns
     * that go beyond simple counter semantics.
     * 
     * <p>Detects the index variable participating in arithmetic InfixExpressions
     * (e.g., {@code i + 1}, {@code i - 1}, {@code i * 2}, {@code i % 2}).
     * This also catches array/list neighbor access patterns like {@code a[i+1]}
     * because the subscript expression {@code i+1} is itself an InfixExpression.</p>
     * 
     * <p>These patterns indicate the loop relies on index relationships between
     * iterations, which may not be safely convertible to stream operations.</p>
     * 
     * @param body the loop body statement
     * @param indexVarName the name of the index variable
     * @return true if the index is used in complex patterns
     * 
     * @see <a href="https://github.com/carstenartur/sandbox/issues/670">Issue #670</a>
     */
    private boolean usesIndexBeyondSimpleAccess(Statement body, String indexVarName) {
        final boolean[] hasComplexUsage = {false};
        
        body.accept(new ASTVisitor() {
            @Override
            public boolean visit(InfixExpression node) {
                // Check if the index variable is used in arithmetic operations
                // like i+1, i-1, i*2, i%2
                if (isIndexInArithmeticExpression(node, indexVarName)) {
                    hasComplexUsage[0] = true;
                    return false;
                }
                return true;
            }
        });
        
        return hasComplexUsage[0];
    }
    
    /**
     * Checks if an InfixExpression uses the index variable in arithmetic.
     * Detects patterns: i+1, i-1, i*2, i%2, etc., including nested and chained
     * arithmetic expressions (e.g., (i + 1) + offset, multiplier * (i - 1)).
     */
    private boolean isIndexInArithmeticExpression(InfixExpression expr, String indexVarName) {
        if (!isArithmeticOperator(expr.getOperator())) {
            return false;
        }

        // Recursively inspect all operands (left, right, and extended) for
        // arithmetic usage of the index variable.
        if (containsIndexInArithmeticOperand(expr.getLeftOperand(), indexVarName)) {
            return true;
        }
        if (containsIndexInArithmeticOperand(expr.getRightOperand(), indexVarName)) {
            return true;
        }
        @SuppressWarnings("unchecked")
        List<Expression> extended = expr.extendedOperands();
        for (Expression operand : extended) {
            if (containsIndexInArithmeticOperand(operand, indexVarName)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns {@code true} if the given operator is an arithmetic operator
     * relevant for index arithmetic detection.
     */
    private boolean isArithmeticOperator(InfixExpression.Operator op) {
        return op == InfixExpression.Operator.PLUS
                || op == InfixExpression.Operator.MINUS
                || op == InfixExpression.Operator.TIMES
                || op == InfixExpression.Operator.DIVIDE
                || op == InfixExpression.Operator.REMAINDER;
    }

    /**
     * Recursively checks whether the given expression contains an arithmetic
     * use of the index variable.
     */
    private boolean containsIndexInArithmeticOperand(Expression expr, String indexVarName) {
        if (expr == null) {
            return false;
        }

        if (expr instanceof SimpleName) {
            return ((SimpleName) expr).getIdentifier().equals(indexVarName);
        }

        if (expr instanceof ParenthesizedExpression) {
            Expression inner = ((ParenthesizedExpression) expr).getExpression();
            return containsIndexInArithmeticOperand(inner, indexVarName);
        }

        if (expr instanceof InfixExpression infix) {
            if (!isArithmeticOperator(infix.getOperator())) {
                return false;
            }
            if (containsIndexInArithmeticOperand(infix.getLeftOperand(), indexVarName)) {
                return true;
            }
            if (containsIndexInArithmeticOperand(infix.getRightOperand(), indexVarName)) {
                return true;
            }
            @SuppressWarnings("unchecked")
            List<Expression> extended = infix.extendedOperands();
            for (Expression operand : extended) {
                if (containsIndexInArithmeticOperand(operand, indexVarName)) {
                    return true;
                }
            }
        }

        return false;
    }
    
    @Override
    public void rewrite(UseFunctionalCallFixCore upp, ForStatement visited,
                        CompilationUnitRewrite cuRewrite, TextEditGroup group,
                        ReferenceHolder<ASTNode, Object> data) throws CoreException {
        
        Object patternObj = data.get(visited);
        if (!(patternObj instanceof ForLoopPattern pattern)) {
            return;
        }
        
        AST ast = cuRewrite.getRoot().getAST();
        ASTRewrite rewrite = cuRewrite.getASTRewrite();
        
        // Build LoopModel
        LoopModel model = buildLoopModel(pattern, ast);
        
        // Create renderer
        ASTStreamRenderer renderer = new ASTStreamRenderer(ast, rewrite, cuRewrite.getRoot(), pattern.body());
        
        // Transform using LoopModelTransformer
        LoopModelTransformer<Expression> transformer = new LoopModelTransformer<>(renderer);
        Expression streamExpression = transformer.transform(model);
        
        if (streamExpression != null) {
            // Wrap in ExpressionStatement
            ExpressionStatement newStatement = ast.newExpressionStatement(streamExpression);
            rewrite.replace(visited, newStatement, group);
            
            // Add IntStream import
            cuRewrite.getImportRewrite().addImport("java.util.stream.IntStream");
        }
    }
    
    /**
     * Builds a LoopModel from the analyzed ForLoopPattern.
     */
    private LoopModel buildLoopModel(ForLoopPattern pattern, AST ast) {
        // Get start and end expressions as strings
        String startStr = pattern.startExpr().toString();
        String endStr = pattern.endExpr().toString();
        
        // Adjust end expression for inclusive range (i <= end)
        if (pattern.inclusive()) {
            // For i <= end, we need IntStream.range(start, (end) + 1)
            // Parenthesize end expression to preserve operator precedence
            endStr = "(" + endStr + ") + 1";
        }
        
        // Build EXPLICIT_RANGE source descriptor
        String rangeExpression = startStr + "," + endStr;
        SourceDescriptor source = new SourceDescriptor(
            SourceDescriptor.SourceType.EXPLICIT_RANGE,
            rangeExpression,
            "int"
        );
        
        // Build element descriptor for the loop variable
        ElementDescriptor element = new ElementDescriptor(
            pattern.loopVarName(),
            "int",
            false // not a collection element
        );
        
        // Extract body statements and convert to strings
        List<String> bodyStatements = extractBodyStatementsAsStrings(pattern.body());
        
        // Build ForEachTerminal
        ForEachTerminal terminal = new ForEachTerminal(bodyStatements, false); // uses forEach (not forEachOrdered)
        
        // Build and return LoopModel
        return new LoopModelBuilder()
            .source(source)
            .element(element)
            .terminal(terminal)
            .build();
    }
    
    /**
     * Extracts statements from the loop body and converts them to expression strings.
     * Trailing semicolons are stripped because ForEachTerminal / ASTStreamRenderer.createExpression()
     * expects pure expressions, not statements.  This matches how
     * {@code JdtLoopExtractor.addSimpleForEachTerminal()} produces body strings for
     * the existing EnhancedForHandler.
     */
    @SuppressWarnings("unchecked")
    private List<String> extractBodyStatementsAsStrings(Statement body) {
        return ExpressionHelper.bodyStatementsToStrings(body);
    }
    
    @Override
    public String getPreview(boolean afterRefactoring) {
        if (afterRefactoring) {
            return "IntStream.range(0, 10).forEach(i -> System.out.println(i));\n";
        }
        return "for (int i = 0; i < 10; i++)\n    System.out.println(i);\n";
    }
    
    /**
     * Immutable representation of an analyzed traditional for-loop pattern.
     *
     * @param loopVarName the loop variable name (e.g. {@code "i"})
     * @param startExpr the start expression (e.g. {@code 0})
     * @param endExpr the end expression (e.g. {@code list.size()})
     * @param inclusive {@code true} for {@code <=}, {@code false} for {@code <}
     * @param body the loop body statement
     */
    private record ForLoopPattern(String loopVarName, Expression startExpr, Expression endExpr,
                                  boolean inclusive, Statement body) {
    }
}