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) {
}
}