ClosedSourceSameUnitParameterMigrationIntegrationTest.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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;

import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.CompositeChange;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IMethodBinding;
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.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;

import org.eclipse.jdt.ui.cleanup.ICleanUpFix;

import org.sandbox.jdt.container.api.ClosedSourceParameterMigrationPlan;
import org.sandbox.jdt.container.api.ContainerLocalRewritePlan;
import org.sandbox.jdt.container.api.ContainerLocalRewritePlan.ArgumentTransfer;
import org.sandbox.jdt.container.api.ContainerLocalRewritePlan.EditKind;
import org.sandbox.jdt.container.api.ContainerLocalRewritePlan.LocalEdit;
import org.sandbox.jdt.container.api.ContainerParameterRewritePlan;
import org.sandbox.jdt.container.api.ContainerParameterRewritePlan.ParameterEdit;
import org.sandbox.jdt.container.api.ContainerShape;
import org.sandbox.jdt.container.api.ContainerUsageProfile.NullContract;
import org.sandbox.jdt.container.api.ContainerUsageProfile.OrderRequirement;
import org.sandbox.jdt.container.api.ContainerUsageProfile.UniquenessRequirement;
import org.sandbox.jdt.container.api.TargetContainerContract;
import org.sandbox.jdt.container.api.TargetContainerContract.Mutability;
import org.sandbox.jdt.ui.tests.quickfix.rules.AbstractEclipseJava;
import org.sandbox.jdt.ui.tests.quickfix.rules.EclipseJava22;

class ClosedSourceSameUnitParameterMigrationIntegrationTest {

	@RegisterExtension
	final AbstractEclipseJava context= new EclipseJava22();

	@Test
	void combinesMultipleParameterOperationsForOneCompilationUnit() throws Exception {
		ICompilationUnit caller= createUnit("Caller.java", callerSource()); //$NON-NLS-1$
		ICompilationUnit contract= createUnit("Contract.java", contractSource()); //$NON-NLS-1$
		String originalCaller= caller.getSource();
		String originalContract= contract.getSource();

		CompilationUnit callerRoot= parse(caller);
		CompilationUnit contractRoot= parse(contract);
		CallerFacts callerFacts= callerFacts(callerRoot);
		ParameterFacts consume= parameterFacts(contractRoot, "consume"); //$NON-NLS-1$
		ParameterFacts consumeAgain= parameterFacts(contractRoot, "consumeAgain"); //$NON-NLS-1$
		ClosedSourceParameterMigrationPlan plan= plan(
				caller,
				callerFacts,
				List.of(
						parameterPlan(contract, consume),
						parameterPlan(contract, consumeAgain)),
				consume.methodHandle());

		assertEquals(
				List.of(caller.getHandleIdentifier(), contract.getHandleIdentifier()),
				plan.affectedCompilationUnitHandles());

		ICleanUpFix callerFix= ClosedSourceParameterMigrationFix.create(
				caller, callerRoot, plan);
		ICleanUpFix contractFix= ClosedSourceParameterMigrationFix.create(
				contract, contractRoot, plan);
		assertNotNull(callerFix);
		assertNotNull(contractFix);

		CompositeChange change= new CompositeChange(
				"Migrate two parameters in one compilation unit"); //$NON-NLS-1$
		change.add(callerFix.createChange(null));
		change.add(contractFix.createChange(null));
		Change undo= change.perform(new NullProgressMonitor());
		assertNotNull(undo);

		assertTrue(caller.getSource().contains("List<String> values")); //$NON-NLS-1$
		assertTrue(contract.getSource().contains(
				"void consume(List<String> values)")); //$NON-NLS-1$
		assertTrue(contract.getSource().contains(
				"void consumeAgain(List<String> values)")); //$NON-NLS-1$

		undo.perform(new NullProgressMonitor());
		assertEquals(originalCaller, caller.getSource());
		assertEquals(originalContract, contract.getSource());
	}

	private static ClosedSourceParameterMigrationPlan plan(
			ICompilationUnit caller,
			CallerFacts callerFacts,
			List<ContainerParameterRewritePlan> parameterPlans,
			String callTargetMethodHandle) {
		TargetContainerContract target= target();
		ContainerLocalRewritePlan callerPlan= new ContainerLocalRewritePlan(
				caller.getHandleIdentifier(),
				callerFacts.bindingKey(),
				"java.util.List", //$NON-NLS-1$
				"java.util.ArrayList", //$NON-NLS-1$
				target,
				List.of(
						new LocalEdit(EditKind.CHANGE_LOCAL_DECLARATION, 1, 1),
						new LocalEdit(EditKind.REPLACE_EMPTY_ARRAY_INITIALIZER, 1, 1),
						new LocalEdit(EditKind.REMOVE_ARRAY_GROWTH, 2, 1),
						new LocalEdit(EditKind.REPLACE_TAIL_WRITE_WITH_ADD, 3, 1),
						new LocalEdit(
								EditKind.VERIFY_ARGUMENT_TRANSFER,
								callerFacts.argumentStart(),
								callerFacts.argumentLength())),
				List.of(new ArgumentTransfer(
						callTargetMethodHandle,
						0,
						callerFacts.argumentStart(),
						callerFacts.argumentLength())));
		return new ClosedSourceParameterMigrationPlan(
				target, callerPlan, parameterPlans);
	}

	private static ContainerParameterRewritePlan parameterPlan(
			ICompilationUnit unit,
			ParameterFacts facts) {
		return new ContainerParameterRewritePlan(
				unit.getHandleIdentifier(),
				facts.methodHandle(),
				facts.bindingKey(),
				0,
				"java.util.List", //$NON-NLS-1$
				target(),
				List.of(new ParameterEdit(
						ContainerParameterRewritePlan.EditKind.CHANGE_PARAMETER_DECLARATION,
						1,
						1)));
	}

	private static CallerFacts callerFacts(CompilationUnit root) {
		String[] bindingKey= { null };
		int[] argumentRange= { -1, -1 };
		root.accept(new ASTVisitor() {
			@Override
			public boolean visit(VariableDeclarationFragment fragment) {
				if ("values".equals(fragment.getName().getIdentifier())) { //$NON-NLS-1$
					IVariableBinding binding= fragment.resolveBinding();
					bindingKey[0]= binding == null
							? null : binding.getVariableDeclaration().getKey();
				}
				return true;
			}

			@Override
			public boolean visit(MethodInvocation invocation) {
				if (!"consume".equals(invocation.getName().getIdentifier()) //$NON-NLS-1$
						|| invocation.arguments().size() != 1) {
					return true;
				}
				Expression argument= (Expression) invocation.arguments().get(0);
				if (argument instanceof SimpleName name
						&& "values".equals(name.getIdentifier())) { //$NON-NLS-1$
					argumentRange[0]= name.getStartPosition();
					argumentRange[1]= name.getLength();
				}
				return true;
			}
		});
		if (bindingKey[0] == null || argumentRange[0] < 0) {
			throw new IllegalStateException("Missing caller source facts"); //$NON-NLS-1$
		}
		return new CallerFacts(
				bindingKey[0], argumentRange[0], argumentRange[1]);
	}

	private static ParameterFacts parameterFacts(
			CompilationUnit root,
			String methodName) {
		String[] methodHandle= { null };
		String[] bindingKey= { null };
		root.accept(new ASTVisitor() {
			@Override
			public boolean visit(MethodDeclaration method) {
				if (!methodName.equals(method.getName().getIdentifier())) {
					return true;
				}
				IMethodBinding binding= method.resolveBinding();
				IJavaElement element= binding == null
						? null : binding.getMethodDeclaration().getJavaElement();
				methodHandle[0]= element == null
						? null : element.getHandleIdentifier();
				SingleVariableDeclaration parameter=
						(SingleVariableDeclaration) method.parameters().get(0);
				IVariableBinding parameterBinding= parameter.resolveBinding();
				bindingKey[0]= parameterBinding == null
						? null : parameterBinding.getVariableDeclaration().getKey();
				return true;
			}
		});
		if (methodHandle[0] == null || bindingKey[0] == null) {
			throw new IllegalStateException("Missing parameter source facts"); //$NON-NLS-1$
		}
		return new ParameterFacts(methodHandle[0], bindingKey[0]);
	}

	private ICompilationUnit createUnit(String name, String source) throws CoreException {
		IPackageFragment fragment= context.getSourceFolder()
				.createPackageFragment("test", false, null); //$NON-NLS-1$
		return fragment.createCompilationUnit(name, source, true, null);
	}

	private CompilationUnit parse(ICompilationUnit unit) {
		ASTParser parser= ASTParser.newParser(AST.getJLSLatest());
		parser.setProject(context.getJavaProject());
		parser.setSource(unit);
		parser.setResolveBindings(true);
		parser.setBindingsRecovery(true);
		parser.setStatementsRecovery(true);
		return (CompilationUnit) parser.createAST(null);
	}

	private static TargetContainerContract target() {
		return new TargetContainerContract(
				ContainerShape.LIST,
				OrderRequirement.ENCOUNTER,
				UniquenessRequirement.DUPLICATES_ALLOWED,
				Mutability.MUTABLE,
				NullContract.ALLOWED,
				"Use one mutable dynamic sequence contract."); //$NON-NLS-1$
	}

	private static String callerSource() {
		return """
			package test;
			import java.util.Arrays;
			class Caller {
				void collect(Contract receiver, String value) {
					String[] values = new String[0];
					values = Arrays.copyOf(values, values.length + 1);
					values[values.length - 1] = value;
					receiver.consume(values);
				}
			}
			""";
	}

	private static String contractSource() {
		return """
			package test;
			interface Contract {
				void consume(String[] values);
				void consumeAgain(String[] values);
			}
			""";
	}

	private record CallerFacts(
			String bindingKey,
			int argumentStart,
			int argumentLength) {
	}

	private record ParameterFacts(String methodHandle, String bindingKey) {
	}
}