AbstractTool.java

/*******************************************************************************
 * Copyright (c) 2021, 2025 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.Collection;
import java.util.Set;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.internal.corext.dom.ScopeAnalyzer;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperationWithSourceRange;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.internal.common.ReferenceHolder;
import org.sandbox.jdt.internal.corext.fix.JUnitCleanUpFixCore;
import org.sandbox.jdt.internal.corext.util.AnnotationUtils;
import org.sandbox.jdt.internal.corext.util.ASTNavigationUtils;
import org.sandbox.jdt.internal.corext.util.NamingUtils;

/**
 * Abstract base class for JUnit migration tools.
 * Provides common functionality for transforming JUnit 3/4 tests to JUnit 5.
 * Delegates to specialized helper classes for most operations.
 * 
 * @param <T> Type found in Visitor
 */
public abstract class AbstractTool<T> {

	/**
	 * Gets all variable names used in the scope of the given AST node.
	 * 
	 * @param node the AST node to analyze
	 * @return collection of variable names used in the node's scope
	 */
	public static Collection<String> getUsedVariableNames(ASTNode node) {
		CompilationUnit root = (CompilationUnit) node.getRoot();
		return new ScopeAnalyzer(root).getUsedVariableNames(node.getStartPosition(), node.getLength());
	}

	/**
	 * Extracts the class name from a field declaration's initializer.
	 * Delegates to {@link NamingUtils#extractClassNameFromField(FieldDeclaration)}.
	 * 
	 * @param field the field declaration to extract from
	 * @return the class name, or null if not found
	 */
	public String extractClassNameFromField(FieldDeclaration field) {
		return NamingUtils.extractClassNameFromField(field);
	}

	/**
	 * Finds JUnit migration opportunities in the compilation unit.
	 * Implementations should scan for patterns that need to be migrated from JUnit 3/4 to JUnit 5.
	 * 
	 * @param fixcore the JUnit cleanup fix core
	 * @param compilationUnit the compilation unit to analyze
	 * @param operations set to collect rewrite operations
	 * @param nodesprocessed set of already processed AST nodes to avoid duplicates
	 */
	public abstract void find(JUnitCleanUpFixCore fixcore, CompilationUnit compilationUnit,
			Set<CompilationUnitRewriteOperationWithSourceRange> operations, Set<ASTNode> nodesprocessed);

	/**
	 * Gets a preview of the code before or after refactoring.
	 * Used to display examples in the Eclipse cleanup preferences UI.
	 * 
	 * @param afterRefactoring if true, returns the "after" preview; if false, returns the "before" preview
	 * @return a code snippet showing the transformation (formatted as Java source code)
	 */
	public abstract String getPreview(boolean afterRefactoring);

	/**
	 * Finds the type definition (TypeDeclaration or AnonymousClassDeclaration) for a field.
	 * Checks the field's initializer and type binding to locate the definition.
	 * 
	 * @param fieldDeclaration the field declaration to analyze
	 * @param cu the compilation unit containing the field
	 * @return the type definition node, or null if not found
	 */
	protected ASTNode getTypeDefinitionForField(FieldDeclaration fieldDeclaration, CompilationUnit cu) {
		return (ASTNode) fieldDeclaration.fragments().stream()
				.filter(VariableDeclarationFragment.class::isInstance)
				.map(VariableDeclarationFragment.class::cast)
				.map(fragment -> getTypeDefinitionFromFragment((VariableDeclarationFragment) fragment, cu))
				.filter(java.util.Objects::nonNull)
				.findFirst()
				.orElse(null);
	}

	private ASTNode getTypeDefinitionFromFragment(VariableDeclarationFragment fragment, CompilationUnit cu) {
		// Check initializer
		Expression initializer = fragment.getInitializer();
		if (initializer instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation) {
			org.eclipse.jdt.core.dom.ClassInstanceCreation classInstanceCreation = (org.eclipse.jdt.core.dom.ClassInstanceCreation) initializer;

			// Check for anonymous class
			AnonymousClassDeclaration anonymousClass = classInstanceCreation.getAnonymousClassDeclaration();
			if (anonymousClass != null) {
				return anonymousClass;
			}

			// Check type binding
			ITypeBinding typeBinding = classInstanceCreation.resolveTypeBinding();
			return ASTNavigationUtils.findTypeDeclarationForBinding(typeBinding, cu);
		}

		// Check field type if no initialization is present
		IVariableBinding fieldBinding = fragment.resolveBinding();
		if (fieldBinding != null) {
			ITypeBinding fieldTypeBinding = fieldBinding.getType();
			return ASTNavigationUtils.findTypeDeclarationForBinding(fieldTypeBinding, cu);
		}

		return null;
	}

	/**
	 * Checks if a class has either a default constructor or no constructors at all.
	 * Used to determine if ExternalResource subclasses can be easily migrated.
	 * 
	 * @param classNode the class to check
	 * @return true if the class has a default constructor or no constructors
	 */
	protected boolean hasDefaultConstructorOrNoConstructor(TypeDeclaration classNode) {
		boolean hasConstructor = false;

		for (Object bodyDecl : classNode.bodyDeclarations()) {
			if (bodyDecl instanceof org.eclipse.jdt.core.dom.MethodDeclaration) {
				org.eclipse.jdt.core.dom.MethodDeclaration method = (org.eclipse.jdt.core.dom.MethodDeclaration) bodyDecl;
				if (method.isConstructor()) {
					hasConstructor = true;
					if (method.parameters().isEmpty() && method.getBody() != null
							&& method.getBody().statements().isEmpty()) {
						return true;
					}
				}
			}
		}
		return !hasConstructor;
	}

	/**
	 * Checks if a variable declaration fragment represents an anonymous class.
	 * 
	 * @param fragment the variable declaration fragment to check
	 * @return true if the fragment's initializer is an anonymous class
	 */
	public boolean isAnonymousClass(VariableDeclarationFragment fragment) {
		Expression initializer = fragment.getInitializer();
		return initializer instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation
				&& ((org.eclipse.jdt.core.dom.ClassInstanceCreation) initializer).getAnonymousClassDeclaration() != null;
	}

	/**
	 * Checks if the given type binding directly extends ExternalResource.
	 * 
	 * @param binding the type binding to check
	 * @return true if the type's superclass is ExternalResource
	 */
	protected boolean isDirectlyExtendingExternalResource(ITypeBinding binding) {
		ITypeBinding superclass = binding.getSuperclass();
		return superclass != null && ORG_JUNIT_RULES_EXTERNAL_RESOURCE.equals(superclass.getQualifiedName());
	}

	/**
	 * Checks if a field is annotated with the specified annotation.
	 * 
	 * @param field the field declaration to check
	 * @param annotationClass the fully qualified annotation class name
	 * @return true if the field has the annotation
	 */
	protected boolean isFieldAnnotatedWith(FieldDeclaration field, String annotationClass) {
		return AnnotationUtils.isFieldAnnotatedWith(field, annotationClass);
	}

	/**
	 * Modifies a class that extends ExternalResource to use JUnit 5 extensions instead.
	 * Delegates to {@link ExternalResourceRefactorer#modifyExternalResourceClass}.
	 * 
	 * @param node the type declaration to modify
	 * @param field the field declaration with ExternalResource
	 * @param fieldStatic whether the field is static (affects callback type)
	 * @param rewriter the AST rewriter
	 * @param ast the AST instance
	 * @param group the text edit group
	 * @param importRewriter the import rewriter
	 */
	protected void modifyExternalResourceClass(TypeDeclaration node, FieldDeclaration field, boolean fieldStatic,
			ASTRewrite rewriter, AST ast, TextEditGroup group, ImportRewrite importRewriter) {
		ExternalResourceRefactorer.modifyExternalResourceClass(node, field, fieldStatic, rewriter, ast, group,
				importRewriter);
	}


	/**
	 * Processes a JUnit migration by applying the necessary AST rewrites.
	 * Implementations should transform the matched pattern into JUnit 5 compatible code.
	 * 
	 * @param group the text edit group for tracking changes
	 * @param rewriter the AST rewriter
	 * @param ast the AST instance
	 * @param importRewriter the import rewriter
	 * @param junitHolder the holder containing JUnit migration information
	 */
	protected abstract void process2Rewrite(TextEditGroup group, ASTRewrite rewriter, AST ast, ImportRewrite importRewriter,
			JunitHolder junitHolder);

	/**
	 * Refactors an anonymous ExternalResource class to implement JUnit 5 callback interfaces.
	 * Delegates to {@link ExternalResourceRefactorer#refactorAnonymousClassToImplementCallbacks}.
	 * 
	 * @param anonymousClass the anonymous class declaration to refactor
	 * @param fieldDeclaration the field containing the anonymous class
	 * @param fieldStatic whether the field is static
	 * @param rewriter the AST rewriter
	 * @param ast the AST instance
	 * @param group the text edit group
	 * @param importRewriter the import rewriter
	 */
	protected void refactorAnonymousClassToImplementCallbacks(AnonymousClassDeclaration anonymousClass,
			FieldDeclaration fieldDeclaration, boolean fieldStatic, ASTRewrite rewriter, AST ast, TextEditGroup group,
			ImportRewrite importRewriter) {
		ExternalResourceRefactorer.refactorAnonymousClassToImplementCallbacks(anonymousClass, fieldDeclaration,
				fieldStatic, rewriter, ast, group, importRewriter);
	}

	/**
	 * Refactors TestName field usage in a class and all its subclasses.
	 * Delegates to {@link TestNameRefactorer#refactorTestnameInClassAndSubclasses}.
	 * 
	 * @param group the text edit group
	 * @param rewriter the AST rewriter
	 * @param ast the AST instance
	 * @param importRewrite the import rewriter
	 * @param node the TestName field declaration to replace
	 */
	protected void refactorTestnameInClassAndSubclasses(TextEditGroup group, ASTRewrite rewriter, AST ast,
			ImportRewrite importRewrite, FieldDeclaration node) {
		TestNameRefactorer.refactorTestnameInClassAndSubclasses(group, rewriter, ast, importRewrite, node);
	}

	/**
	 * Reorders parameters in a method invocation to match JUnit 5 assertion parameter order.
	 * Delegates to {@link AssertionRefactorer#reorderParameters}.
	 * 
	 * @param node the method invocation to reorder
	 * @param rewriter the AST rewriter
	 * @param group the text edit group
	 * @param oneparam assertion methods with one value parameter
	 * @param twoparam assertion methods with two value parameters
	 */
	public void reorderParameters(MethodInvocation node, ASTRewrite rewriter, TextEditGroup group, Set<String> oneparam,
			Set<String> twoparam) {
		AssertionRefactorer.reorderParameters(node, rewriter, group, oneparam, twoparam);
	}

	/**
	 * Standard helper for processing found nodes in the common pattern.
	 * Creates a JunitHolder, stores the node, adds it to the data holder,
	 * and creates a rewrite operation.
	 * 
	 * @param fixcore the cleanup fix core
	 * @param operations the set of operations to add to
	 * @param node the AST node that was found
	 * @param dataHolder the reference holder for storing data
	 * @return true to continue processing other nodes (fluent API semantics)
	 */
	protected boolean addStandardRewriteOperation(JUnitCleanUpFixCore fixcore,
			Set<CompilationUnitRewriteOperationWithSourceRange> operations, ASTNode node,
			ReferenceHolder<Integer, JunitHolder> dataHolder) {
		JunitHolder mh = new JunitHolder();
		mh.minv = node;
		dataHolder.put(dataHolder.size(), mh);
		operations.add(fixcore.rewrite(dataHolder));
		return true;
	}

	/**
	 * Handles import declaration changes for migrating JUnit 4 to JUnit 5.
	 * Delegates to {@link ImportHelper#changeImportDeclaration}.
	 * 
	 * @param node the import declaration to change
	 * @param importRewriter the import rewriter to use
	 * @param sourceClass the JUnit 4 fully qualified class name
	 * @param targetClass the JUnit 5 fully qualified class name
	 */
	protected void changeImportDeclaration(ImportDeclaration node, ImportRewrite importRewriter, String sourceClass,
			String targetClass) {
		ImportHelper.changeImportDeclaration(node, importRewriter, sourceClass, targetClass);
	}

	/**
	 * Applies the JUnit migration rewrite to the compilation unit.
	 * Delegates to the abstract process2Rewrite method for actual transformation.
	 * 
	 * @param upp the JUnit cleanup fix core
	 * @param hit the reference holder containing migration information
	 * @param cuRewrite the compilation unit rewrite
	 * @param group the text edit group
	 */
	public void rewrite(JUnitCleanUpFixCore upp, ReferenceHolder<Integer, JunitHolder> hit,
			CompilationUnitRewrite cuRewrite, TextEditGroup group) {
		ASTRewrite rewriter = cuRewrite.getASTRewrite();
		AST ast = cuRewrite.getRoot().getAST();
		ImportRewrite importRewriter = cuRewrite.getImportRewrite();
		JunitHolder junitHolder = hit.get(hit.size() - 1);
		process2Rewrite(group, rewriter, ast, importRewriter, junitHolder);
		hit.remove(hit.size() - 1);
	}
}