AbstractMethodMigrationPlugin.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.lib;

import static org.sandbox.jdt.internal.corext.fix.helper.lib.JUnitConstants.*;

import java.util.Set;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperationWithSourceRange;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.internal.common.HelperVisitorFactory;
import org.sandbox.jdt.internal.common.ReferenceHolder;
import org.sandbox.jdt.internal.corext.fix.JUnitCleanUpFixCore;

/**
 * Abstract base class for plugins that migrate method calls from one class to another.
 * Used for Assert → Assertions and Assume → Assumptions migrations.
 * 
 * <p>Subclasses need to implement:</p>
 * <ul>
 *   <li>{@link #getSourceClass()} - The JUnit 4 class (e.g., "org.junit.Assert")</li>
 *   <li>{@link #getTargetClass()} - The JUnit 5 class (e.g., "org.junit.jupiter.api.Assertions")</li>
 *   <li>{@link #getTargetSimpleName()} - Simple name of target class (e.g., "Assertions")</li>
 *   <li>{@link #getMethodNames()} - Set of method names to migrate</li>
 *   <li>{@link #getPreview(boolean)} - Preview for UI</li>
 * </ul>
 * 
 * <p>The base class handles:</p>
 * <ul>
 *   <li>Finding method calls and static imports</li>
 *   <li>Replacing class references in qualified calls</li>
 *   <li>Updating imports</li>
 *   <li>Parameter reordering (message to last position)</li>
 * </ul>
 */
public abstract class AbstractMethodMigrationPlugin extends AbstractTool<ReferenceHolder<Integer, JunitHolder>> {

	/**
	 * Returns the fully qualified name of the source class (JUnit 4).
	 * @return e.g., "org.junit.Assert"
	 */
	protected abstract String getSourceClass();
	
	/**
	 * Returns the fully qualified name of the target class (JUnit 5).
	 * @return e.g., "org.junit.jupiter.api.Assertions"
	 */
	protected abstract String getTargetClass();
	
	/**
	 * Returns the simple name of the target class.
	 * @return e.g., "Assertions"
	 */
	protected abstract String getTargetSimpleName();
	
	/**
	 * Returns the set of method names to migrate.
	 * @return e.g., Set.of("assertEquals", "assertTrue", "assertFalse", ...)
	 */
	protected abstract Set<String> getMethodNames();
	
	/**
	 * Returns methods that require parameter reordering (message moves to last).
	 * Default implementation returns all methods.
	 * @return set of method names that need parameter reordering
	 */
	protected Set<String> getMethodsRequiringReorder() {
		return getMethodNames();
	}

	@Override
	public void find(JUnitCleanUpFixCore fixcore, CompilationUnit compilationUnit,
			Set<CompilationUnitRewriteOperationWithSourceRange> operations, Set<ASTNode> nodesprocessed) {
		ReferenceHolder<Integer, JunitHolder> dataHolder = ReferenceHolder.createIndexed();
		HelperVisitorFactory.forMethodCalls(getSourceClass(), getMethodNames())
			.andStaticImports()
			.andImportsOf(getSourceClass())
			.in(compilationUnit)
			.excluding(nodesprocessed)
			.processEach(dataHolder, (visited, aholder) -> processFoundNode(fixcore, operations, visited, aholder));
	}

	private boolean processFoundNode(JUnitCleanUpFixCore fixcore,
			Set<CompilationUnitRewriteOperationWithSourceRange> operations, ASTNode node,
			ReferenceHolder<Integer, JunitHolder> dataHolder) {
		return addStandardRewriteOperation(fixcore, operations, node, dataHolder);
	}

	@Override
	protected void process2Rewrite(TextEditGroup group, ASTRewrite rewriter, AST ast,
			ImportRewrite importRewriter, JunitHolder junitHolder) {
		ASTNode node = junitHolder.getMinv();
		
		if (node instanceof MethodInvocation) {
			processMethodInvocation(group, rewriter, ast, importRewriter, (MethodInvocation) node);
		} else if (node instanceof ImportDeclaration) {
			processImportDeclaration(group, rewriter, ast, importRewriter, 
				(ImportDeclaration) node);
		}
	}

	/**
	 * Processes a method invocation, updating the class reference and reordering parameters if needed.
	 */
	protected void processMethodInvocation(TextEditGroup group, ASTRewrite rewriter, AST ast,
			ImportRewrite importRewriter, MethodInvocation methodInvocation) {
		
		String methodName = methodInvocation.getName().getIdentifier();
		
		// Update qualified expression if present (e.g., Assert.assertEquals → Assertions.assertEquals)
		Expression expression = methodInvocation.getExpression();
		if (expression instanceof SimpleName) {
			SimpleName simpleName = (SimpleName) expression;
			String sourceSimpleName = getSourceClass().substring(getSourceClass().lastIndexOf('.') + 1);
			if (sourceSimpleName.equals(simpleName.getIdentifier())) {
				SimpleName newName = ast.newSimpleName(getTargetSimpleName());
				ASTNodes.replaceButKeepComment(rewriter, simpleName, newName, group);
				importRewriter.addImport(getTargetClass());
				importRewriter.removeImport(getSourceClass());
			}
		}
		
		// Reorder parameters if needed (message moves to last position in JUnit 5)
		if (getMethodsRequiringReorder().contains(methodName)) {
			reorderMessageParameter(group, rewriter, methodInvocation);
		}
	}

	/**
	 * Processes an import declaration, replacing the source import with target import.
	 * 
	 * <p>Handles both specific and wildcard imports:</p>
	 * <ul>
	 *   <li>Wildcard: import static org.junit.Assert.* → import static org.junit.jupiter.api.Assertions.*</li>
	 *   <li>Specific: import static org.junit.Assert.assertEquals → import static org.junit.jupiter.api.Assertions.assertEquals</li>
	 * </ul>
	 */
	protected void processImportDeclaration(TextEditGroup group, ASTRewrite rewriter, AST ast,
			ImportRewrite importRewriter, ImportDeclaration importDecl) {
		
		String importName = importDecl.getName().getFullyQualifiedName();
		
		if (importDecl.isStatic()) {
			// Handle static imports
			if (importDecl.isOnDemand()) {
				// Wildcard import: import static org.junit.Assert.*
				// Note: importName is "org.junit.Assert" (without .*)
				if (getSourceClass().equals(importName)) {
					importRewriter.removeStaticImport(importName + ".*");
					importRewriter.addStaticImport(getTargetClass(), "*", false);
				}
			} else {
				// Specific import: import static org.junit.Assert.assertEquals
				// Note: importName is "org.junit.Assert.assertEquals"
				if (importName.startsWith(getSourceClass() + ".")) {
					String methodName = importName.substring(getSourceClass().length() + 1);
					importRewriter.removeStaticImport(importName);
					importRewriter.addStaticImport(getTargetClass(), methodName, false);
				}
			}
		} else {
			// Handle regular imports
			if (getSourceClass().equals(importName)) {
				importRewriter.removeImport(getSourceClass());
				importRewriter.addImport(getTargetClass());
			}
		}
	}

	/**
	 * Reorders parameters so that the message parameter moves to the last position.
	 * JUnit 4: assertEquals(String message, expected, actual)
	 * JUnit 5: assertEquals(expected, actual, String message)
	 * 
	 * Delegates to existing {@link #reorderParameters} method.
	 */
	protected void reorderMessageParameter(TextEditGroup group, ASTRewrite rewriter,
			MethodInvocation methodInvocation) {
		// Delegate to existing reorderParameters method in AbstractTool
		// which handles the parameter reordering logic
		reorderParameters(methodInvocation, rewriter, group, ONEPARAM_ASSERTIONS, TWOPARAM_ASSERTIONS);
	}
}