ASTNavigationUtils.java

/*******************************************************************************
 * Copyright (c) 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.util;

import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;

/**
 * Utility class for navigating and finding nodes in the AST tree.
 * Provides common operations for locating type declarations, compilation units,
 * and other AST elements needed during JUnit migration.
 */
public final class ASTNavigationUtils {

	private ASTNavigationUtils() {
		// Utility class - prevent instantiation
	}

	/**
	 * Finds the CompilationUnit that contains the given AST node.
	 * 
	 * @param node the AST node to start from
	 * @return the containing CompilationUnit, or null if not found
	 */
	public static CompilationUnit findCompilationUnit(ASTNode node) {
		return ASTNodes.getTypedAncestor(node, CompilationUnit.class);
	}

	/**
	 * Gets the parent TypeDeclaration for the given AST node.
	 * 
	 * @param node the AST node to start from
	 * @return the enclosing TypeDeclaration, or null if none found
	 */
	public static TypeDeclaration getParentTypeDeclaration(ASTNode node) {
		return ASTNodes.getTypedAncestor(node, TypeDeclaration.class);
	}

	/**
	 * Finds the enclosing TypeDeclaration for the given AST node.
	 * This is an alias for {@link #getParentTypeDeclaration(ASTNode)}.
	 * 
	 * @param node the AST node to start from
	 * @return the enclosing TypeDeclaration, or null if not found
	 */
	public static TypeDeclaration findEnclosingTypeDeclaration(ASTNode node) {
		return getParentTypeDeclaration(node);
	}

	/**
	 * Finds a type declaration by its fully qualified name within a Java project.
	 * 
	 * @param javaProject the Java project to search in
	 * @param fullyQualifiedTypeName the fully qualified type name
	 * @return the TypeDeclaration if found, or null otherwise
	 * @throws RuntimeException if there is an error accessing the Java model
	 */
	public static TypeDeclaration findTypeDeclaration(IJavaProject javaProject, String fullyQualifiedTypeName) {
		try {
			IType type = javaProject.findType(fullyQualifiedTypeName);
			if (type != null && type.exists()) {
				CompilationUnit unit = parseCompilationUnit(type.getCompilationUnit());
				return findTypeDeclarationInCompilationUnit(unit, fullyQualifiedTypeName);
			}
		} catch (JavaModelException e) {
			throw new RuntimeException("Failed to find type declaration for: " + fullyQualifiedTypeName, e); //$NON-NLS-1$
		}
		return null;
	}

	/**
	 * Finds a type declaration within a compilation unit by name.
	 * 
	 * @param unit the compilation unit to search
	 * @param fullyQualifiedTypeName the fully qualified type name
	 * @return the TypeDeclaration if found, or null otherwise
	 */
	public static TypeDeclaration findTypeDeclarationInCompilationUnit(CompilationUnit unit, String fullyQualifiedTypeName) {
		for (Object obj : unit.types()) {
			if (obj instanceof TypeDeclaration) {
				TypeDeclaration typeDecl = (TypeDeclaration) obj;
				TypeDeclaration result = findTypeDeclarationInType(typeDecl, fullyQualifiedTypeName);
				if (result != null) {
					return result;
				}
			}
		}
		return null;
	}

	/**
	 * Finds a type declaration by type binding within a compilation unit.
	 * 
	 * @param typeBinding the type binding to match
	 * @param cu the compilation unit to search
	 * @return the TypeDeclaration if found, or null otherwise
	 */
	public static TypeDeclaration findTypeDeclarationInCompilationUnit(ITypeBinding typeBinding, CompilationUnit cu) {
		final TypeDeclaration[] result = { null };

		cu.accept(new ASTVisitor() {
			private boolean checkAndMatchBinding(AbstractTypeDeclaration node, ITypeBinding typeBinding) {
				ITypeBinding binding = node.resolveBinding();
				if (binding != null && ASTNodes.areBindingsEqual(binding, typeBinding)) {
					result[0] = (TypeDeclaration) node;
					return false;
				}
				return true;
			}

			@Override
			public boolean visit(AnnotationTypeDeclaration node) {
				return checkAndMatchBinding(node, typeBinding);
			}

			@Override
			public boolean visit(EnumDeclaration node) {
				return checkAndMatchBinding(node, typeBinding);
			}

			@Override
			public boolean visit(TypeDeclaration node) {
				return checkAndMatchBinding(node, typeBinding);
			}
		});

		return result[0];
	}

	/**
	 * Finds a type declaration within the project for the given type binding.
	 * 
	 * @param typeBinding the type binding to find
	 * @return the TypeDeclaration if found, or null otherwise
	 */
	public static TypeDeclaration findTypeDeclarationInProject(ITypeBinding typeBinding) {
		IType type = (IType) typeBinding.getJavaElement();
		return type != null ? findTypeDeclaration(type.getJavaProject(), type.getFullyQualifiedName()) : null;
	}

	/**
	 * Recursively searches for a type declaration within a type hierarchy.
	 * 
	 * @param typeDecl the type declaration to start from
	 * @param qualifiedTypeName the qualified type name to find
	 * @return the TypeDeclaration if found, or null otherwise
	 */
	public static TypeDeclaration findTypeDeclarationInType(TypeDeclaration typeDecl, String qualifiedTypeName) {
		if (getQualifiedName(typeDecl).equals(qualifiedTypeName)) {
			return typeDecl;
		}

		for (TypeDeclaration nestedType : typeDecl.getTypes()) {
			TypeDeclaration result = findTypeDeclarationInType(nestedType, qualifiedTypeName);
			if (result != null) {
				return result;
			}
		}
		return null;
	}

	/**
	 * Finds the type declaration for a given type binding, searching first in the
	 * compilation unit and then in the project.
	 * 
	 * @param typeBinding the type binding to find
	 * @param cu the compilation unit to search first
	 * @return the type declaration if found, or null otherwise
	 */
	public static ASTNode findTypeDeclarationForBinding(ITypeBinding typeBinding, CompilationUnit cu) {
		if (typeBinding == null)
			return null;

		TypeDeclaration typeDecl = findTypeDeclarationInCompilationUnit(typeBinding, cu);
		return typeDecl != null ? typeDecl : findTypeDeclarationInProject(typeBinding);
	}

	/**
	 * Determines the fully qualified name of a TypeDeclaration.
	 * 
	 * @param typeDecl the type declaration
	 * @return the fully qualified name including package and nested class separators
	 */
	public static String getQualifiedName(TypeDeclaration typeDecl) {
		StringBuilder qualifiedName = new StringBuilder(typeDecl.getName().getIdentifier());
		ASTNode parent = typeDecl.getParent();

		// Process nested classes
		while (parent instanceof TypeDeclaration) {
			TypeDeclaration parentType = (TypeDeclaration) parent;
			qualifiedName.insert(0, parentType.getName().getIdentifier() + "$"); // $ for nested classes //$NON-NLS-1$
			parent = parent.getParent();
		}

		// Add package name
		CompilationUnit compilationUnit = ASTNodes.getTypedAncestor(typeDecl, CompilationUnit.class);
		if (compilationUnit != null && compilationUnit.getPackage() != null) {
			String packageName = compilationUnit.getPackage().getName().getFullyQualifiedName();
			qualifiedName.insert(0, packageName + "."); //$NON-NLS-1$
		}

		return qualifiedName.toString();
	}

	/**
	 * Parses a compilation unit from an ICompilationUnit.
	 * 
	 * @param iCompilationUnit the compilation unit to parse
	 * @return the parsed CompilationUnit
	 */
	public static CompilationUnit parseCompilationUnit(org.eclipse.jdt.core.ICompilationUnit iCompilationUnit) {
		ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
		parser.setKind(ASTParser.K_COMPILATION_UNIT);
		parser.setSource(iCompilationUnit);
		parser.setResolveBindings(true);
		return (CompilationUnit) parser.createAST(null);
	}
}