PatternMatchSupport.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 - initial API and implementation
 *******************************************************************************/
package org.sandbox.jdt.triggerpattern.api;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Name;
import org.sandbox.jdt.triggerpattern.api.GuardContext.UnknownSemanticRequirement;

/** Shared source-node identity, automatic bindings and pattern constraints. */
final class PatternMatchSupport {

	private PatternMatchSupport() {
	}

	static Match createMatch(ASTNode node, Map<String, Object> bindings, int offset, int length,
			List<UnknownSemanticRequirement> unknownRequirements) {
		Map<String, Object> completeBindings = new HashMap<>(bindings);
		completeBindings.put("$_", node); //$NON-NLS-1$
		for (ASTNode current = node; current != null; current = current.getParent()) {
			if (current instanceof AbstractTypeDeclaration) {
				completeBindings.put("$this", current); //$NON-NLS-1$
				break;
			}
		}
		return new Match(node, completeBindings, offset, length, unknownRequirements);
	}

	static boolean satisfiesConstraints(Pattern pattern, Match match) {
		String overridesType = pattern.getOverridesType();
		if (overridesType != null && !overridesType.isEmpty()
				&& (!(match.getMatchedNode() instanceof MethodDeclaration method)
						|| !checkOverrides(method, overridesType))) {
			return false;
		}
		ConstraintVariableType[] constraints = pattern.getConstraints();
		return constraints == null || checkTypeConstraints(match, constraints);
	}

	/**
	 * Checks whether a match satisfies all type constraints.
	 *
	 * <p>For each constraint, looks up the bound AST node for the placeholder
	 * variable and resolves its {@link ITypeBinding}. The constraint is satisfied
	 * if the resolved type matches the expected type (by simple name or qualified
	 * name). Missing placeholders or unresolved necessary bindings reject the
	 * match, because an explicit semantic constraint must be proven.</p>
	 *
	 * @param match the match to check
	 * @param constraints the type constraints
	 * @return {@code true} if all constraints are satisfied
	 * @since 1.4.0
	 */
	static boolean checkTypeConstraints(Match match, ConstraintVariableType[] constraints) {
		for (ConstraintVariableType constraint : constraints) {
			String variable = constraint.variable();
			String expectedType = constraint.type();

			ASTNode node = match.getBinding(variable);
			if (node == null) {
				return false;
			}

			ITypeBinding typeBinding = resolveTypeBinding(node);
			if (typeBinding == null) {
				return false;
			}

			if (!matchesTypeOrSupertype(typeBinding, expectedType)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Resolves the type binding for an AST node.
	 *
	 * @param node the AST node
	 * @return the type binding, or {@code null} if not resolvable
	 */
	private static ITypeBinding resolveTypeBinding(ASTNode node) {
		if (node instanceof Name name) {
			IBinding binding = name.resolveBinding();
			if (binding instanceof IVariableBinding varBinding) {
				return varBinding.getType();
			}
			if (binding instanceof ITypeBinding typeBinding) {
				return typeBinding;
			}
		}
		if (node instanceof MethodInvocation methodInv) {
			IMethodBinding methodBinding = methodInv.resolveMethodBinding();
			if (methodBinding != null) {
				return methodBinding.getReturnType();
			}
		}
		if (node instanceof Expression expr) {
			return expr.resolveTypeBinding();
		}
		return null;
	}

	/**
	 * Checks if a type binding matches an expected type name (including supertypes).
	 *
	 * @param typeBinding the resolved type binding
	 * @param expectedType the expected type name (simple or qualified)
	 * @return {@code true} if the type matches
	 */
	private static boolean matchesTypeOrSupertype(ITypeBinding typeBinding, String expectedType) {
		return matchesTypeOrSupertype(typeBinding, expectedType, new java.util.HashSet<>());
	}

	/**
	 * Recursively checks if a type binding matches an expected type name,
	 * using a visited set to prevent infinite recursion.
	 */
	private static boolean matchesTypeOrSupertype(ITypeBinding typeBinding, String expectedType,
			java.util.Set<String> visited) {
		if (typeBinding == null || typeBinding.isRecovered()) {
			return false;
		}
		String qualifiedName = typeBinding.getQualifiedName();
		if (!visited.add(qualifiedName)) {
			return false; // Already visited — break potential cycle
		}
		ITypeBinding erasure = typeBinding.getErasure();
		if (typeBinding.getName().equals(expectedType)
				|| qualifiedName.equals(expectedType)
				|| erasure != null && (erasure.getName().equals(expectedType)
						|| erasure.getQualifiedName().equals(expectedType))) {
			return true;
		}
		// Check superclass chain
		ITypeBinding superclass = typeBinding.getSuperclass();
		if (matchesTypeOrSupertype(superclass, expectedType, visited)) {
			return true;
		}
		// Check interfaces
		for (ITypeBinding iface : typeBinding.getInterfaces()) {
			if (matchesTypeOrSupertype(iface, expectedType, visited)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Checks if a method declaration overrides a method from the specified type.
	 *
	 * <p>Uses binding resolution to traverse the type hierarchy and find overridden methods.
	 * Returns {@code false} if bindings are not available (e.g., when parsing from raw source).</p>
	 *
	 * @param method the method declaration to check
	 * @param overridesType the fully qualified type name that must declare the overridden method
	 * @return {@code true} if the method overrides a method from the specified type
	 */
	static boolean checkOverrides(MethodDeclaration method, String overridesType) {
		IMethodBinding methodBinding = method.resolveBinding();
		if (methodBinding == null) {
			return false;
		}

		ITypeBinding declaringClass = methodBinding.getDeclaringClass();
		if (declaringClass == null) {
			return false;
		}

		// Walk the superclass chain looking for the overridden method
		ITypeBinding superClass = declaringClass.getSuperclass();
		while (superClass != null) {
			if (overridesType.equals(superClass.getQualifiedName())) {
				// Found the target type - check if it declares a method that is overridden
				for (IMethodBinding superMethod : superClass.getDeclaredMethods()) {
					if (methodBinding.overrides(superMethod)) {
						return true;
					}
				}
				// Target type found but method not declared there - stop searching
				return false;
			}
			superClass = superClass.getSuperclass();
		}

		// Also check interfaces
		return checkOverridesInInterfaces(methodBinding, declaringClass, overridesType);
	}

	/**
	 * Checks if a method overrides a method declared in an interface matching the specified type.
	 *
	 * @param methodBinding the method binding to check
	 * @param typeBinding the declaring type
	 * @param overridesType the target interface type
	 * @return {@code true} if the method overrides a method from a matching interface
	 */
	private static boolean checkOverridesInInterfaces(IMethodBinding methodBinding, ITypeBinding typeBinding,
			String overridesType) {
		for (ITypeBinding iface : typeBinding.getInterfaces()) {
			if (overridesType.equals(iface.getQualifiedName())) {
				// Found the target interface - check if it declares the overridden method
				for (IMethodBinding ifaceMethod : iface.getDeclaredMethods()) {
					if (methodBinding.overrides(ifaceMethod)) {
						return true;
					}
				}
				// Target interface found but method not declared there - stop searching
				return false;
			}
			// Recursively check super-interfaces
			if (checkOverridesInInterfaces(methodBinding, iface, overridesType)) {
				return true;
			}
		}
		return false;
	}

}