VariableResolver.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.dom.ASTNode;
import org.eclipse.jdt.core.dom.ArrayType;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.Initializer;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;

/**
 * Utility class for resolving variable types and declarations in AST nodes.
 * 
 * <p>
 * This class provides centralized variable resolution functionality for
 * sandbox cleanup plugins. It walks the AST tree to find variable declarations
 * and extract type information, which is critical for generating type-aware
 * code transformations.
 * </p>
 * 
 * <p><b>Key Functionality:</b></p>
 * <ul>
 * <li>Variable type resolution across scopes</li>
 * <li>Type binding extraction</li>
 * <li>Annotation checking (@NotNull, @NonNull)</li>
 * <li>Support for primitive and reference types</li>
 * </ul>
 * 
 * @see TypeCheckingUtils
 * @see AnnotationUtils
 */
public final class VariableResolver {

	/**
	 * Private constructor to prevent instantiation of this utility class.
	 */
	private VariableResolver() {
		// Utility class - no instances allowed
	}

	/**
	 * Attempts to determine the type name of a variable by searching for its
	 * declaration in the AST tree.
	 * 
	 * <p>
	 * This method walks up the AST tree starting from the given node, searching
	 * through all parent scopes (blocks, methods, initializers, lambdas) to find
	 * the variable declaration. It returns the simple type name (e.g., "int",
	 * "String", "double") if found.
	 * </p>
	 * 
	 * <p><b>Supported Scopes:</b></p>
	 * <ul>
	 * <li>Block statements</li>
	 * <li>Method declarations</li>
	 * <li>Initializer blocks (instance or static)</li>
	 * <li>Lambda expressions</li>
	 * </ul>
	 * 
	 * <p><b>Examples:</b></p>
	 * <pre>{@code
	 * // Finds variable in method scope
	 * public void method() {
	 *     int count = 0;  // getVariableType("count") → "int"
	 *     for (Item item : items) {
	 *         count++;
	 *     }
	 * }
	 * 
	 * // Finds variable in block scope
	 * {
	 *     double sum = 0.0;  // getVariableType("sum") → "double"
	 *     for (Number n : numbers) {
	 *         sum += n.doubleValue();
	 *     }
	 * }
	 * }</pre>
	 * 
	 * @param startNode the starting node for the search (typically a loop node)
	 *                  (must not be null)
	 * @param varName   the variable name to look up (must not be null)
	 * @return the simple type name (e.g., "double", "int", "String") or null if not
	 *         found
	 * @throws IllegalArgumentException if startNode or varName is null
	 */
	public static String getVariableType(ASTNode startNode, String varName) {
		if (startNode == null) {
			throw new IllegalArgumentException("startNode cannot be null"); //$NON-NLS-1$
		}
		if (varName == null) {
			throw new IllegalArgumentException("varName cannot be null"); //$NON-NLS-1$
		}

		// Walk up the AST tree searching for the variable in each scope
		ASTNode currentNode = startNode.getParent();

		while (currentNode != null) {
			// Search in blocks
			if (currentNode instanceof Block) {
				Block block = (Block) currentNode;
				String type = searchBlockForVariableType(block, varName);
				if (type != null) {
					return type;
				}
			}
			// Search in method bodies
			else if (currentNode instanceof MethodDeclaration) {
				MethodDeclaration method = (MethodDeclaration) currentNode;
				if (method.getBody() != null) {
					String type = searchBlockForVariableType(method.getBody(), varName);
					if (type != null) {
						return type;
					}
				}
			}
			// Search in initializer blocks (instance or static)
			else if (currentNode instanceof Initializer) {
				Initializer initializer = (Initializer) currentNode;
				if (initializer.getBody() != null) {
					String type = searchBlockForVariableType(initializer.getBody(), varName);
					if (type != null) {
						return type;
					}
				}
			}
			// Search in lambda expressions
			else if (currentNode instanceof LambdaExpression) {
				LambdaExpression lambda = (LambdaExpression) currentNode;
				if (lambda.getBody() instanceof Block) {
					String type = searchBlockForVariableType((Block) lambda.getBody(), varName);
					if (type != null) {
						return type;
					}
				}
			}

			// Move up to parent scope
			currentNode = currentNode.getParent();
		}

		return null;
	}

	/**
	 * Searches a block for a variable declaration and returns its type.
	 * 
	 * <p>
	 * This method iterates through all statements in the block looking for
	 * {@link VariableDeclarationStatement}s that declare the specified variable. It
	 * handles primitive types, simple types, and array types, using bindings when
	 * available for accurate type resolution.
	 * </p>
	 * 
	 * <p><b>Type Resolution Strategy:</b></p>
	 * <ol>
	 * <li>Primitive types: Returns the primitive type code (e.g., "int", "double")</li>
	 * <li>Simple types with bindings: Returns the binding's simple name</li>
	 * <li>Simple types without bindings: Returns the fully qualified name</li>
	 * <li>Array types: Recursively resolves element type and appends "[]"</li>
	 * <li>Other types: Returns the string representation of the type</li>
	 * </ol>
	 * 
	 * @param block   the block to search (may be null)
	 * @param varName the variable name to find (must not be null)
	 * @return the simple type name or null if not found
	 * @throws IllegalArgumentException if varName is null
	 */
	public static String searchBlockForVariableType(Block block, String varName) {
		if (varName == null) {
			throw new IllegalArgumentException("varName cannot be null"); //$NON-NLS-1$
		}

		if (block == null) {
			return null;
		}

		for (Object stmtObj : block.statements()) {
			if (stmtObj instanceof VariableDeclarationStatement) {
				VariableDeclarationStatement varDecl = (VariableDeclarationStatement) stmtObj;
				for (Object fragObj : varDecl.fragments()) {
					if (fragObj instanceof VariableDeclarationFragment) {
						VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
						if (frag.getName().getIdentifier().equals(varName)) {
							return extractTypeName(varDecl.getType());
						}
					}
				}
			}
		}
		return null;
	}

	/**
	 * Extracts the type name from a Type node.
	 * 
	 * @param type the type to extract the name from
	 * @return the type name, or null if unable to determine
	 */
	private static String extractTypeName(Type type) {
		if (type == null) {
			return null;
		}

		if (type.isPrimitiveType()) {
			return ((PrimitiveType) type).getPrimitiveTypeCode().toString();
		} else if (type.isSimpleType()) {
			SimpleType simpleType = (SimpleType) type;
			ITypeBinding binding = simpleType.resolveBinding();
			if (binding != null) {
				return binding.getName();
			} else {
				return simpleType.getName().getFullyQualifiedName();
			}
		} else if (type.isArrayType()) {
			ArrayType arrayType = (ArrayType) type;
			Type elementType = arrayType.getElementType();
			String elementTypeName = extractTypeName(elementType);
			return elementTypeName != null ? elementTypeName + "[]" : null; //$NON-NLS-1$
		} else {
			return type.toString();
		}
	}

	/**
	 * Gets the type binding for a variable name.
	 * 
	 * <p>
	 * This method finds the variable declaration in the AST tree and returns its
	 * {@link ITypeBinding}. Type bindings provide access to full type information
	 * including qualified names, which is useful for distinguishing between
	 * different types with the same simple name.
	 * </p>
	 * 
	 * <p><b>Use Cases:</b></p>
	 * <ul>
	 * <li>Distinguishing {@code String} concatenation from numeric addition</li>
	 * <li>Checking if a type is a specific class (e.g., {@code java.lang.String})</li>
	 * <li>Accessing type hierarchy information</li>
	 * </ul>
	 * 
	 * @param startNode the starting node for the search (must not be null)
	 * @param varName   the variable name (must not be null)
	 * @return the type binding, or null if not found
	 * @throws IllegalArgumentException if startNode or varName is null
	 */
	public static ITypeBinding getTypeBinding(ASTNode startNode, String varName) {
		if (startNode == null) {
			throw new IllegalArgumentException("startNode cannot be null"); //$NON-NLS-1$
		}
		if (varName == null) {
			throw new IllegalArgumentException("varName cannot be null"); //$NON-NLS-1$
		}

		VariableDeclarationFragment frag = findVariableDeclaration(startNode, varName);
		if (frag != null) {
			IVariableBinding binding = frag.resolveBinding();
			if (binding != null) {
				return binding.getType();
			}
		}
		return null;
	}

	/**
	 * Finds the variable declaration fragment for a given variable name.
	 * 
	 * <p>
	 * This method searches up the AST tree starting from the given node to find the
	 * {@link VariableDeclarationFragment} that declares the specified variable. It
	 * searches through all blocks in parent scopes.
	 * </p>
	 * 
	 * <p><b>Search Strategy:</b></p>
	 * <ol>
	 * <li>Start from the given node and walk up to parent nodes</li>
	 * <li>For each Block encountered, search its statements</li>
	 * <li>Look for VariableDeclarationStatements containing the variable</li>
	 * <li>Return the first matching VariableDeclarationFragment found</li>
	 * </ol>
	 * 
	 * @param startNode the starting node for the search (must not be null)
	 * @param varName   the variable name to find (must not be null)
	 * @return the VariableDeclarationFragment, or null if not found
	 * @throws IllegalArgumentException if startNode or varName is null
	 */
	public static VariableDeclarationFragment findVariableDeclaration(ASTNode startNode, String varName) {
		if (startNode == null) {
			throw new IllegalArgumentException("startNode cannot be null"); //$NON-NLS-1$
		}
		if (varName == null) {
			throw new IllegalArgumentException("varName cannot be null"); //$NON-NLS-1$
		}

		// Try to find the variable declaration in the AST
		// Start from the given node and walk up to find variable declarations
		ASTNode current = startNode;
		while (current != null) {
			if (current instanceof Block) {
				Block block = (Block) current;
				for (Object stmtObj : block.statements()) {
					if (stmtObj instanceof VariableDeclarationStatement) {
						VariableDeclarationStatement varDecl = (VariableDeclarationStatement) stmtObj;
						for (Object fragObj : varDecl.fragments()) {
							if (fragObj instanceof VariableDeclarationFragment) {
								VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
								if (varName.equals(frag.getName().getIdentifier())) {
									return frag;
								}
							}
						}
					}
				}
			}
			current = current.getParent();
		}

		return null;
	}

	/**
	 * Checks if a variable has a @NotNull or @NonNull annotation.
	 * 
	 * <p>
	 * This is used to determine if certain operations can be safely used.
	 * For example, String::concat can be safely used instead of a null-safe lambda
	 * (a, b) -> a + b when the accumulator variable is guaranteed non-null.
	 * </p>
	 * 
	 * <p><b>Supported Annotations:</b></p>
	 * <ul>
	 * <li>{@code @NotNull} (various packages)</li>
	 * <li>{@code @NonNull} (various packages)</li>
	 * </ul>
	 * 
	 * <p><b>Example:</b></p>
	 * <pre>{@code
	 * @NotNull String result = "";
	 * for (String s : strings) {
	 *     result += s;  // Can use String::concat safely
	 * }
	 * }</pre>
	 * 
	 * @param startNode the starting node for the search (must not be null)
	 * @param varName   the variable name to check (must not be null)
	 * @return true if the variable has a @NotNull or @NonNull annotation
	 * @throws IllegalArgumentException if startNode or varName is null
	 */
	public static boolean hasNotNullAnnotation(ASTNode startNode, String varName) {
		if (startNode == null) {
			throw new IllegalArgumentException("startNode cannot be null"); //$NON-NLS-1$
		}
		if (varName == null) {
			throw new IllegalArgumentException("varName cannot be null"); //$NON-NLS-1$
		}

		VariableDeclarationFragment frag = findVariableDeclaration(startNode, varName);
		if (frag != null) {
			IVariableBinding binding = frag.resolveBinding();
			if (binding != null) {
				return hasNotNullAnnotationOnBinding(binding);
			}
		}
		return false;
	}

	/**
	 * Checks if a binding has @NotNull or @NonNull annotation.
	 * 
	 * <p>
	 * This is a helper method that examines the annotations on a variable binding
	 * to determine if it has a non-null annotation from any package. The check is
	 * done by examining the qualified name of each annotation type and looking for
	 * names ending with ".NotNull" or ".NonNull".
	 * </p>
	 * 
	 * @param binding the variable binding to check (may be null)
	 * @return true if the binding has a @NotNull or @NonNull annotation
	 */
	public static boolean hasNotNullAnnotationOnBinding(IVariableBinding binding) {
		if (binding == null) {
			return false;
		}

		IAnnotationBinding[] annotations = binding.getAnnotations();
		if (annotations != null) {
			for (IAnnotationBinding annotation : annotations) {
				ITypeBinding annotationType = annotation.getAnnotationType();
				if (annotationType != null) {
					String qualifiedName = annotationType.getQualifiedName();
					if (qualifiedName != null
							&& (qualifiedName.endsWith(".NotNull") || qualifiedName.endsWith(".NonNull"))) { //$NON-NLS-1$ //$NON-NLS-2$
						return true;
					}
				}
			}
		}

		return false;
	}
}