ContainerParameterRewriteIntegrationTest.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.ArrayList;
import java.util.Comparator;
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.EnhancedForStatement;
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.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;

import org.sandbox.jdt.container.api.ContainerParameterRewritePlan;
import org.sandbox.jdt.container.api.ContainerParameterRewritePlan.EditKind;
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 ContainerParameterRewriteIntegrationTest {

	@RegisterExtension
	final AbstractEclipseJava context= new EclipseJava22();

	@Test
	void convertsAReadOnlyArrayParameterToList() throws Exception {
		ICompilationUnit unit= createUnit("""
			package test;
			class Sample {
				void consume(String[] values) {
					int count = values.length;
					for (String value : values) {
						System.out.println(value + count);
					}
				}
			}
			""");
		CompilationUnit root= parse(unit);

		ContainerParameterRewriteFix.create(
				unit, root, plan(unit, root, 1, 1))
				.createChange(null).perform(null);

		String source= unit.getSource();
		assertTrue(source.contains("import java.util.List;")); //$NON-NLS-1$
		assertTrue(source.contains("void consume(List<String> values)")); //$NON-NLS-1$
		assertTrue(source.contains("values.size()")); //$NON-NLS-1$
		assertTrue(source.contains("for (String value : values)")); //$NON-NLS-1$
		assertFalse(source.contains("values.length")); //$NON-NLS-1$
	}

	@Test
	void rejectsAnUnexpectedParameterUse() throws Exception {
		ICompilationUnit unit= createUnit("""
			package test;
			class Sample {
				void consume(String[] values) {
					System.identityHashCode(values);
				}
			}
			""");
		CompilationUnit root= parse(unit);

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

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

	@Test
	void rejectsParameterCaptureEvenWhenTheNestedUseIsOnlyLength() throws Exception {
		ICompilationUnit unit= createUnit("""
			package test;
			class Sample {
				void consume(String[] values) {
					Runnable task = () -> System.out.println(values.length);
					task.run();
				}
			}
			""");
		CompilationUnit root= parse(unit);

		CoreException exception= assertThrows(CoreException.class, () ->
				ContainerParameterRewriteFix.create(
						unit, root, plan(unit, root, 1, 0)));

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

	@Test
	void rejectsChangedLengthAndIterationCounts() throws Exception {
		ICompilationUnit unit= createUnit("""
			package test;
			class Sample {
				void consume(String[] values) {
					System.out.println(values.length);
					for (String value : values) {
						System.out.println(value);
					}
				}
			}
			""");
		CompilationUnit root= parse(unit);

		CoreException length= assertThrows(CoreException.class, () ->
				ContainerParameterRewriteFix.create(
						unit, root, plan(unit, root, 0, 1)));
		assertTrue(length.getMessage().contains("length occurrence count")); //$NON-NLS-1$

		CoreException iteration= assertThrows(CoreException.class, () ->
				ContainerParameterRewriteFix.create(
						unit, root, plan(unit, root, 1, 0)));
		assertTrue(iteration.getMessage().contains("encounter iteration")); //$NON-NLS-1$
	}

	@Test
	void rejectsShiftedUsageRangesWithUnchangedCounts() throws Exception {
		ICompilationUnit unit= createUnit("""
			package test;
			class Sample {
				void consume(String[] values) {
					int count = values.length;
					for (String value : values) {
						System.out.println(value + count);
					}
				}
			}
			""");
		ContainerParameterRewritePlan originalPlan= plan(unit, parse(unit), 1, 1);
		unit.getBuffer().setContents(unit.getSource().replace(
				"int count = values.length;", //$NON-NLS-1$
				"\n\t\tint count = values.length;")); //$NON-NLS-1$
		unit.save(null, true);

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

		assertTrue(exception.getMessage().contains("source ranges changed")); //$NON-NLS-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 ContainerParameterRewritePlan plan(
			ICompilationUnit unit,
			CompilationUnit root,
			int lengthCount,
			int iterationCount) {
		SourceFacts facts= sourceFacts(root);
		if (lengthCount > facts.lengths().size()
				|| iterationCount > facts.iterations().size()) {
			throw new IllegalArgumentException("Requested evidence exceeds source facts"); //$NON-NLS-1$
		}
		List<ParameterEdit> edits= new ArrayList<>();
		edits.add(new ParameterEdit(
				EditKind.CHANGE_PARAMETER_DECLARATION,
				facts.parameter().getStartPosition(),
				facts.parameter().getLength()));
		facts.lengths().stream().limit(lengthCount).forEach(range -> edits.add(
				new ParameterEdit(
						EditKind.REPLACE_LENGTH_WITH_SIZE,
						range.start(), range.length())));
		facts.iterations().stream().limit(iterationCount).forEach(range -> edits.add(
				new ParameterEdit(
						EditKind.VERIFY_ENCOUNTER_ITERATION,
						range.start(), range.length())));
		return new ContainerParameterRewritePlan(
				unit.getHandleIdentifier(),
				facts.methodJavaElementHandle(),
				facts.parameterBindingKey(),
				0,
				"java.util.List", //$NON-NLS-1$
				targetContract(),
				edits);
	}

	private static SourceFacts sourceFacts(CompilationUnit root) {
		MethodDeclaration method= method(root);
		SingleVariableDeclaration parameter=
				(SingleVariableDeclaration) method.parameters().get(0);
		IVariableBinding parameterBinding= parameter.resolveBinding();
		IMethodBinding methodBinding= method.resolveBinding();
		IJavaElement methodElement= methodBinding == null
				? null : methodBinding.getMethodDeclaration().getJavaElement();
		if (parameterBinding == null || methodElement == null) {
			throw new IllegalStateException("Missing method or parameter binding"); //$NON-NLS-1$
		}
		String bindingKey= parameterBinding.getVariableDeclaration().getKey();
		List<SourceRange> lengths= new ArrayList<>();
		List<SourceRange> iterations= new ArrayList<>();
		method.accept(new ASTVisitor() {
			@Override
			public boolean visit(QualifiedName name) {
				if ("length".equals(name.getName().getIdentifier()) //$NON-NLS-1$
						&& name.getQualifier() instanceof SimpleName qualifier
						&& qualifier.resolveBinding() instanceof IVariableBinding binding
						&& bindingKey.equals(binding.getVariableDeclaration().getKey())) {
					lengths.add(new SourceRange(
							name.getStartPosition(), name.getLength()));
				}
				return true;
			}

			@Override
			public boolean visit(EnhancedForStatement enhanced) {
				if (enhanced.getExpression() instanceof SimpleName name
						&& name.resolveBinding() instanceof IVariableBinding binding
						&& bindingKey.equals(binding.getVariableDeclaration().getKey())) {
					iterations.add(new SourceRange(
							name.getStartPosition(), name.getLength()));
				}
				return true;
			}
		});
		Comparator<SourceRange> order= Comparator.comparingInt(SourceRange::start);
		lengths.sort(order);
		iterations.sort(order);
		return new SourceFacts(
				parameter,
				methodElement.getHandleIdentifier(),
				bindingKey,
				List.copyOf(lengths),
				List.copyOf(iterations));
	}

	private static MethodDeclaration method(CompilationUnit root) {
		MethodDeclaration[] result= { null };
		root.accept(new ASTVisitor() {
			@Override
			public boolean visit(MethodDeclaration declaration) {
				if ("consume".equals(declaration.getName().getIdentifier())) { //$NON-NLS-1$
					result[0]= declaration;
				}
				return true;
			}
		});
		if (result[0] == null) {
			throw new IllegalStateException("Missing consume method"); //$NON-NLS-1$
		}
		return result[0];
	}

	private static TargetContainerContract targetContract() {
		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(
			SingleVariableDeclaration parameter,
			String methodJavaElementHandle,
			String parameterBindingKey,
			List<SourceRange> lengths,
			List<SourceRange> iterations) {
	}

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