ClosedSourceParameterMigrationIntegrationTest.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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList;
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.EnhancedForStatement;
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.QualifiedName;
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 ClosedSourceParameterMigrationIntegrationTest {
@RegisterExtension
final AbstractEclipseJava context= new EclipseJava22();
@Test
void appliesAndUndoesCallerAndParameterAtomically() throws Exception {
ICompilationUnit caller= createUnit("Caller.java", callerSource()); //$NON-NLS-1$
ICompilationUnit receiver= createUnit("Receiver.java", receiverSource()); //$NON-NLS-1$
String originalCaller= caller.getSource();
String originalReceiver= receiver.getSource();
CompilationUnit callerRoot= parse(caller);
CompilationUnit receiverRoot= parse(receiver);
CallerFacts callerFacts= callerFacts(callerRoot);
ParameterFacts parameterFacts= parameterFacts(receiverRoot);
ClosedSourceParameterMigrationPlan plan= plan(
caller, receiver, callerFacts, parameterFacts);
ICleanUpFix callerFix= ClosedSourceParameterMigrationFix.create(
caller, callerRoot, plan);
ICleanUpFix receiverFix= ClosedSourceParameterMigrationFix.create(
receiver, receiverRoot, plan);
assertNotNull(callerFix);
assertNotNull(receiverFix);
CompositeChange change= new CompositeChange(
"Migrate caller and parameter container contract"); //$NON-NLS-1$
change.add(callerFix.createChange(null));
change.add(receiverFix.createChange(null));
Change undo= change.perform(new NullProgressMonitor());
assertNotNull(undo);
String migratedCaller= caller.getSource();
String migratedReceiver= receiver.getSource();
assertTrue(migratedCaller.contains("List<String> values")); //$NON-NLS-1$
assertTrue(migratedCaller.contains("new ArrayList<>()")); //$NON-NLS-1$
assertTrue(migratedCaller.contains("values.add(value)")); //$NON-NLS-1$
assertTrue(migratedCaller.contains("receiver.consume(values)")); //$NON-NLS-1$
assertFalse(migratedCaller.contains("Arrays.copyOf(values")); //$NON-NLS-1$
assertTrue(migratedReceiver.contains("void consume(List<String> values)")); //$NON-NLS-1$
assertTrue(migratedReceiver.contains("values.size()")); //$NON-NLS-1$
assertFalse(migratedReceiver.contains("String[] values")); //$NON-NLS-1$
undo.perform(new NullProgressMonitor());
assertTrue(caller.getSource().contains("String[] values = new String[0]")); //$NON-NLS-1$
assertTrue(receiver.getSource().contains("void consume(String[] values)")); //$NON-NLS-1$
assertTrue(caller.getSource().contains("Arrays.copyOf(values")); //$NON-NLS-1$
assertTrue(receiver.getSource().contains("values.length")); //$NON-NLS-1$
assertTrue(caller.getSource().equals(originalCaller));
assertTrue(receiver.getSource().equals(originalReceiver));
}
private static ClosedSourceParameterMigrationPlan plan(
ICompilationUnit caller,
ICompilationUnit receiver,
CallerFacts callerFacts,
ParameterFacts parameterFacts) {
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(
parameterFacts.methodHandle(),
0,
callerFacts.argumentStart(),
callerFacts.argumentLength())));
List<ParameterEdit> parameterEdits= new ArrayList<>();
parameterEdits.add(new ParameterEdit(
ContainerParameterRewritePlan.EditKind.CHANGE_PARAMETER_DECLARATION,
1,
1));
parameterEdits.add(new ParameterEdit(
ContainerParameterRewritePlan.EditKind.REPLACE_LENGTH_WITH_SIZE,
parameterFacts.lengthStart(),
parameterFacts.lengthLength()));
parameterEdits.add(new ParameterEdit(
ContainerParameterRewritePlan.EditKind.VERIFY_ENCOUNTER_ITERATION,
parameterFacts.iterationStart(),
parameterFacts.iterationLength()));
ContainerParameterRewritePlan parameterPlan= new ContainerParameterRewritePlan(
receiver.getHandleIdentifier(),
parameterFacts.methodHandle(),
parameterFacts.bindingKey(),
0,
"java.util.List", //$NON-NLS-1$
target,
parameterEdits);
return new ClosedSourceParameterMigrationPlan(
target, callerPlan, parameterPlan);
}
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[] methodHandle= { null };
String[] bindingKey= { null };
int[] lengthRange= { -1, -1 };
int[] iterationRange= { -1, -1 };
root.accept(new ASTVisitor() {
@Override
public boolean visit(MethodDeclaration method) {
if (!"consume".equals(method.getName().getIdentifier())) { //$NON-NLS-1$
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;
}
@Override
public boolean visit(QualifiedName name) {
if ("length".equals(name.getName().getIdentifier()) //$NON-NLS-1$
&& name.getQualifier() instanceof SimpleName qualifier
&& "values".equals(qualifier.getIdentifier())) { //$NON-NLS-1$
lengthRange[0]= name.getStartPosition();
lengthRange[1]= name.getLength();
}
return true;
}
@Override
public boolean visit(EnhancedForStatement statement) {
Expression expression= statement.getExpression();
if (expression instanceof SimpleName name
&& "values".equals(name.getIdentifier())) { //$NON-NLS-1$
iterationRange[0]= expression.getStartPosition();
iterationRange[1]= expression.getLength();
}
return true;
}
});
if (methodHandle[0] == null || bindingKey[0] == null
|| lengthRange[0] < 0 || iterationRange[0] < 0) {
throw new IllegalStateException("Missing parameter source facts"); //$NON-NLS-1$
}
return new ParameterFacts(
methodHandle[0],
bindingKey[0],
lengthRange[0],
lengthRange[1],
iterationRange[0],
iterationRange[1]);
}
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(Receiver 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 receiverSource() {
return """
package test;
class Receiver {
void consume(String[] values) {
int count = values.length;
for (String value : values) {
System.out.println(value + count);
}
}
}
""";
}
private record CallerFacts(
String bindingKey,
int argumentStart,
int argumentLength) {
}
private record ParameterFacts(
String methodHandle,
String bindingKey,
int lengthStart,
int lengthLength,
int iterationStart,
int iterationLength) {
}
}