ExternalResourceJUnitPlugin.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.CompilationUnit;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
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 ExternalResource classes to JUnit 5 lifecycle callbacks.
 * <p>
 * Transforms classes that extend {@code org.junit.rules.ExternalResource} to implement
 * JUnit 5 callback interfaces ({@code BeforeEachCallback}, {@code AfterEachCallback}).
 * Renames lifecycle methods (before/after) to match JUnit 5 naming conventions.
 * </p>
 */
public class ExternalResourceJUnitPlugin 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<>();
		HelperVisitor.callTypeDeclarationVisitor(ORG_JUNIT_RULES_EXTERNAL_RESOURCE, compilationUnit, dataHolder,
				nodesprocessed, (visited, aholder) -> processFoundNode(fixcore, operations, visited, aholder,nodesprocessed));
	}

	/**
	 * Processes a type declaration that extends ExternalResource.
	 * Only processes types that either:
	 * - Directly extend ExternalResource, or
	 * - Indirectly extend ExternalResource AND have before()/after() lifecycle methods
	 * 
	 * @param fixcore the cleanup fix core
	 * @param operations the set of operations to add to
	 * @param node the type declaration found
	 * @param dataHolder the reference holder for data
	 * @param nodesprocessed set of already processed nodes
	 * @return false to continue visiting
	 */
	private boolean processFoundNode(JUnitCleanUpFixCore fixcore,
			Set<CompilationUnitRewriteOperationWithSourceRange> operations, TypeDeclaration node,
			ReferenceHolder<Integer, JunitHolder> dataHolder, Set<ASTNode> nodesprocessed) {
		if (nodesprocessed.contains(node)) {
			return false;
		}
		
		// For indirect subclasses, only process if they have lifecycle methods
		if (!isDirectlyExtendingExternalResource(node.resolveBinding())) {
			boolean hasLifecycleMethod = false;
			for (MethodDeclaration method : node.getMethods()) {
				String methodName = method.getName().getIdentifier();
				if (METHOD_BEFORE.equals(methodName) || METHOD_AFTER.equals(methodName)) {
					hasLifecycleMethod = true;
					break;
				}
			}
			if (!hasLifecycleMethod) {
				return false;
			}
		}
		
		nodesprocessed.add(node);
		JunitHolder mh= new JunitHolder();
		mh.minv= node;
		dataHolder.put(dataHolder.size(), mh);
		operations.add(fixcore.rewrite(dataHolder));
		return false;
	}

	@Override
	protected
	void process2Rewrite(TextEditGroup group, ASTRewrite rewriter, AST ast, ImportRewrite importRewriter,
			JunitHolder junitHolder) {
		TypeDeclaration node= junitHolder.getTypeDeclaration();
		modifyExternalResourceClass(node, null, false, rewriter, ast, group, importRewriter);
	}
	
	@Override
	public String getPreview(boolean afterRefactoring) {
		if (afterRefactoring) {
			return """
					class MyExternalResource implements BeforeEachCallback, AfterEachCallback {
						@Override
						public void beforeEach(ExtensionContext context) throws Exception {
						}

						@Override
						public void afterEach(ExtensionContext context) {
						}
					}
					"""; //$NON-NLS-1$
		}
		return """
				class MyExternalResource extends ExternalResource {
					@Override
					protected void before() throws Throwable {
					}

					@Override
					protected void after() {
					}
				}
				"""; //$NON-NLS-1$
	}

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