LoopToFunctional.java

/*******************************************************************************
 * Copyright (c) 2021 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.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
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.jdt.internal.common.HelperVisitor;
import org.sandbox.jdt.internal.common.ReferenceHolder;
import org.sandbox.jdt.internal.corext.fix.UseFunctionalCallFixCore;
import org.sandbox.jdt.internal.corext.fix.helper.ConsecutiveLoopGroupDetector.ConsecutiveLoopGroup;

/**
 * Converts enhanced for-loops to functional stream operations.
 * 
 * <p>
 * This class implements the Eclipse JDT cleanup framework to find and transform
 * imperative for-loops into declarative stream pipelines. It integrates with
 * the Eclipse IDE's quick fix and cleanup mechanisms.
 * </p>
 * 
 * <p><b>Example Transformation:</b></p>
 * <pre>{@code
 * // Before:
 * for (Integer l : ls) {
 *     System.out.println(l);
 * }
 * 
 * // After:
 * ls.forEach(l -> {
 *     System.out.println(l);
 * });
 * }</pre>
 * 
 * <p><b>Integration with Eclipse:</b></p>
 * <p>
 * This class extends {@link AbstractFunctionalCall} and is registered as a
 * cleanup contributor in the Eclipse JDT UI framework. It participates in:
 * <ul>
 * <li>Source cleanup actions (Ctrl+Shift+F in Eclipse)</li>
 * <li>Quick fix suggestions (Ctrl+1)</li>
 * <li>Batch cleanup operations</li>
 * </ul>
 * </p>
 * 
 * <p><b>Processing Flow:</b></p>
 * <ol>
 * <li>{@link #find(UseFunctionalCallFixCore, CompilationUnit, Set, Set)}: 
 *     Visits all EnhancedForStatements and identifies convertible loops</li>
 * <li>{@link #rewrite(UseFunctionalCallFixCore, EnhancedForStatement, CompilationUnitRewrite, TextEditGroup)}:
 *     Performs the actual AST transformation for each identified loop</li>
 * <li>{@link #getPreview(boolean)}: Provides before/after preview in Eclipse UI</li>
 * </ol>
 * 
 * <p><b>Safety Checks:</b></p>
 * <p>
 * The conversion only occurs if:
 * <ul>
 * <li>{@link PreconditionsChecker} validates the loop is safe to refactor</li>
 * <li>{@link StreamPipelineBuilder} successfully analyzes the loop body</li>
 * <li>All variables are effectively final</li>
 * <li>No break, labeled continue, or exception throwing occurs</li>
 * </ul>
 * </p>
 * 
 * @see AbstractFunctionalCall
 * @see StreamPipelineBuilder
 * @see PreconditionsChecker
 * @see Refactorer
 */
public class LoopToFunctional extends AbstractFunctionalCall<EnhancedForStatement> {

	@Override
	public void find(UseFunctionalCallFixCore fixcore, CompilationUnit compilationUnit,
			Set<CompilationUnitRewriteOperation> operations, Set<ASTNode> nodesprocessed) {
		
		// PHASE 8: Pre-process to detect consecutive loops adding to same collection
		// This must happen before individual loop processing to avoid incorrect overwrites
		detectAndProcessConsecutiveLoops(fixcore, compilationUnit, operations, nodesprocessed);
		
		// Continue with individual loop processing for non-grouped loops
		ReferenceHolder<Integer, FunctionalHolder> dataHolder= new ReferenceHolder<>();
		ReferenceHolder<ASTNode, Object> sharedDataHolder = new ReferenceHolder<>();
		HelperVisitor.callEnhancedForStatementVisitor(compilationUnit, dataHolder, nodesprocessed,
				(visited, aholder) -> processFoundNode(fixcore, operations, nodesprocessed, visited, aholder, sharedDataHolder),(visited, aholder) -> {});
	}
	
	/**
	 * Detects and processes consecutive loops that add to the same collection.
	 * 
	 * <p>Phase 8 feature: Multiple consecutive for-loops adding to the same list
	 * are converted to Stream.concat() instead of being converted individually
	 * (which would cause overwrites).</p>
	 * 
	 * @param fixcore the fix core instance
	 * @param compilationUnit the compilation unit to scan
	 * @param operations the set to add operations to
	 * @param nodesprocessed the set of already processed nodes
	 */
	private void detectAndProcessConsecutiveLoops(UseFunctionalCallFixCore fixcore, 
			CompilationUnit compilationUnit,
			Set<CompilationUnitRewriteOperation> operations, 
			Set<ASTNode> nodesprocessed) {
		
		// Visit all blocks to find consecutive loop groups
		compilationUnit.accept(new ASTVisitor() {
			@Override
			public boolean visit(Block block) {
				List<ConsecutiveLoopGroup> groups = ConsecutiveLoopGroupDetector.detectGroups(block);
				
				for (ConsecutiveLoopGroup group : groups) {
					// Create a rewrite operation for this group
					operations.add(fixcore.rewriteConsecutiveLoops(group));
					
					// Mark all loops in the group as processed to prevent individual conversion
					for (EnhancedForStatement loop : group.getLoops()) {
						nodesprocessed.add(loop);
					}
				}
				
				return true; // Continue visiting nested blocks
			}
		});
	}

	private boolean processFoundNode(UseFunctionalCallFixCore fixcore,
			Set<CompilationUnitRewriteOperation> operations, Set<ASTNode> nodesprocessed, EnhancedForStatement visited,
			ReferenceHolder<Integer, FunctionalHolder> dataHolder, ReferenceHolder<ASTNode, Object> sharedDataHolder) {
		// Skip loops that have already been processed (e.g., as part of a consecutive loop group)
		if (nodesprocessed.contains(visited)) {
			return false; // Don't visit children of already-processed loops
		}
		
		PreconditionsChecker pc = new PreconditionsChecker(visited, (CompilationUnit) visited.getRoot());
		if (!pc.isSafeToRefactor()) {
			// Loop cannot be safely refactored to functional style
			// Return true to continue visiting children - inner loops may still be convertible
			return false;
		}
		// Check if the loop can be analyzed for stream conversion
		StreamPipelineBuilder builder = new StreamPipelineBuilder(visited, pc);
		if (!builder.analyze()) {
			// Cannot convert this loop to functional style
			// Return true to continue visiting children - inner loops may still be convertible
			return false;
		}
		// V1 doesn't need to store data in the holder, but we pass it to maintain signature compatibility
		operations.add(fixcore.rewrite(visited, sharedDataHolder));
		nodesprocessed.add(visited);
		// Return false to prevent visiting children since this loop was converted
		// (children are now part of the lambda expression)
		return false;
	}

	@Override
	public void rewrite(UseFunctionalCallFixCore upp, final EnhancedForStatement visited,
			final CompilationUnitRewrite cuRewrite, TextEditGroup group, 
			org.sandbox.jdt.internal.common.ReferenceHolder<ASTNode, Object> data) throws CoreException {
		ASTRewrite rewrite = cuRewrite.getASTRewrite();
		PreconditionsChecker pc = new PreconditionsChecker(visited, (CompilationUnit) visited.getRoot());
		Refactorer refactorer = new Refactorer(visited, rewrite, pc, group, cuRewrite);
		// Preconditions already checked in find(), but refactorer.refactor() handles edge cases
		refactorer.refactor();
	}

	@Override
	public String getPreview(boolean afterRefactoring) {
		if (afterRefactoring) {
			return "ls.forEach(l -> {\n	System.out.println(l);\n});\n"; //$NON-NLS-1$
		}
		return "for (Integer l : ls)\n	System.out.println(l);\n\n"; //$NON-NLS-1$
	}
}