Refactorer.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.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.text.edits.TextEditGroup;

/**
 * Orchestrates the refactoring of enhanced for-loops into functional stream pipelines.
 * 
 * <p>
 * This class serves as the main entry point for converting imperative for-loops
 * into declarative stream operations. It coordinates the analysis, validation,
 * and transformation phases using {@link PreconditionsChecker} and
 * {@link StreamPipelineBuilder}.
 * </p>
 * 
 * <p><b>Refactoring Process:</b></p>
 * <ol>
 * <li><b>Precondition Check</b>: Validates loop is safe to refactor (no breaks,
 *     returns, exceptions, etc.)</li>
 * <li><b>Analysis</b>: Parses loop body into stream operations</li>
 * <li><b>Pipeline Construction</b>: Builds method invocation chain</li>
 * <li><b>AST Replacement</b>: Replaces for-loop with stream pipeline in AST</li>
 * </ol>
 * 
 * <p><b>Usage Example:</b></p>
 * <pre>{@code
 * PreconditionsChecker preconditions = new PreconditionsChecker(forLoop, cu);
 * Refactorer refactorer = new Refactorer(forLoop, rewrite, preconditions, group);
 * 
 * if (refactorer.isRefactorable()) {
 *     refactorer.refactor(); // Performs the transformation
 * }
 * }</pre>
 * 
 * <p><b>AST Modification:</b></p>
 * <p>
 * This class uses Eclipse JDT's {@link ASTRewrite} mechanism to modify the AST.
 * All changes are tracked in a {@link TextEditGroup} for editor integration.
 * Comments on the original for-loop are preserved when possible.
 * </p>
 * 
 * <p><b>Thread Safety:</b> This class is not thread-safe. Create a new instance
 * for each refactoring operation.</p>
 * 
 * @see StreamPipelineBuilder
 * @see PreconditionsChecker
 * @see ASTRewrite
 */
public class Refactorer {
	
	private static final String JAVA_UTIL_ARRAYS = "java.util.Arrays"; //$NON-NLS-1$
	private static final String JAVA_UTIL_STREAM_COLLECTORS = StreamConstants.COLLECTORS_CLASS; //$NON-NLS-1$
	
	private final EnhancedForStatement forLoop;
	private final ASTRewrite rewrite;
	private final PreconditionsChecker preconditions;
	private final TextEditGroup group;
	private final CompilationUnitRewrite cuRewrite;

	/**
	 * Creates a new Refactorer.
	 * 
	 * @param forLoop       the enhanced for-loop to refactor
	 * @param rewrite       the AST rewrite to use
	 * @param preconditions the preconditions checker
	 * @param group         the text edit group for tracking changes
	 * @deprecated Use {@link #Refactorer(EnhancedForStatement, ASTRewrite, PreconditionsChecker, TextEditGroup, CompilationUnitRewrite)} instead
	 */
	@Deprecated
	public Refactorer(EnhancedForStatement forLoop, ASTRewrite rewrite, PreconditionsChecker preconditions,
			TextEditGroup group) {
		this(forLoop, rewrite, preconditions, group, null);
	}
	
	/**
	 * Creates a new Refactorer with CompilationUnitRewrite for import management.
	 * 
	 * @param forLoop       the enhanced for-loop to refactor
	 * @param rewrite       the AST rewrite to use
	 * @param preconditions the preconditions checker
	 * @param group         the text edit group for tracking changes
	 * @param cuRewrite     the compilation unit rewrite for import management (may be null)
	 */
	public Refactorer(EnhancedForStatement forLoop, ASTRewrite rewrite, PreconditionsChecker preconditions,
			TextEditGroup group, CompilationUnitRewrite cuRewrite) {
		this.forLoop = forLoop;
		this.rewrite = rewrite;
		this.preconditions = preconditions;
		this.group = group;
		this.cuRewrite = cuRewrite;
	}

	/** Checks if the loop can be refactored to a stream operation. */
	public boolean isRefactorable() {
		if (!preconditions.isSafeToRefactor()) {
			return false;
		}
		// Also verify that the StreamPipelineBuilder can analyze and build the pipeline
		StreamPipelineBuilder builder = new StreamPipelineBuilder(forLoop, preconditions);
		return builder.analyze() && builder.buildPipeline() != null;
	}

	/**
	 * Performs the refactoring of the loop into a stream operation. Uses
	 * StreamPipelineBuilder for all conversions.
	 */
	public void refactor() {
		refactorWithBuilder();
	}

	/**
	 * Refactors the loop using the StreamPipelineBuilder approach.
	 */
	private void refactorWithBuilder() {
		StreamPipelineBuilder builder = new StreamPipelineBuilder(forLoop, preconditions);

		if (!builder.analyze()) {
			return; // Cannot convert
		}

		MethodInvocation pipeline = builder.buildPipeline();
		Statement replacement = builder.wrapPipeline(pipeline);
		if (replacement != null) {
			// Add Arrays import if needed (for array iteration)
			if (builder.needsArraysImport() && cuRewrite != null) {
				cuRewrite.getImportRewrite().addImport(JAVA_UTIL_ARRAYS);
			}
			// Add Collectors import if needed (for collect operations)
			if (builder.needsCollectorsImport() && cuRewrite != null) {
				cuRewrite.getImportRewrite().addImport(JAVA_UTIL_STREAM_COLLECTORS);
			}
			
			// Check if this is a COLLECT operation with a preceding empty collection declaration
			Statement mergedDeclaration = tryMergeWithPrecedingDeclaration(builder, pipeline, replacement);
			if (mergedDeclaration != null) {
				// Use the merged declaration instead
				replacement = mergedDeclaration;
			}
			
			ASTNodes.replaceButKeepComment(rewrite, forLoop, replacement, group);
		}
	}
	
	/**
	 * Attempts to merge a COLLECT operation with its preceding empty collection declaration.
	 * 
	 * <p>Pattern detected:</p>
	 * <pre>{@code
	 * List<Integer> result = new ArrayList<>();  // Empty collection declaration
	 * for (Integer l : ls) {
	 *     result.add(l);  // COLLECT operation
	 * }
	 * }</pre>
	 * 
	 * <p>Merged result:</p>
	 * <pre>{@code
	 * List<Integer> result = ls.stream().collect(Collectors.toList());
	 * }</pre>
	 * 
	 * @param builder the stream pipeline builder
	 * @param pipeline the stream pipeline method invocation
	 * @param replacement the current replacement statement (assignment)
	 * @return a merged VariableDeclarationStatement if merge is possible, null otherwise
	 */
	private Statement tryMergeWithPrecedingDeclaration(StreamPipelineBuilder builder, 
			MethodInvocation pipeline, Statement replacement) {
		// Only apply to COLLECT operations
		if (!builder.isCollectOperation()) {
			return null;
		}
		
		// The replacement must be an ExpressionStatement containing an assignment
		if (!(replacement instanceof ExpressionStatement)) {
			return null;
		}
		
		// Find the target variable from the CollectPatternDetector
		String targetVariable = builder.getCollectTargetVariable();
		if (targetVariable == null) {
			return null;
		}
		
		// Get the parent block
		ASTNode parent = forLoop.getParent();
		if (!(parent instanceof Block)) {
			return null;
		}
		
		Block block = (Block) parent;
		List<?> statements = block.statements();
		
		// Find the index of the for-loop
		int forLoopIndex = -1;
		for (int i = 0; i < statements.size(); i++) {
			if (statements.get(i) == forLoop) {
				forLoopIndex = i;
				break;
			}
		}
		
		// Check if there's a statement before the for-loop
		if (forLoopIndex <= 0) {
			return null;
		}
		
		Statement precedingStmt = (Statement) statements.get(forLoopIndex - 1);
		
		// Check if the preceding statement is an empty collection declaration for the same variable
		String declaredVar = CollectPatternDetector.isEmptyCollectionDeclaration(precedingStmt);
		if (declaredVar == null || !declaredVar.equals(targetVariable)) {
			return null;
		}
		
		// We have a match! Create a merged VariableDeclarationStatement
		VariableDeclarationStatement originalDecl = (VariableDeclarationStatement) precedingStmt;
		VariableDeclarationFragment originalFragment = 
				(VariableDeclarationFragment) originalDecl.fragments().get(0);
		
		AST ast = forLoop.getAST();
		
		// Create new VariableDeclarationFragment with the pipeline as initializer
		VariableDeclarationFragment newFragment = ast.newVariableDeclarationFragment();
		newFragment.setName(ast.newSimpleName(targetVariable));
		newFragment.setInitializer((MethodInvocation) ASTNode.copySubtree(ast, pipeline));
		
		// Create new VariableDeclarationStatement with the same type
		VariableDeclarationStatement newDecl = ast.newVariableDeclarationStatement(newFragment);
		Type originalType = originalDecl.getType();
		newDecl.setType((Type) ASTNode.copySubtree(ast, originalType));
		
		// Copy modifiers if any
		newDecl.modifiers().addAll(ASTNode.copySubtrees(ast, originalDecl.modifiers()));
		
		// Remove the preceding declaration
		rewrite.remove(precedingStmt, group);
		
		return newDecl;
	}
}