ContainerLocalArgumentTransferVerifier.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.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.SimpleName;

import org.sandbox.jdt.container.api.ContainerLocalRewritePlan;
import org.sandbox.jdt.container.api.ContainerLocalRewritePlan.ArgumentTransfer;

/** Revalidates unchanged caller arguments against their exact planned method target. */
final class ContainerLocalArgumentTransferVerifier {

	private static final String PLUGIN_ID= "sandbox_common"; //$NON-NLS-1$

	private ContainerLocalArgumentTransferVerifier() {
	}

	static void verify(
			ICompilationUnit unit,
			org.eclipse.jdt.core.dom.CompilationUnit root,
			ContainerLocalRewritePlan plan) throws CoreException {
		if (plan.argumentTransfers().isEmpty()) {
			return;
		}
		Map<SourceRange, ArgumentTransfer> expected=
				HashMap.newHashMap(plan.argumentTransfers().size());
		for (ArgumentTransfer transfer : plan.argumentTransfers()) {
			expected.put(new SourceRange(
					transfer.sourceStart(), transfer.sourceLength()), transfer);
		}
		Set<SourceRange> observed= HashSet.newHashSet(expected.size());
		CoreException[] failure= { null };
		root.accept(new ASTVisitor() {
			@Override
			public boolean visit(SimpleName name) {
				if (failure[0] != null) {
					return false;
				}
				SourceRange range= new SourceRange(
						name.getStartPosition(), name.getLength());
				ArgumentTransfer transfer= expected.get(range);
				if (transfer == null) {
					return true;
				}
				if (!plannedVariable(name, plan.bindingKey())
						|| !(name.getParent() instanceof MethodInvocation invocation)) {
					failure[0]= stale(unit,
							"planned argument transfer no longer contains the local value"); //$NON-NLS-1$
					return false;
				}
				int argumentIndex= invocation.arguments().indexOf(name);
				if (argumentIndex != transfer.parameterIndex()
						|| !transfer.methodJavaElementHandle().equals(
								methodHandle(invocation.resolveMethodBinding()))) {
					failure[0]= stale(unit, "argument-transfer target changed"); //$NON-NLS-1$
					return false;
				}
				if (!observed.add(range)) {
					failure[0]= stale(unit,
							"argument-transfer source range resolves more than once"); //$NON-NLS-1$
					return false;
				}
				return true;
			}
		});
		if (failure[0] != null) {
			throw failure[0];
		}
		if (!observed.equals(expected.keySet())) {
			throw stale(unit, "argument-transfer occurrence set changed"); //$NON-NLS-1$
		}
	}

	private static boolean plannedVariable(SimpleName name, String bindingKey) {
		return name.resolveBinding() instanceof IVariableBinding binding
				&& bindingKey.equals(binding.getVariableDeclaration().getKey());
	}

	private static String methodHandle(IMethodBinding binding) {
		IJavaElement element= binding == null
				? null : binding.getMethodDeclaration().getJavaElement();
		return element == null ? "" : element.getHandleIdentifier(); //$NON-NLS-1$
	}

	private static CoreException stale(ICompilationUnit unit, String detail) {
		return new CoreException(new Status(
				IStatus.ERROR,
				PLUGIN_ID,
				"Container rewrite plan is stale for " //$NON-NLS-1$
						+ unit.getElementName() + ": " + detail)); //$NON-NLS-1$
	}

	private record SourceRange(int start, int length) {
	}
}