JUnitMultiFileRewriteOperation.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
 *******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.multifile;

import static org.sandbox.jdt.internal.corext.fix.helper.lib.JUnitConstants.ANNOTATION_REGISTER_EXTENSION;
import static org.sandbox.jdt.internal.corext.fix.helper.lib.JUnitConstants.ORG_JUNIT_CLASS_RULE;
import static org.sandbox.jdt.internal.corext.fix.helper.lib.JUnitConstants.ORG_JUNIT_JUPITER_API_EXTENSION_REGISTER_EXTENSION;
import static org.sandbox.jdt.internal.corext.fix.helper.lib.JUnitConstants.ORG_JUNIT_RULE;

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.runtime.CoreException;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
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.core.dom.rewrite.ListRewrite;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperationWithSourceRange;
import org.eclipse.jdt.internal.corext.fix.LinkedProposalModelCore;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;

import org.eclipse.text.edits.TextEditGroup;

import org.sandbox.jdt.internal.corext.fix.helper.lib.ExternalResourceRefactorer;

/** Applies the local part of a coordinated JUnit migration plan. */
final class JUnitMultiFileRewriteOperation extends CompilationUnitRewriteOperationWithSourceRange {

	record FieldEdit(String bindingKey, boolean classRule, Annotation ruleAnnotation) {
	}

	record ResourceTypeEdit(String bindingKey, boolean classRule) {
	}

	record ResolvedEdits(CompilationUnit root, Map<FieldDeclaration, FieldEdit> fields,
			Map<TypeDeclaration, ResourceTypeEdit> resourceTypes, Set<String> fieldKeys, Set<String> typeKeys) {

		static Builder builder(CompilationUnit root) {
			return new Builder(root);
		}

		static final class Builder {
			private final CompilationUnit root;
			private final Map<FieldDeclaration, FieldEdit> fields= new LinkedHashMap<>();
			private final Map<TypeDeclaration, ResourceTypeEdit> resourceTypes= new LinkedHashMap<>();
			private final Set<String> fieldKeys= new LinkedHashSet<>();
			private final Set<String> typeKeys= new LinkedHashSet<>();

			Builder(CompilationUnit root) {
				this.root= root;
			}

			void addField(FieldDeclaration field, String bindingKey, boolean classRule, Annotation ruleAnnotation) {
				if (ruleAnnotation != null) {
					fields.put(field, new FieldEdit(bindingKey, classRule, ruleAnnotation));
					fieldKeys.add(bindingKey);
				}
			}

			void addResourceType(TypeDeclaration type, String bindingKey, boolean classRule) {
				resourceTypes.put(type, new ResourceTypeEdit(bindingKey, classRule));
				typeKeys.add(bindingKey);
			}

			ResolvedEdits build() {
				return new ResolvedEdits(root, Map.copyOf(fields), Map.copyOf(resourceTypes), Set.copyOf(fieldKeys),
						Set.copyOf(typeKeys));
			}
		}
	}

	private final ResolvedEdits edits;

	JUnitMultiFileRewriteOperation(ResolvedEdits edits) {
		this.edits= edits;
	}

	@Override
	public void rewriteASTInternal(CompilationUnitRewrite cuRewrite, LinkedProposalModelCore linkedModel)
			throws CoreException {
		TextEditGroup group= createTextEditGroup("Migrate named ExternalResource and its rule fields", cuRewrite); //$NON-NLS-1$
		ASTRewrite rewrite= cuRewrite.getASTRewrite();
		AST ast= cuRewrite.getRoot().getAST();
		ImportRewrite imports= cuRewrite.getImportRewrite();

		for (Map.Entry<FieldDeclaration, FieldEdit> entry : edits.fields().entrySet()) {
			rewriteRuleField(entry.getKey(), entry.getValue(), rewrite, ast, imports, group);
		}
		removeUnusedRuleImports(imports);

		for (Map.Entry<TypeDeclaration, ResourceTypeEdit> entry : edits.resourceTypes().entrySet()) {
			ExternalResourceRefactorer.modifyExternalResourceClass(entry.getKey(), null, entry.getValue().classRule(),
					rewrite, ast, group, imports);
		}
	}

	private void rewriteRuleField(FieldDeclaration field, FieldEdit edit, ASTRewrite rewrite, AST ast,
			ImportRewrite imports, TextEditGroup group) {
		rewrite.remove(edit.ruleAnnotation(), group);
		if (!hasRegisterExtension(field)) {
			String annotationName= imports.addImport(ORG_JUNIT_JUPITER_API_EXTENSION_REGISTER_EXTENSION);
			MarkerAnnotation annotation= ast.newMarkerAnnotation();
			annotation.setTypeName(ast.newName(annotationName));
			ListRewrite modifiers= rewrite.getListRewrite(field, FieldDeclaration.MODIFIERS2_PROPERTY);
			modifiers.insertFirst(annotation, group);
		}
	}

	private boolean hasRegisterExtension(FieldDeclaration field) {
		for (Object modifier : field.modifiers()) {
			if (modifier instanceof Annotation annotation) {
				ITypeBinding binding= annotation.resolveTypeBinding();
				if (binding != null
						&& ORG_JUNIT_JUPITER_API_EXTENSION_REGISTER_EXTENSION.equals(binding.getQualifiedName())) {
					return true;
				}
				if (binding == null && ANNOTATION_REGISTER_EXTENSION.equals(annotation.getTypeName().getFullyQualifiedName())) {
					return true;
				}
			}
		}
		return false;
	}

	private void removeUnusedRuleImports(ImportRewrite imports) {
		Set<Annotation> planned= edits.fields().values().stream().map(FieldEdit::ruleAnnotation)
				.collect(java.util.stream.Collectors.toSet());
		boolean[] remainingRule= { false };
		boolean[] remainingClassRule= { false };
		edits.root().accept(new ASTVisitor() {
			@Override
			public boolean visit(MarkerAnnotation node) {
				inspect(node);
				return true;
			}

			@Override
			public boolean visit(org.eclipse.jdt.core.dom.SingleMemberAnnotation node) {
				inspect(node);
				return true;
			}

			@Override
			public boolean visit(org.eclipse.jdt.core.dom.NormalAnnotation node) {
				inspect(node);
				return true;
			}

			private void inspect(Annotation annotation) {
				if (planned.contains(annotation)) {
					return;
				}
				ITypeBinding binding= annotation.resolveTypeBinding();
				if (binding == null) {
					return;
				}
				if (ORG_JUNIT_RULE.equals(binding.getQualifiedName())) {
					remainingRule[0]= true;
				} else if (ORG_JUNIT_CLASS_RULE.equals(binding.getQualifiedName())) {
					remainingClassRule[0]= true;
				}
			}
		});
		if (!remainingRule[0]) {
			imports.removeImport(ORG_JUNIT_RULE);
		}
		if (!remainingClassRule[0]) {
			imports.removeImport(ORG_JUNIT_CLASS_RULE);
		}
	}
}