ContainerFlowScopeSearch.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.cleanup.multifile;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;

import org.sandbox.jdt.container.api.ContainerFlowSearchPlan;
import org.sandbox.jdt.container.api.ContainerFlowSearchPlan.SearchSeed;
import org.sandbox.jdt.container.api.ResolvedContainerFlowSearchPlan;
import org.sandbox.jdt.container.api.ResolvedContainerFlowSearchPlan.ResolvedSearchTarget;
import org.sandbox.jdt.container.api.ResolvedContainerFlowSearchPlan.TargetKind;

/**
 * Resolves an AST-free container flow search plan to the source compilation units
 * required by coordinated multi-file planning.
 *
 * <p>This class deliberately does not implement another JDT reference search. It
 * resolves stable Java-model handles, expands method override families and delegates
 * all workspace matching, source-root policy and binary-boundary checks to
 * {@link RelatedCompilationUnitSearch}.</p>
 */
public final class ContainerFlowScopeSearch {

	/** Stable result of one flow-scope expansion attempt. */
	public record Result(
			List<ICompilationUnit> compilationUnits,
			ResolvedContainerFlowSearchPlan resolvedPlan,
			boolean complete,
			List<String> rejectionReasons) {

		public Result {
			compilationUnits= List.copyOf(compilationUnits);
			Objects.requireNonNull(resolvedPlan, "resolvedPlan"); //$NON-NLS-1$
			rejectionReasons= List.copyOf(rejectionReasons);
		}
	}

	@FunctionalInterface
	interface ElementResolver {
		IJavaElement resolve(String handleIdentifier);
	}

	@FunctionalInterface
	interface RelatedUnitFinder {
		RelatedCompilationUnitSearch.Result find(
				IJavaProject project,
				Collection<? extends IJavaElement> targets,
				Collection<ICompilationUnit> initialUnits,
				Collection<ICompilationUnit> allowedUnits,
				IProgressMonitor monitor) throws CoreException;
	}

	@FunctionalInterface
	interface MethodFamilyResolver {
		MethodFamily resolve(IMethod method, IProgressMonitor monitor);
	}

	record MethodFamily(List<IMethod> methods, boolean complete, List<String> rejectionReasons) {
		MethodFamily {
			methods= List.copyOf(methods);
			rejectionReasons= List.copyOf(rejectionReasons);
		}
	}

	private final ElementResolver elementResolver;
	private final MethodFamilyResolver methodFamilyResolver;
	private final RelatedUnitFinder relatedUnitFinder;

	private ContainerFlowScopeSearch() {
		this(JavaCore::create,
				ContainerFlowScopeSearch::resolveMethodFamily,
				RelatedCompilationUnitSearch::findReferences);
	}

	ContainerFlowScopeSearch(
			ElementResolver elementResolver,
			MethodFamilyResolver methodFamilyResolver,
			RelatedUnitFinder relatedUnitFinder) {
		this.elementResolver= Objects.requireNonNull(elementResolver, "elementResolver"); //$NON-NLS-1$
		this.methodFamilyResolver= Objects.requireNonNull(methodFamilyResolver, "methodFamilyResolver"); //$NON-NLS-1$
		this.relatedUnitFinder= Objects.requireNonNull(relatedUnitFinder, "relatedUnitFinder"); //$NON-NLS-1$
	}

	/**
	 * Finds all editable source units needed by the supplied flow search plan.
	 *
	 * @param project coordinated Java project
	 * @param plan AST-free search plan
	 * @param currentScope source units already admitted
	 * @param allowedUnits complete source-root-policy allow-list
	 * @param monitor progress monitor, may be {@code null}
	 * @return deterministic source closure and completeness diagnostics
	 * @throws CoreException if the delegated JDT search fails
	 */
	public static Result findRelatedUnits(
			IJavaProject project,
			ContainerFlowSearchPlan plan,
			Collection<ICompilationUnit> currentScope,
			Collection<ICompilationUnit> allowedUnits,
			IProgressMonitor monitor) throws CoreException {
		return new ContainerFlowScopeSearch().find(
				project, plan, currentScope, allowedUnits, monitor);
	}

	Result find(
			IJavaProject project,
			ContainerFlowSearchPlan plan,
			Collection<ICompilationUnit> currentScope,
			Collection<ICompilationUnit> allowedUnits,
			IProgressMonitor monitor) throws CoreException {
		Objects.requireNonNull(project, "project"); //$NON-NLS-1$
		Objects.requireNonNull(plan, "plan"); //$NON-NLS-1$
		Objects.requireNonNull(currentScope, "currentScope"); //$NON-NLS-1$
		Objects.requireNonNull(allowedUnits, "allowedUnits"); //$NON-NLS-1$
		checkCanceled(monitor);

		if (plan.isEmpty()) {
			RelatedCompilationUnitSearch.Result validated= validateCurrentScope(
					project, currentScope, allowedUnits);
			return result(
					validated,
					ResolvedContainerFlowSearchPlan.empty(),
					List.of());
		}

		Map<String, IJavaElement> targetsByHandle= new LinkedHashMap<>();
		Map<String, ResolvedSearchTarget> resolvedByKey= new LinkedHashMap<>();
		Set<String> rejectionReasons= new LinkedHashSet<>();
		for (SearchSeed seed : plan.seeds()) {
			checkCanceled(monitor);
			resolveSeed(
					seed,
					targetsByHandle,
					resolvedByKey,
					rejectionReasons,
					monitor);
		}

		RelatedCompilationUnitSearch.Result searched;
		if (targetsByHandle.isEmpty()) {
			searched= validateCurrentScope(project, currentScope, allowedUnits);
		} else {
			searched= relatedUnitFinder.find(
					project,
					targetsByHandle.values(),
					currentScope,
					allowedUnits,
					monitor);
		}
		return result(
				searched,
				new ResolvedContainerFlowSearchPlan(
						new ArrayList<>(resolvedByKey.values())),
				rejectionReasons);
	}

	private void resolveSeed(
			SearchSeed seed,
			Map<String, IJavaElement> targetsByHandle,
			Map<String, ResolvedSearchTarget> resolvedByKey,
			Set<String> rejectionReasons,
			IProgressMonitor monitor) {
		if (!seed.hasJavaElementHandle()) {
			rejectionReasons.add("A container flow search seed has no Java-model handle: " //$NON-NLS-1$
					+ seed.sourceNodeId());
			return;
		}

		IJavaElement element= elementResolver.resolve(seed.javaElementHandle());
		if (element == null || !element.exists()) {
			rejectionReasons.add("A container flow Java-model handle cannot be resolved: " //$NON-NLS-1$
					+ seed.javaElementHandle());
			return;
		}

		switch (seed.kind()) {
			case FIELD_REFERENCES -> addField(
					seed, element, targetsByHandle, resolvedByKey, rejectionReasons);
			case METHOD_DECLARATION, METHOD_CALLERS -> addMethod(
					seed, element, targetsByHandle, resolvedByKey, rejectionReasons);
			case METHOD_OVERRIDE_FAMILY -> addMethodFamily(
					seed,
					element,
					targetsByHandle,
					resolvedByKey,
					rejectionReasons,
					monitor);
		}
	}

	private static void addField(
			SearchSeed seed,
			IJavaElement element,
			Map<String, IJavaElement> targetsByHandle,
			Map<String, ResolvedSearchTarget> resolvedByKey,
			Set<String> rejectionReasons) {
		if (!(element instanceof IField field)) {
			rejectionReasons.add("A field-reference seed does not resolve to an IField: " //$NON-NLS-1$
					+ seed.javaElementHandle());
			return;
		}
		addTarget(field, targetsByHandle);
		addResolved(seed, TargetKind.FIELD, field, resolvedByKey);
	}

	private static void addMethod(
			SearchSeed seed,
			IJavaElement element,
			Map<String, IJavaElement> targetsByHandle,
			Map<String, ResolvedSearchTarget> resolvedByKey,
			Set<String> rejectionReasons) {
		if (!(element instanceof IMethod method)) {
			rejectionReasons.add("A method search seed does not resolve to an IMethod: " //$NON-NLS-1$
					+ seed.javaElementHandle());
			return;
		}
		addTarget(method, targetsByHandle);
		addResolved(seed, TargetKind.METHOD, method, resolvedByKey);
	}

	private void addMethodFamily(
			SearchSeed seed,
			IJavaElement element,
			Map<String, IJavaElement> targetsByHandle,
			Map<String, ResolvedSearchTarget> resolvedByKey,
			Set<String> rejectionReasons,
			IProgressMonitor monitor) {
		if (!(element instanceof IMethod method)) {
			rejectionReasons.add("An override-family seed does not resolve to an IMethod: " //$NON-NLS-1$
					+ seed.javaElementHandle());
			return;
		}

		MethodFamily family= methodFamilyResolver.resolve(method, monitor);
		for (IMethod member : family.methods()) {
			addTarget(member, targetsByHandle);
			addResolved(seed, TargetKind.METHOD, member, resolvedByKey);
		}
		if (!family.complete()) {
			rejectionReasons.addAll(family.rejectionReasons());
		}
	}

	private static void addResolved(
			SearchSeed seed,
			TargetKind targetKind,
			IJavaElement element,
			Map<String, ResolvedSearchTarget> resolvedByKey) {
		if (element == null || !element.exists()) {
			return;
		}
		String handle= element.getHandleIdentifier();
		if (handle == null || handle.isBlank()) {
			return;
		}
		ResolvedSearchTarget target= new ResolvedSearchTarget(
				seed.sourceNodeId(),
				seed.kind(),
				targetKind,
				seed.bindingKey(),
				seed.ownerKey(),
				handle,
				seed.signatureIndex(),
				seed.reason());
		resolvedByKey.putIfAbsent(target.stableKey(), target);
	}

	private static void addTarget(
			IJavaElement element,
			Map<String, IJavaElement> targetsByHandle) {
		if (element == null || !element.exists()) {
			return;
		}
		String handle= element.getHandleIdentifier();
		if (handle != null && !handle.isBlank()) {
			targetsByHandle.putIfAbsent(handle, element);
		}
	}

	private static MethodFamily resolveMethodFamily(IMethod method, IProgressMonitor monitor) {
		Map<String, IMethod> methodsByHandle= new LinkedHashMap<>();
		Set<String> reasons= new LinkedHashSet<>();
		addMethod(methodsByHandle, method);

		IType declaringType= method.getDeclaringType();
		if (declaringType == null || !declaringType.exists()) {
			reasons.add("The method declaring type is missing while expanding the override family."); //$NON-NLS-1$
			return methodFamily(methodsByHandle, reasons);
		}

		try {
			ITypeHierarchy hierarchy= declaringType.newTypeHierarchy(monitor);
			for (IType type : hierarchy.getAllTypes()) {
				checkCanceled(monitor);
				IMethod[] corresponding= type.findMethods(method);
				if (corresponding == null) {
					continue;
				}
				for (IMethod candidate : corresponding) {
					addMethod(methodsByHandle, candidate);
				}
			}
		} catch (JavaModelException exception) {
			reasons.add("JDT could not resolve the complete method override family: " //$NON-NLS-1$
					+ exception.getMessage());
		}
		return methodFamily(methodsByHandle, reasons);
	}

	private static MethodFamily methodFamily(
			Map<String, IMethod> methodsByHandle,
			Set<String> reasons) {
		List<IMethod> methods= new ArrayList<>(methodsByHandle.values());
		methods.sort(Comparator.comparing(IJavaElement::getHandleIdentifier));
		return new MethodFamily(methods, reasons.isEmpty(), new ArrayList<>(reasons));
	}

	private static void addMethod(Map<String, IMethod> methodsByHandle, IMethod method) {
		if (method == null || !method.exists()) {
			return;
		}
		String handle= method.getHandleIdentifier();
		if (handle != null && !handle.isBlank()) {
			methodsByHandle.putIfAbsent(handle, method);
		}
	}

	private static RelatedCompilationUnitSearch.Result validateCurrentScope(
			IJavaProject project,
			Collection<ICompilationUnit> currentScope,
			Collection<ICompilationUnit> allowedUnits) {
		RelatedCompilationUnitSearch.MatchAccumulator accumulator=
				new RelatedCompilationUnitSearch.MatchAccumulator(
						project, currentScope, allowedUnits);
		return accumulator.finish();
	}

	private static Result result(
			RelatedCompilationUnitSearch.Result searchResult,
			ResolvedContainerFlowSearchPlan resolvedPlan,
			Collection<String> additionalReasons) {
		Set<String> reasons= new LinkedHashSet<>(searchResult.rejectionReasons());
		reasons.addAll(additionalReasons);
		return new Result(
				searchResult.compilationUnits(),
				resolvedPlan,
				searchResult.complete() && reasons.isEmpty(),
				new ArrayList<>(reasons));
	}

	private static void checkCanceled(IProgressMonitor monitor) {
		if (monitor != null && monitor.isCanceled()) {
			throw new OperationCanceledException();
		}
	}
}