LoopToFunctionalV2.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.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.model.LoopMetadata;
import org.sandbox.functional.core.model.LoopModel;
import org.sandbox.functional.core.model.SourceDescriptor;
import org.sandbox.functional.core.transformer.LoopModelTransformer;
import org.sandbox.jdt.internal.common.HelperVisitor;
import org.sandbox.jdt.internal.common.ReferenceHolder;
import org.sandbox.jdt.internal.corext.fix.UseFunctionalCallFixCore;

/**
 * V2 implementation using the Unified Loop Representation (ULR) architecture.
 * 
 * <p>This class uses the abstract LoopModel from the core module and
 * the ASTStreamRenderer to generate JDT AST nodes.</p>
 * 
 * @see LoopModel
 * @see ASTStreamRenderer
 * @see LoopModelTransformer
 */
public class LoopToFunctionalV2 extends AbstractFunctionalCall<EnhancedForStatement> {
    
    private final JdtLoopExtractor extractor = new JdtLoopExtractor();
    
    @Override
    public void find(UseFunctionalCallFixCore fixcore, CompilationUnit compilationUnit,
                     Set<CompilationUnitRewriteOperation> operations, Set<ASTNode> nodesprocessed) {
        
        ReferenceHolder<ASTNode, Object> dataHolder = new ReferenceHolder<>();
        HelperVisitor.callEnhancedForStatementVisitor(compilationUnit, dataHolder, nodesprocessed,
                (visited, holder) -> processFoundNode(fixcore, operations, nodesprocessed, visited, holder),
                (visited, holder) -> {});
    }
    
    private boolean processFoundNode(UseFunctionalCallFixCore fixcore,
                                      Set<CompilationUnitRewriteOperation> operations,
                                      Set<ASTNode> nodesprocessed,
                                      EnhancedForStatement visited,
                                      ReferenceHolder<ASTNode, Object> holder) {
        
        // Extract LoopModel AND original body from AST
        JdtLoopExtractor.ExtractedLoop extracted = extractor.extract(visited);
        
        // Check if convertible using the model's metadata
        if (!isConvertible(extracted.model)) {
            return false;
        }
        
        // Store extracted loop (model + body) for later rewrite in the shared holder
        holder.put(visited, extracted);
        operations.add(fixcore.rewrite(visited, holder));
        nodesprocessed.add(visited);
        
        return false;
    }
    
    @Override
    public void rewrite(UseFunctionalCallFixCore upp, EnhancedForStatement visited,
                        CompilationUnitRewrite cuRewrite, TextEditGroup group,
                        ReferenceHolder<ASTNode, Object> data) throws CoreException {
        
        // Get the extracted loop from the holder (passed from find())
        JdtLoopExtractor.ExtractedLoop extracted = (JdtLoopExtractor.ExtractedLoop) data.get(visited);
        
        if (extracted == null || !isConvertible(extracted.model)) {
            return;
        }
        
        AST ast = cuRewrite.getRoot().getAST();
        ASTRewrite rewrite = cuRewrite.getASTRewrite();
        CompilationUnit compilationUnit = cuRewrite.getRoot();
        
        // Create renderer with original body for AST node access
        ASTStreamRenderer renderer = new ASTStreamRenderer(ast, rewrite, compilationUnit, extracted.originalBody);
        
        Expression streamExpression;
        boolean usedDirectForEach = false;
        
        // Check if we can use direct forEach (no intermediate operations, ForEachTerminal)
        if (canUseDirectForEach(extracted.model)) {
            // Use direct forEach rendering (e.g., list.forEach(...) instead of list.stream().forEach(...))
            org.sandbox.functional.core.terminal.ForEachTerminal terminal = 
                (org.sandbox.functional.core.terminal.ForEachTerminal) extracted.model.getTerminal();
            String varName = extracted.model.getElement() != null 
                ? extracted.model.getElement().variableName() 
                : "x";
            streamExpression = renderer.renderDirectForEach(
                extracted.model.getSource(), 
                terminal.bodyStatements(), 
                varName, 
                terminal.ordered()
            );
            usedDirectForEach = true;
        } else {
            // Use standard stream-based transformation
            LoopModelTransformer<Expression> transformer = new LoopModelTransformer<>(renderer);
            streamExpression = transformer.transform(extracted.model);
        }
        
        if (streamExpression != null) {
            // Create the replacement statement
            ExpressionStatement newStatement = ast.newExpressionStatement(streamExpression);
            
            // Replace the for statement
            rewrite.replace(visited, newStatement, group);
            
            // Add necessary imports (only for stream-based transformations)
            if (!usedDirectForEach) {
                addImports(cuRewrite, extracted.model);
            } else {
                // For direct forEach on arrays, we still need Arrays import
                if (extracted.model.getSource().type() == SourceDescriptor.SourceType.ARRAY) {
                    cuRewrite.getImportRewrite().addImport("java.util.Arrays");
                }
            }
        }
    }
    
    /**
     * Checks if the loop model can use direct forEach (without .stream() prefix).
     * 
     * <p>Direct forEach is used for the simplest forEach patterns to generate more idiomatic code:</p>
     * <ul>
     *   <li>No intermediate operations (no filter, map, etc.)</li>
     *   <li>Terminal operation is ForEachTerminal</li>
     *   <li>Source is COLLECTION or ITERABLE (arrays need Arrays.stream().forEach())</li>
     * </ul>
     * 
     * <p><b>Immutability Considerations:</b></p>
     * <p>Direct forEach is safe for both mutable and immutable collections:
     * <ul>
     *   <li>Immutable collections (List.of, Collections.unmodifiableList, etc.) support forEach</li>
     *   <li>forEach is a terminal operation that only reads elements</li>
     *   <li>No structural modifications are made to the collection</li>
     *   <li>Side effects within the lambda body are the user's responsibility</li>
     * </ul>
     * </p>
     * 
     * @param model the loop model to check
     * @return true if direct forEach can be used
     */
    private boolean canUseDirectForEach(LoopModel model) {
        if (model == null || model.getTerminal() == null) {
            return false;
        }
        
        // Must have no intermediate operations
        if (!model.getOperations().isEmpty()) {
            return false;
        }
        
        // Terminal must be ForEachTerminal
        if (!(model.getTerminal() instanceof org.sandbox.functional.core.terminal.ForEachTerminal)) {
            return false;
        }
        
        // Source must be COLLECTION or ITERABLE
        // Arrays don't have a forEach method, so they still need a stream-based forEach path
        // and are intentionally handled outside of this direct-forEach optimization.
        SourceDescriptor.SourceType sourceType = model.getSource().type();
        return sourceType == SourceDescriptor.SourceType.COLLECTION 
            || sourceType == SourceDescriptor.SourceType.ITERABLE;
    }
    
    private boolean isConvertible(LoopModel model) {
        if (model == null) return false;
        
        LoopMetadata metadata = model.getMetadata();
        if (metadata == null) return true; // No metadata = assume convertible
        
        // Don't convert if has break, continue, or return
        return !metadata.hasBreak() && 
               !metadata.hasContinue() && 
               !metadata.hasReturn() &&
               !metadata.modifiesCollection();
    }
    
    private void addImports(CompilationUnitRewrite cuRewrite, LoopModel model) {
        // Add necessary imports based on source type
        switch (model.getSource().type()) {
            case ARRAY:
                cuRewrite.getImportRewrite().addImport("java.util.Arrays");
                break;
            case ITERABLE:
                cuRewrite.getImportRewrite().addImport("java.util.stream.StreamSupport");
                break;
            default:
                // No additional imports needed for Collection.stream()
                break;
        }
        
        // Add Collectors import if using collect terminal
        if (model.getTerminal() instanceof org.sandbox.functional.core.terminal.CollectTerminal) {
            cuRewrite.getImportRewrite().addImport("java.util.stream.Collectors");
        }
    }
    
    @Override
    public String getPreview(boolean afterRefactoring) {
        if (afterRefactoring) {
            return "items.forEach(item -> System.out.println(item));\n"; //$NON-NLS-1$
        }
        return "for (String item : items)\n    System.out.println(item);\n"; //$NON-NLS-1$
    }
}