CollectPatternDetector.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 org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.sandbox.jdt.internal.corext.util.VariableResolver;

/**
 * Detects and handles COLLECT patterns in loop statements.
 * 
 * <p>This class is responsible for identifying collection accumulation patterns
 * that can be converted to stream collect operations:</p>
 * 
 * <ul>
 * <li><b>List.add():</b> {@code result.add(item)}</li>
 * <li><b>Set.add():</b> {@code set.add(item)}</li>
 * </ul>
 * 
 * <p><b>Usage Example:</b></p>
 * <pre>{@code
 * CollectPatternDetector detector = new CollectPatternDetector(forLoop);
 * ProspectiveOperation collectOp = detector.detectCollectOperation(stmt);
 * if (collectOp != null) {
 *     String targetVar = detector.getTargetVariable();
 *     CollectorType collectorType = detector.getCollectorType();
 *     // ... use in stream pipeline
 * }
 * }</pre>
 * 
 * @see ProspectiveOperation
 * @see CollectorType
 * @see StreamPipelineBuilder
 */
public final class CollectPatternDetector {

	private final ASTNode contextNode;
	private String targetVariable = null;
	private CollectorType collectorType = null;

	/**
	 * Creates a new CollectPatternDetector.
	 * 
	 * @param contextNode the context node (typically the for-loop) for type resolution
	 * @throws IllegalArgumentException if contextNode is null
	 */
	public CollectPatternDetector(ASTNode contextNode) {
		if (contextNode == null) {
			throw new IllegalArgumentException("contextNode cannot be null");
		}
		this.contextNode = contextNode;
	}

	/**
	 * Returns the target collection variable name detected during the last
	 * {@link #detectCollectOperation(Statement)} call.
	 * 
	 * @return the target variable name, or null if no collect was detected
	 */
	public String getTargetVariable() {
		return targetVariable;
	}

	/**
	 * Returns the collector type detected during the last
	 * {@link #detectCollectOperation(Statement)} call.
	 * 
	 * @return the collector type (TO_LIST or TO_SET), or null if not detected
	 */
	public CollectorType getCollectorType() {
		return collectorType;
	}

	/**
	 * Detects if a statement contains a COLLECT pattern.
	 * 
	 * <p><b>Supported Patterns:</b></p>
	 * <ul>
	 * <li>Collection add: {@code result.add(item)}, {@code set.add(value)}</li>
	 * </ul>
	 * 
	 * <p><b>Examples:</b></p>
	 * <pre>{@code
	 * // TO_LIST pattern
	 * result.add(item);  // → .collect(Collectors.toList())
	 * 
	 * // TO_SET pattern
	 * set.add(value);  // → .collect(Collectors.toSet())
	 * }</pre>
	 * 
	 * @param stmt the statement to check
	 * @return a COLLECT operation if detected, null otherwise
	 */
	public ProspectiveOperation detectCollectOperation(Statement stmt) {
		if (!(stmt instanceof ExpressionStatement)) {
			return null;
		}

		ExpressionStatement exprStmt = (ExpressionStatement) stmt;
		Expression expr = exprStmt.getExpression();

		// Check for method invocation: result.add(item)
		if (expr instanceof MethodInvocation) {
			return detectAddMethodPattern((MethodInvocation) expr, stmt);
		}

		return null;
	}

	/**
	 * Detects collection.add() patterns.
	 * Pattern: result.add(item)
	 */
	private ProspectiveOperation detectAddMethodPattern(MethodInvocation methodInv, Statement stmt) {
		// Check if method name is "add"
		if (!"add".equals(methodInv.getName().getIdentifier())) {
			return null;
		}

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

		String varName = ((SimpleName) receiver).getIdentifier();

		// Resolve the type of the collection variable
		ITypeBinding varType = VariableResolver.getTypeBinding(contextNode, varName);
		if (varType == null) {
			return null;
		}

		// Determine the collector type based on the collection type
		CollectorType type = CollectorType.fromCollectionType(varType.getErasure().getQualifiedName());
		if (type == null) {
			return null;
		}

		// Extract the expression being added (the argument to add())
		if (methodInv.arguments().isEmpty()) {
			return null;
		}

		Expression addedExpr = (Expression) methodInv.arguments().get(0);

		targetVariable = varName;
		collectorType = type;

		// Create a COLLECT operation with the added expression
		ProspectiveOperation op = new ProspectiveOperation(addedExpr, OperationType.COLLECT, null);
		op.setCollectorType(type);
		op.setTargetVariable(varName);

		return op;
	}

	/**
	 * Checks if a statement declares and initializes an empty collection.
	 * Pattern: List<T> result = new ArrayList<>();
	 * 
	 * @param stmt the statement to check
	 * @return the variable name if it's an empty collection initialization, null otherwise
	 */
	public static String isEmptyCollectionDeclaration(Statement stmt) {
		if (!(stmt instanceof VariableDeclarationStatement)) {
			return null;
		}

		VariableDeclarationStatement varDecl = (VariableDeclarationStatement) stmt;
		if (varDecl.fragments().size() != 1) {
			return null;
		}

		VariableDeclarationFragment fragment = (VariableDeclarationFragment) varDecl.fragments().get(0);
		Expression initializer = fragment.getInitializer();

		// Check if initialized with new ArrayList<>() or new HashSet<>()
		if (!(initializer instanceof ClassInstanceCreation)) {
			return null;
		}

		ClassInstanceCreation creation = (ClassInstanceCreation) initializer;
		
		// Check that no arguments are passed (empty collection)
		if (!creation.arguments().isEmpty()) {
			return null;
		}

		ITypeBinding typeBinding = creation.resolveTypeBinding();
		if (typeBinding == null) {
			return null;
		}

		// Check if it's a supported collection type
		String qualifiedName = typeBinding.getErasure().getQualifiedName();
		if (CollectorType.fromCollectionType(qualifiedName) != null) {
			return fragment.getName().getIdentifier();
		}

		return null;
	}

	/**
	 * Extracts the expression being added from a COLLECT operation.
	 * For example, in "result.add(foo(item))", extracts "foo(item)".
	 * 
	 * @param stmt the statement containing the collect operation
	 * @return the expression to be collected, or null if none
	 */
	public Expression extractCollectExpression(Statement stmt) {
		if (!(stmt instanceof ExpressionStatement)) {
			return null;
		}

		ExpressionStatement exprStmt = (ExpressionStatement) stmt;
		Expression expr = exprStmt.getExpression();

		if (expr instanceof MethodInvocation) {
			MethodInvocation methodInv = (MethodInvocation) expr;
			if ("add".equals(methodInv.getName().getIdentifier()) && !methodInv.arguments().isEmpty()) {
				return (Expression) methodInv.arguments().get(0);
			}
		}

		return null;
	}

	/**
	 * Checks if the target collection variable is read (not just written to) 
	 * within the loop body. Reading the collection during iteration prevents 
	 * safe conversion to stream collect.
	 * 
	 * <p><b>Example of unsafe read:</b></p>
	 * <pre>{@code
	 * for (Integer item : items) {
	 *     result.add(item);
	 *     System.out.println("Size: " + result.size());  // Read prevents conversion
	 * }
	 * }</pre>
	 * 
	 * @param loopBody the loop body statement
	 * @param collectTargetVar the name of the collection variable being collected to
	 * @return true if the target variable is read during iteration
	 */
	public boolean isTargetReadDuringIteration(Statement loopBody, String collectTargetVar) {
		if (loopBody == null || collectTargetVar == null) {
			return false;
		}
		
		final boolean[] hasRead = {false};
		
		loopBody.accept(new ASTVisitor() {
			@Override
			public boolean visit(MethodInvocation node) {
				Expression receiver = node.getExpression();
				if (receiver instanceof SimpleName) {
					String varName = ((SimpleName) receiver).getIdentifier();
					if (varName.equals(collectTargetVar)) {
						String methodName = node.getName().getIdentifier();
						// "add" is a write operation, anything else is a read
						if (!"add".equals(methodName)) {
							hasRead[0] = true;
						}
					}
				}
				return true;
			}
			
			@Override
			public boolean visit(SimpleName node) {
				// Check if this is a direct reference to the target variable
				// that is not part of a method invocation on that variable
				if (node.getIdentifier().equals(collectTargetVar)) {
					// Check if parent is a method invocation where this is the receiver
					ASTNode parent = node.getParent();
					if (parent instanceof MethodInvocation) {
						MethodInvocation parentMethod = (MethodInvocation) parent;
						if (parentMethod.getExpression() == node) {
							// This is the receiver of a method call - handled above
							return true;
						}
					}
					// Direct reference to the variable (e.g., passing it to a method) is a read
					hasRead[0] = true;
				}
				return true;
			}
		});
		
		return hasRead[0];
	}
}