ContainerLocalArgumentTransferIntegrationTest.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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
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.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.MethodInvocation;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;

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.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 ContainerLocalArgumentTransferIntegrationTest {

	@RegisterExtension
	final AbstractEclipseJava context= new EclipseJava22();

	@Test
	void retainsOneExactlyPlannedMethodArgument() throws Exception {
		ICompilationUnit unit= createUnit(source(false));
		CompilationUnit root= parse(unit);
		SourceFacts facts= facts(root);

		ContainerLocalRewriteFix.create(unit, root, plan(unit, facts))
				.createChange(null).perform(null);

		String transformed= unit.getSource();
		assertTrue(transformed.contains("List<String> values")); //$NON-NLS-1$
		assertTrue(transformed.contains("new ArrayList<>()")); //$NON-NLS-1$
		assertTrue(transformed.contains("values.add(value)")); //$NON-NLS-1$
		assertTrue(transformed.contains("consume(values)")); //$NON-NLS-1$
		assertFalse(transformed.contains("Arrays.copyOf(values")); //$NON-NLS-1$
	}

	@Test
	void rejectsAnAdditionalUnplannedMethodArgument() throws Exception {
		ICompilationUnit unit= createUnit(source(true));
		CompilationUnit root= parse(unit);
		SourceFacts facts= facts(root);

		CoreException exception= assertThrows(CoreException.class, () ->
				ContainerLocalRewriteFix.create(unit, root, plan(unit, facts)));

		assertTrue(exception.getMessage().contains("unexpected use")); //$NON-NLS-1$
	}

	@Test
	void rejectsAChangedTargetWithTheSameArgumentSourceRange() throws Exception {
		ICompilationUnit unit= createUnit(source(false));
		CompilationUnit originalRoot= parse(unit);
		ContainerLocalRewritePlan originalPlan= plan(unit, facts(originalRoot));
		unit.getBuffer().setContents(
				unit.getSource().replace("consume(values);", "another(values);")); //$NON-NLS-1$ //$NON-NLS-2$
		unit.save(null, true);

		CoreException exception= assertThrows(CoreException.class, () ->
				ContainerLocalRewriteFix.create(unit, parse(unit), originalPlan));

		assertTrue(exception.getMessage().contains("argument-transfer target changed")); //$NON-NLS-1$
	}

	private ContainerLocalRewritePlan plan(
			ICompilationUnit unit,
			SourceFacts facts) {
		return new ContainerLocalRewritePlan(
				unit.getHandleIdentifier(),
				facts.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,
								facts.argumentStart(),
								facts.argumentLength())),
				List.of(new ArgumentTransfer(
						facts.methodJavaElementHandle(),
						0,
						facts.argumentStart(),
						facts.argumentLength())));
	}

	private static SourceFacts facts(CompilationUnit root) {
		String[] bindingKey= { null };
		String[] methodHandle= { null };
		int[] range= { -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 (range[0] >= 0
						|| !"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$
					range[0]= name.getStartPosition();
					range[1]= name.getLength();
					IMethodBinding method= invocation.resolveMethodBinding();
					IJavaElement element= method == null
							? null : method.getMethodDeclaration().getJavaElement();
					methodHandle[0]= element == null
							? null : element.getHandleIdentifier();
				}
				return true;
			}
		});
		if (bindingKey[0] == null || methodHandle[0] == null || range[0] < 0) {
			throw new IllegalStateException("Missing planned source facts"); //$NON-NLS-1$
		}
		return new SourceFacts(
				bindingKey[0], methodHandle[0], range[0], range[1]);
	}

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

	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 String source(boolean secondCall) {
		String template= """
			package test;
			import java.util.Arrays;
			class Sample {
				void collect(String value) {
					String[] values = new String[0];
					values = Arrays.copyOf(values, values.length + 1);
					values[values.length - 1] = value;
					consume(values);
					__SECOND_CALL__
				}
				void consume(String[] values) {}
				void another(String[] values) {}
			}
			""";
		return template.replace(
				"__SECOND_CALL__", secondCall ? "consume(values);" : ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
	}

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

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