IteratorWhileToEnhancedFor.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.ArrayList;
import java.util.List;
import java.util.Set;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.dom.AST;
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.Statement;
import org.eclipse.jdt.core.dom.WhileStatement;
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.LoopModel;
import org.sandbox.functional.core.model.SourceDescriptor;
import org.sandbox.functional.core.terminal.ForEachTerminal;
import org.sandbox.jdt.internal.common.ReferenceHolder;
import org.sandbox.jdt.internal.corext.fix.UseFunctionalCallFixCore;
import org.sandbox.jdt.internal.corext.fix.helper.IteratorPatternDetector.IteratorPattern;

/**
 * Transformer for converting iterator-based while-loops to enhanced for-loops.
 * 
 * <p>Transformation: {@code Iterator<T> it = c.iterator(); while (it.hasNext()) { T item = it.next(); ... }} → {@code for (T item : collection) { ... }}</p>
 * 
 * <p>Uses the ULR pipeline: {@code LoopModelBuilder → LoopModel → ASTEnhancedForRenderer}.</p>
 * 
 * <p><b>Safety rules (Issue #670):</b></p>
 * <ul>
 *   <li>Rejects conversion when iterator.remove() is used (cannot be expressed in enhanced for)</li>
 *   <li>Rejects conversion when multiple iterator.next() calls are detected</li>
 *   <li>Rejects conversion when break or labeled continue is present</li>
 * </ul>
 * 
 * @see LoopModel
 * @see ASTEnhancedForRenderer
 * @see <a href="https://github.com/carstenartur/sandbox/issues/453">Issue #453</a>
 * @see <a href="https://github.com/carstenartur/sandbox/issues/549">Issue #549</a>
 * @see <a href="https://github.com/carstenartur/sandbox/issues/670">Issue #670</a>
 */
public class IteratorWhileToEnhancedFor extends AbstractFunctionalCall<ASTNode> {

	private final IteratorPatternDetector patternDetector = new IteratorPatternDetector();
	private final IteratorLoopAnalyzer loopAnalyzer = new IteratorLoopAnalyzer();
	
	@Override
	public void find(UseFunctionalCallFixCore fixcore, CompilationUnit compilationUnit,
			Set<CompilationUnitRewriteOperation> operations, Set<ASTNode> nodesprocessed) {
		compilationUnit.accept(new ASTVisitor() {
			@Override
			public boolean visit(WhileStatement node) {
				if (nodesprocessed.contains(node)) {
					return false;
				}
				
				// Find previous statement for while-iterator pattern
				if (!IteratorPatternDetector.isStatementInBlock(node)) {
					return true;
				}
				
				Block parentBlock = (Block) node.getParent();
				Statement previousStmt = IteratorPatternDetector.findPreviousStatement(parentBlock, node);
				
				IteratorPattern pattern = patternDetector.detectWhilePattern(node, previousStmt);
				if (pattern == null) {
					return true;
				}
				
				// Issue #670: Safety check - reject conversion if iterator has unsafe usage
				// (remove(), multiple next(), break, labeled continue)
				IteratorLoopAnalyzer.SafetyAnalysis analysis = loopAnalyzer.analyze(
						node.getBody(), pattern.iteratorVariableName());
				if (!analysis.isSafe()) {
					return true;
				}
				
				// Mark both the iterator declaration and the while loop as processed
				nodesprocessed.add(previousStmt);
				nodesprocessed.add(node);
				
				ReferenceHolder<ASTNode, Object> holder = ReferenceHolder.create();
				holder.put(node, pattern);
				holder.put(previousStmt, pattern); // Store pattern for both nodes
				
				operations.add(fixcore.rewrite(node, holder));
				
				return false;
			}
		});
	}

	@Override
	public void rewrite(UseFunctionalCallFixCore useExplicitEncodingFixCore, ASTNode visited,
			CompilationUnitRewrite cuRewrite, TextEditGroup group, ReferenceHolder<ASTNode, Object> data)
			throws CoreException {
		if (!(visited instanceof WhileStatement whileStmt)) {
			return;
		}
		
		Object patternObj = data.get(visited);
		if (!(patternObj instanceof IteratorPattern pattern)) {
			return;
		}
		
		AST ast = cuRewrite.getAST();
		ASTRewrite rewrite = cuRewrite.getASTRewrite();
		
		// Find the iterator declaration statement that should be removed
		Block parentBlock = (Block) whileStmt.getParent();
		Statement iteratorDecl = IteratorPatternDetector.findPreviousStatement(parentBlock, whileStmt);
		
		// Build LoopModel from the iterator-while pattern using ULR pipeline
		LoopModel model = buildLoopModel(pattern);
		
		// Extract body statements (skip the first item = it.next() declaration)
		List<Statement> bodyStatements = extractBodyStatements(whileStmt);
		
		// Render enhanced for-loop using ULR-based renderer
		ASTEnhancedForRenderer renderer = new ASTEnhancedForRenderer(ast, rewrite);
		renderer.render(model, whileStmt, iteratorDecl, bodyStatements, group);
	}

	/**
	 * Builds a LoopModel from an iterator-while pattern using the ULR pipeline.
	 */
	private LoopModel buildLoopModel(IteratorPattern pattern) {
		String collectionExpr = pattern.collectionExpression().toString();
		String elementType = pattern.elementType() != null ? pattern.elementType() : "Object"; //$NON-NLS-1$
		String elementName = "item"; //$NON-NLS-1$
		
		return new LoopModelBuilder()
			.source(SourceDescriptor.SourceType.COLLECTION, collectionExpr, elementType)
			.element(elementName, elementType, false)
			.terminal(new ForEachTerminal(List.of(), false))
			.build();
	}

	/**
	 * Extracts the actual body statements from the while loop, skipping the
	 * first {@code T item = it.next()} variable declaration.
	 */
	private List<Statement> extractBodyStatements(WhileStatement whileStmt) {
		List<Statement> result = new ArrayList<>();
		Statement whileBody = whileStmt.getBody();
		
		if (whileBody instanceof Block block) {
			boolean skipFirst = false;
			
			// Check if first statement is item = it.next()
			if (!block.statements().isEmpty()) {
				Object firstStmt = block.statements().get(0);
				if (firstStmt instanceof org.eclipse.jdt.core.dom.VariableDeclarationStatement) {
					skipFirst = true;
				}
			}
			
			int startIdx = skipFirst ? 1 : 0;
			for (int i = startIdx; i < block.statements().size(); i++) {
				result.add((Statement) block.statements().get(i));
			}
		} else {
			result.add(whileBody);
		}
		return result;
	}

	@Override
	public String getPreview(boolean afterRefactoring) {
		if (afterRefactoring) {
			return """
					for (String item : items) {
						System.out.println(item);
					}
					""";
		}
		return """
				Iterator<String> it = items.iterator();
				while (it.hasNext()) {
					String item = it.next();
					System.out.println(item);
				}
				""";
	}
}