ExceptionCleanupHelper.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.triggerpattern.cleanup;

import java.util.List;

import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.CatchClause;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.TryStatement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.UnionType;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.refactoring.structure.ImportRemover;
import org.eclipse.text.edits.TextEditGroup;

/**
 * Reusable utility for removing checked exceptions from method throws clauses,
 * catch clauses, and simplifying empty try statements after exception removal.
 *
 * <p>This class was extracted from {@code AbstractExplicitEncoding} to be
 * accessible to both the encoding plugin and the trigger-pattern DSL engine.
 * It is generic enough to work with <b>any</b> checked exception, not just
 * {@code UnsupportedEncodingException}.</p>
 *
 * @since 1.3.5
 */
public class ExceptionCleanupHelper {

	private ExceptionCleanupHelper() {
		// utility class – not instantiable
	}

	/**
	 * Removes the specified checked exception from the enclosing method's throws
	 * clause or from catch clauses in a try statement.
	 *
	 * @param visited         the AST node that was modified, must not be null
	 * @param exceptionFQN    fully qualified name of the exception to remove
	 *                        (e.g. {@code "java.io.UnsupportedEncodingException"})
	 * @param exceptionSimple simple name of the exception
	 *                        (e.g. {@code "UnsupportedEncodingException"})
	 * @param group           the text edit group for tracking changes
	 * @param rewrite         the AST rewrite context
	 * @param importRemover   the import remover for tracking removed nodes
	 */
	public static void removeCheckedException(
			ASTNode visited,
			String exceptionFQN,
			String exceptionSimple,
			TextEditGroup group,
			ASTRewrite rewrite,
			ImportRemover importRemover) {

		ASTNode parent = findEnclosingMethodOrTry(visited);
		if (parent == null) {
			return;
		}

		if (parent instanceof MethodDeclaration method) {
			removeExceptionFromMethodThrows(method, exceptionFQN, exceptionSimple, rewrite, group, importRemover);
		} else if (parent instanceof TryStatement tryStatement) {
			int removedCount = removeExceptionFromTryCatch(tryStatement, exceptionFQN, exceptionSimple, rewrite, group, importRemover);
			simplifyEmptyTryStatement(tryStatement, rewrite, group, removedCount);
		}
	}

	// ------------------------------------------------------------------
	// package-private helpers (visible for testing inside the same package)
	// ------------------------------------------------------------------

	static ASTNode findEnclosingMethodOrTry(ASTNode node) {
		if (node == null) {
			return null;
		}
		ASTNode tryStmt = ASTNodes.getFirstAncestorOrNull(node, TryStatement.class);
		ASTNode methodDecl = ASTNodes.getFirstAncestorOrNull(node, MethodDeclaration.class);
		if (tryStmt != null) {
			return tryStmt;
		}
		return methodDecl;
	}

	static boolean isTargetException(Type type, String exceptionSimple) {
		return type.toString().equals(exceptionSimple);
	}

	static void removeExceptionFromMethodThrows(
			MethodDeclaration method,
			String exceptionFQN,
			String exceptionSimple,
			ASTRewrite rewrite,
			TextEditGroup group,
			ImportRemover importRemover) {

		ListRewrite throwsRewrite = rewrite.getListRewrite(method,
				MethodDeclaration.THROWN_EXCEPTION_TYPES_PROPERTY);
		List<Type> thrownExceptions = method.thrownExceptionTypes();
		for (Type exceptionType : thrownExceptions) {
			if (isTargetException(exceptionType, exceptionSimple)) {
				throwsRewrite.remove(exceptionType, group);
				importRemover.registerRemovedNode(exceptionType);
			}
		}
	}

	static boolean removeExceptionFromUnionType(
			UnionType unionType,
			CatchClause catchClause,
			String exceptionSimple,
			ASTRewrite rewrite,
			TextEditGroup group) {

		ListRewrite unionRewrite = rewrite.getListRewrite(unionType, UnionType.TYPES_PROPERTY);
		List<Type> types = unionType.types();

		List<Type> typesToRemove = types.stream()
				.filter(t -> isTargetException(t, exceptionSimple))
				.toList();

		typesToRemove.forEach(type -> unionRewrite.remove(type, group));

		int remainingCount = types.size() - typesToRemove.size();
		if (remainingCount == 1) {
			Type remainingType = types.stream()
					.filter(type -> !typesToRemove.contains(type))
					.findFirst()
					.orElse(null);
			if (remainingType != null) {
				rewrite.replace(unionType, remainingType, group);
			}
		} else if (remainingCount == 0) {
			rewrite.remove(catchClause, group);
			return true;
		}
		return false;
	}

	static int removeExceptionFromTryCatch(
			TryStatement tryStatement,
			String exceptionFQN,
			String exceptionSimple,
			ASTRewrite rewrite,
			TextEditGroup group,
			ImportRemover importRemover) {

		int removedCount = 0;
		List<CatchClause> catchClauses = tryStatement.catchClauses();
		for (CatchClause catchClause : catchClauses) {
			SingleVariableDeclaration exception = catchClause.getException();
			Type exceptionType = exception.getType();

			if (exceptionType instanceof UnionType unionType) {
				if (removeExceptionFromUnionType(unionType, catchClause, exceptionSimple, rewrite, group)) {
					removedCount++;
				}
			} else if (isTargetException(exceptionType, exceptionSimple)) {
				rewrite.remove(catchClause, group);
				importRemover.registerRemovedNode(catchClause);
				removedCount++;
			}
		}
		return removedCount;
	}

	static void simplifyEmptyTryStatement(TryStatement tryStatement, ASTRewrite rewrite, TextEditGroup group, int removedCatchCount) {
		int remainingCatchClauses = tryStatement.catchClauses().size() - removedCatchCount;
		if (remainingCatchClauses > 0 || tryStatement.getFinally() != null) {
			return;
		}

		Block tryBlock = tryStatement.getBody();
		boolean hasResources = !tryStatement.resources().isEmpty();
		boolean hasStatements = !tryBlock.statements().isEmpty();

		if (!hasResources && !hasStatements) {
			rewrite.remove(tryStatement, group);
		} else if (!hasResources && tryStatement.getParent() instanceof Block parentBlock) {
			// Inline statements from try body into the parent block,
			// replacing the try statement with its individual statements
			// to avoid producing an orphaned { ... } block.
			// NOTE: Callers must register child rewrites (e.g., replaceAndRemoveNLS)
			// BEFORE invoking removeUnsupportedEncodingException (which triggers
			// this method). createMoveTarget marks nodes as moved, and
			// replaceAndRemoveNLS fails silently on already-moved nodes.
			ListRewrite parentListRewrite = rewrite.getListRewrite(parentBlock, Block.STATEMENTS_PROPERTY);
			List<?> tryStatements = tryBlock.statements();
			for (int i = tryStatements.size() - 1; i >= 0; i--) {
				ASTNode stmt = (ASTNode) tryStatements.get(i);
				ASTNode moved = rewrite.createMoveTarget(stmt);
				parentListRewrite.insertAfter(moved, tryStatement, group);
			}
			rewrite.remove(tryStatement, group);
		}
	}
}