ConsecutiveLoopGroupDetector.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 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.SimpleName;
import org.eclipse.jdt.core.dom.Statement;

/**
 * Detects consecutive for-loops that add to the same collection variable.
 * 
 * <p>This detector identifies patterns where multiple consecutive enhanced for-loops
 * all add elements to the same collection. These can be converted to Stream.concat()
 * to preserve all elements instead of overwriting the collection.</p>
 * 
 * <p><b>Pattern Example:</b></p>
 * <pre>{@code
 * List<RuleEntry> entries = new ArrayList<>();
 * for (MethodRule rule : methodRules) {
 *     entries.add(new RuleEntry(rule, TYPE_METHOD));
 * }
 * for (TestRule rule : testRules) {
 *     entries.add(new RuleEntry(rule, TYPE_TEST));
 * }
 * 
 * // Should convert to:
 * List<RuleEntry> entries = Stream.concat(
 *     methodRules.stream().map(rule -> new RuleEntry(rule, TYPE_METHOD)),
 *     testRules.stream().map(rule -> new RuleEntry(rule, TYPE_TEST))
 * ).collect(Collectors.toList());
 * }</pre>
 * 
 * <p><b>Detection Requirements:</b></p>
 * <ul>
 * <li>Loops must be consecutive (only comments allowed between them)</li>
 * <li>All loops must add to the same collection variable</li>
 * <li>The target collection must not be read between loops</li>
 * <li>Each loop body must contain only a simple add operation</li>
 * </ul>
 * 
 * @see LoopToFunctional
 * @see StreamPipelineBuilder
 */
public class ConsecutiveLoopGroupDetector {

	/**
	 * Represents a group of consecutive loops adding to the same collection.
	 */
	public static class ConsecutiveLoopGroup {
		private final String targetVariable;
		private final List<EnhancedForStatement> loops;

		public ConsecutiveLoopGroup(String targetVariable, List<EnhancedForStatement> loops) {
			this.targetVariable = targetVariable;
			this.loops = new ArrayList<>(loops);
		}

		public String getTargetVariable() {
			return targetVariable;
		}

		public List<EnhancedForStatement> getLoops() {
			return new ArrayList<>(loops);
		}

		public int size() {
			return loops.size();
		}
	}

	/**
	 * Detects all consecutive loop groups in the given block.
	 * 
	 * @param block the block to analyze
	 * @return list of detected consecutive loop groups (size 2+)
	 */
	public static List<ConsecutiveLoopGroup> detectGroups(Block block) {
		if (block == null) {
			return new ArrayList<>();
		}

		List<ConsecutiveLoopGroup> groups = new ArrayList<>();
		@SuppressWarnings("unchecked")
		List<Statement> statements = block.statements();

		int i = 0;
		while (i < statements.size()) {
			Statement stmt = statements.get(i);

			// Check if this is an EnhancedForStatement with simple add pattern
			if (stmt instanceof EnhancedForStatement) {
				EnhancedForStatement firstLoop = (EnhancedForStatement) stmt;
				String targetVar = getTargetAddVariable(firstLoop);

				if (targetVar != null) {
					// Found a loop that adds to a variable - check for consecutive similar loops
					List<EnhancedForStatement> group = new ArrayList<>();
					group.add(firstLoop);

					// Scan forward for consecutive loops adding to same variable
					int j = i + 1;
					while (j < statements.size()) {
						Statement nextStmt = statements.get(j);

						if (nextStmt instanceof EnhancedForStatement) {
							EnhancedForStatement nextLoop = (EnhancedForStatement) nextStmt;
							String nextTargetVar = getTargetAddVariable(nextLoop);

							if (targetVar.equals(nextTargetVar)) {
								// Same target variable - add to group
								group.add(nextLoop);
								j++;
							} else {
								// Different target or not an add pattern - stop group
								break;
							}
						} else {
							// Non-loop statement breaks the consecutive sequence
							break;
						}
					}

					// Only create a group if we found 2+ consecutive loops
					if (group.size() >= 2) {
						groups.add(new ConsecutiveLoopGroup(targetVar, group));
						i = j; // Skip past all loops in this group
						continue;
					}
				}
			}

			i++;
		}

		return groups;
	}

	/**
	 * Extracts the target variable name if the loop body is a simple add pattern.
	 * 
	 * <p>Detects patterns like:</p>
	 * <pre>{@code
	 * for (Type item : collection) {
	 *     targetList.add(expression);
	 * }
	 * }</pre>
	 * 
	 * @param loop the enhanced for-loop to check
	 * @return the target variable name, or null if not a simple add pattern
	 */
	private static String getTargetAddVariable(EnhancedForStatement loop) {
		Statement body = loop.getBody();

		// Handle both Block and single statement
		List<Statement> bodyStatements = new ArrayList<>();
		if (body instanceof Block) {
			@SuppressWarnings("unchecked")
			List<Statement> stmts = ((Block) body).statements();
			bodyStatements.addAll(stmts);
		} else {
			bodyStatements.add(body);
		}

		// Must be exactly one statement
		if (bodyStatements.size() != 1) {
			return null;
		}

		Statement stmt = bodyStatements.get(0);
		if (!(stmt instanceof ExpressionStatement)) {
			return null;
		}

		ExpressionStatement exprStmt = (ExpressionStatement) stmt;
		if (!(exprStmt.getExpression() instanceof MethodInvocation)) {
			return null;
		}

		MethodInvocation methodInv = (MethodInvocation) exprStmt.getExpression();

		// Check method name is "add"
		if (!"add".equals(methodInv.getName().getIdentifier())) {
			return null;
		}

		// Check invoked on a SimpleName (the collection variable)
		if (!(methodInv.getExpression() instanceof SimpleName)) {
			return null;
		}

		SimpleName receiver = (SimpleName) methodInv.getExpression();
		return receiver.getIdentifier();
	}
}