RunWithJUnitPlugin.java

/*******************************************************************************
 * Copyright (c) 2021 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 static org.sandbox.jdt.internal.corext.fix.helper.lib.JUnitConstants.*;

/*-
 * #%L
 * Sandbox junit cleanup
 * %%
 * Copyright (C) 2024 hammer
 * %%
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 * 
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the Eclipse
 * Public License, v. 2.0 are satisfied: GNU General Public License, version 2
 * with the GNU Classpath Exception which is
 * available at https://www.gnu.org/software/classpath/license.html.
 * 
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 * #L%
 */

import java.util.Set;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeLiteral;
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.HelperVisitor;
import org.sandbox.jdt.internal.common.ReferenceHolder;
import org.sandbox.jdt.internal.corext.fix.JUnitCleanUpFixCore;
import org.sandbox.jdt.internal.corext.fix.helper.lib.AbstractTool;
import org.sandbox.jdt.internal.corext.fix.helper.lib.JunitHolder;

/**
 * Plugin to migrate JUnit 4 @RunWith and @Suite.SuiteClasses to JUnit 5 equivalents.
 */
public class RunWithJUnitPlugin extends AbstractTool<ReferenceHolder<Integer, JunitHolder>> {

	@Override
	public void find(JUnitCleanUpFixCore fixcore, CompilationUnit compilationUnit,
			Set<CompilationUnitRewriteOperationWithSourceRange> operations, Set<ASTNode> nodesprocessed) {
		ReferenceHolder<Integer, JunitHolder> dataHolder= new ReferenceHolder<>();
		
		// Find @RunWith annotations
		HelperVisitor.forAnnotation(ORG_JUNIT_RUNWITH)
			.in(compilationUnit)
			.excluding(nodesprocessed)
			.processEach(dataHolder, (visited, aholder) -> {
				if (visited instanceof SingleMemberAnnotation) {
					return processFoundNodeRunWith(fixcore, operations, (Annotation) visited, aholder);
				}
				return true;
			});
		
		// Find @Suite.SuiteClasses annotations
		HelperVisitor.forAnnotation(ORG_JUNIT_SUITE_SUITECLASSES)
			.in(compilationUnit)
			.excluding(nodesprocessed)
			.processEach(dataHolder, (visited, aholder) -> {
				if (visited instanceof SingleMemberAnnotation) {
					return processFoundNodeSuite(fixcore, operations, (Annotation) visited, aholder);
				}
				return true;
			});
	}

	private boolean processFoundNodeRunWith(JUnitCleanUpFixCore fixcore,
			Set<CompilationUnitRewriteOperationWithSourceRange> operations, Annotation node,
			ReferenceHolder<Integer, JunitHolder> dataHolder) {
		JunitHolder mh= new JunitHolder();
		mh.minv= node;
		mh.minvname= node.getTypeName().getFullyQualifiedName();
		if (node instanceof SingleMemberAnnotation mynode) {
			Expression value= mynode.getValue();
			if (value instanceof TypeLiteral myvalue) {
				ITypeBinding classBinding= myvalue.resolveTypeBinding();
				String runnerQualifiedName = null;
				
				// Try to get qualified name from binding
				// For TypeLiteral (e.g., Suite.class), we need to get the type being referenced, not Class itself
				if (classBinding != null) {
					// Get the type from the TypeLiteral's type, not from the Class binding
					Type type = myvalue.getType();
					if (type != null) {
						ITypeBinding typeBinding = type.resolveBinding();
						if (typeBinding != null) {
							runnerQualifiedName = typeBinding.getQualifiedName();
						}
					}
				}
				
				// If binding resolution failed, try to get fully qualified name from the AST
				if (runnerQualifiedName == null || runnerQualifiedName.isEmpty()) {
					Type runnerType = myvalue.getType();
					if (runnerType != null) {
						String typeName = runnerType.toString();
						// Only use it if it's a fully qualified name (contains a dot)
						if (typeName != null && typeName.contains(".")) {
							runnerQualifiedName = typeName;
						}
						// Special case: Suite is a JUnit library class, so we can safely migrate it
						// even with just the simple name (unlike third-party frameworks)
						else if ("Suite".equals(typeName)) {
							runnerQualifiedName = ORG_JUNIT_SUITE;
						}
						// For other simple names, we can't safely migrate to avoid false positives
					}
				}
				
				// Handle Suite runner
				if (ORG_JUNIT_SUITE.equals(runnerQualifiedName)) {
					mh.value= ORG_JUNIT_RUNWITH;
					dataHolder.put(dataHolder.size(), mh);
					operations.add(fixcore.rewrite(dataHolder));
					return true; // Continue processing other annotations
				}
				
				// Handle Mockito runners - only check qualified names to avoid false positives
				if (ORG_MOCKITO_JUNIT_MOCKITO_JUNIT_RUNNER.equals(runnerQualifiedName) ||
						ORG_MOCKITO_RUNNERS_MOCKITO_JUNIT_RUNNER.equals(runnerQualifiedName)) {
					mh.value= ORG_MOCKITO_JUNIT_MOCKITO_JUNIT_RUNNER;
					dataHolder.put(dataHolder.size(), mh);
					operations.add(fixcore.rewrite(dataHolder));
					return true; // Continue processing other annotations
				}
				
				// Handle Spring runners - only check qualified names to avoid false positives
				if (ORG_SPRINGFRAMEWORK_TEST_CONTEXT_JUNIT4_SPRING_RUNNER.equals(runnerQualifiedName) ||
						ORG_SPRINGFRAMEWORK_TEST_CONTEXT_JUNIT4_SPRING_JUNIT4_CLASS_RUNNER.equals(runnerQualifiedName)) {
					mh.value= ORG_SPRINGFRAMEWORK_TEST_CONTEXT_JUNIT4_SPRING_RUNNER;
					dataHolder.put(dataHolder.size(), mh);
					operations.add(fixcore.rewrite(dataHolder));
					return true; // Continue processing other annotations
				}
			}
		}
		// Return true to continue processing other annotations
		return true;
	}

	private boolean processFoundNodeSuite(JUnitCleanUpFixCore fixcore,
			Set<CompilationUnitRewriteOperationWithSourceRange> operations, Annotation node,
			ReferenceHolder<Integer, JunitHolder> dataHolder) {
		JunitHolder mh= new JunitHolder();
		mh.minv= node;
		mh.minvname= node.getTypeName().getFullyQualifiedName();
		mh.value= ORG_JUNIT_SUITE_SUITECLASSES;
		dataHolder.put(dataHolder.size(), mh);
		operations.add(fixcore.rewrite(dataHolder));
		// Return true to continue processing other annotations
		return true;
	}

	@Override
	protected
	void process2Rewrite(TextEditGroup group, ASTRewrite rewriter, AST ast, ImportRewrite importRewriter,
			JunitHolder junitHolder) {
		Annotation minv= junitHolder.getAnnotation();
		Annotation newAnnotation= null;
		
		if (ORG_JUNIT_SUITE_SUITECLASSES.equals(junitHolder.value)) {
			// Handle @Suite.SuiteClasses migration
			SingleMemberAnnotation mynode= (SingleMemberAnnotation) minv;
			newAnnotation= ast.newSingleMemberAnnotation();
			((SingleMemberAnnotation) newAnnotation)
					.setValue(ASTNodes.createMoveTarget(rewriter, mynode.getValue()));
			newAnnotation.setTypeName(ast.newSimpleName(ANNOTATION_SELECT_CLASSES));
			importRewriter.addImport(ORG_JUNIT_PLATFORM_SUITE_API_SELECT_CLASSES);
			importRewriter.removeImport(ORG_JUNIT_SUITE_SUITECLASSES);
			importRewriter.removeImport(ORG_JUNIT_SUITE);
		} else if (ORG_JUNIT_RUNWITH.equals(junitHolder.value)) {
			// Handle @RunWith(Suite.class) migration
			newAnnotation= ast.newMarkerAnnotation();
			newAnnotation.setTypeName(ast.newSimpleName(ANNOTATION_SUITE));
			// Add new import FIRST, then remove old ones
			importRewriter.addImport(ORG_JUNIT_JUPITER_SUITE);
			importRewriter.removeImport(ORG_JUNIT_SUITE);
			importRewriter.removeImport(ORG_JUNIT_RUNWITH);
		} else if (ORG_MOCKITO_JUNIT_MOCKITO_JUNIT_RUNNER.equals(junitHolder.value)) {
			// Handle @RunWith(MockitoJUnitRunner.class) migration
			SingleMemberAnnotation extendWithAnnotation= ast.newSingleMemberAnnotation();
			extendWithAnnotation.setTypeName(ast.newSimpleName(ANNOTATION_EXTEND_WITH));
			TypeLiteral typeLiteral= ast.newTypeLiteral();
			typeLiteral.setType(ast.newSimpleType(ast.newName(MOCKITO_EXTENSION)));
			extendWithAnnotation.setValue(typeLiteral);
			newAnnotation= extendWithAnnotation;
			importRewriter.addImport(ORG_JUNIT_JUPITER_API_EXTENSION_EXTEND_WITH);
			importRewriter.addImport(ORG_MOCKITO_JUNIT_JUPITER_MOCKITO_EXTENSION);
			importRewriter.removeImport(ORG_MOCKITO_JUNIT_MOCKITO_JUNIT_RUNNER);
			importRewriter.removeImport(ORG_MOCKITO_RUNNERS_MOCKITO_JUNIT_RUNNER);
		} else if (ORG_SPRINGFRAMEWORK_TEST_CONTEXT_JUNIT4_SPRING_RUNNER.equals(junitHolder.value)) {
			// Handle @RunWith(SpringRunner.class) migration
			SingleMemberAnnotation extendWithAnnotation= ast.newSingleMemberAnnotation();
			extendWithAnnotation.setTypeName(ast.newSimpleName(ANNOTATION_EXTEND_WITH));
			TypeLiteral typeLiteral= ast.newTypeLiteral();
			typeLiteral.setType(ast.newSimpleType(ast.newName(SPRING_EXTENSION)));
			extendWithAnnotation.setValue(typeLiteral);
			newAnnotation= extendWithAnnotation;
			importRewriter.addImport(ORG_JUNIT_JUPITER_API_EXTENSION_EXTEND_WITH);
			importRewriter.addImport(ORG_SPRINGFRAMEWORK_TEST_CONTEXT_JUNIT_JUPITER_SPRING_EXTENSION);
			importRewriter.removeImport(ORG_SPRINGFRAMEWORK_TEST_CONTEXT_JUNIT4_SPRING_RUNNER);
			importRewriter.removeImport(ORG_SPRINGFRAMEWORK_TEST_CONTEXT_JUNIT4_SPRING_JUNIT4_CLASS_RUNNER);
		}
		
		if (newAnnotation != null) {
			ASTNodes.replaceButKeepComment(rewriter, minv, newAnnotation, group);
			importRewriter.removeImport(ORG_JUNIT_RUNWITH);
		}
	}
	
	@Override
	public String getPreview(boolean afterRefactoring) {
		if (afterRefactoring) {
			return """
					@Suite
					@SelectClasses({
						MyTest.class
					})
					"""; //$NON-NLS-1$
		}
		return """
				@RunWith(Suite.class)
				@Suite.SuiteClasses({
					MyTest.class
				})
				"""; //$NON-NLS-1$
	}

	@Override
	public String toString() {
		return "RunWith"; //$NON-NLS-1$
	}
}