ContainerLocalRewriteIntegrationTest.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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
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.jdt.core.ICompilationUnit;
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.IVariableBinding;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.sandbox.jdt.container.api.ContainerLocalRewritePlan;
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 ContainerLocalRewriteIntegrationTest {
@RegisterExtension
final AbstractEclipseJava context= new EclipseJava22();
@Test
void appliesTheStrictLocalRewrite() throws Exception {
ICompilationUnit unit= createUnit("""
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;
int count = values.length;
for (String current : values) {
System.out.println(current + count);
}
}
}
""");
CompilationUnit root= parse(unit);
ContainerLocalRewriteFix.create(
unit, root, plan(unit, root, 1, 1, 1, 1))
.createChange(null).perform(null);
String source= unit.getSource();
assertTrue(source.contains("List<String> values")); //$NON-NLS-1$
assertTrue(source.contains("new ArrayList<>()")); //$NON-NLS-1$
assertTrue(source.contains("values.add(value)")); //$NON-NLS-1$
assertTrue(source.contains("values.size()")); //$NON-NLS-1$
assertFalse(source.contains("Arrays.copyOf(values")); //$NON-NLS-1$
assertFalse(source.contains("values[values.length - 1]")); //$NON-NLS-1$
}
@Test
void rewritesSeveralAdjacentAppendPairs() throws Exception {
ICompilationUnit unit= createUnit("""
package test;
import java.util.Arrays;
class Sample {
void collect(String first, String second) {
String[] values = new String[0];
values = Arrays.copyOf(values, values.length + 1);
values[values.length - 1] = first;
values = Arrays.copyOf(values, values.length + 1);
values[values.length - 1] = second;
}
}
""");
CompilationUnit root= parse(unit);
ContainerLocalRewriteFix.create(
unit, root, plan(unit, root, 2, 2, 0, 0))
.createChange(null).perform(null);
String source= unit.getSource();
assertEquals(2, occurrences(source, "values.add(")); //$NON-NLS-1$
assertTrue(source.contains("values.add(first)")); //$NON-NLS-1$
assertTrue(source.contains("values.add(second)")); //$NON-NLS-1$
}
@Test
void rejectsAnUnexpectedBindingUse() throws Exception {
ICompilationUnit unit= createUnit("""
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;
System.identityHashCode(values);
}
}
""");
CompilationUnit root= parse(unit);
CoreException exception= assertThrows(CoreException.class, () ->
ContainerLocalRewriteFix.create(
unit, root, plan(unit, root, 1, 1, 0, 0)));
assertTrue(exception.getMessage().contains("unexpected use")); //$NON-NLS-1$
}
@Test
void rejectsChangedEditingAndIterationCounts() throws Exception {
ICompilationUnit editingUnit= createUnit(appendSource());
CompilationUnit editingRoot= parse(editingUnit);
CoreException editing= assertThrows(CoreException.class, () ->
ContainerLocalRewriteFix.create(
editingUnit, editingRoot,
plan(editingUnit, editingRoot, 2, 2, 0, 0)));
assertTrue(editing.getMessage().contains("occurrence count changed")); //$NON-NLS-1$
ICompilationUnit iterationUnit= createUnit("""
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;
for (String current : values) {
System.out.println(current);
}
}
}
""");
CompilationUnit iterationRoot= parse(iterationUnit);
CoreException iteration= assertThrows(CoreException.class, () ->
ContainerLocalRewriteFix.create(
iterationUnit, iterationRoot,
plan(iterationUnit, iterationRoot, 1, 1, 0, 0)));
assertTrue(iteration.getMessage().contains("encounter iteration")); //$NON-NLS-1$
}
@Test
void rejectsAnInterveningStatement() throws Exception {
ICompilationUnit unit= createUnit("""
package test;
import java.util.Arrays;
class Sample {
void collect(String value) {
String[] values = new String[0];
values = Arrays.copyOf(values, values.length + 1);
System.out.println(value);
values[values.length - 1] = value;
}
}
""");
CompilationUnit root= parse(unit);
CoreException exception= assertThrows(CoreException.class, () ->
ContainerLocalRewriteFix.create(
unit, root, plan(unit, root, 1, 1, 0, 0)));
assertTrue(exception.getMessage().contains("immediately followed")); //$NON-NLS-1$
}
@Test
void rejectsTheWrongCompilationUnitHandle() throws Exception {
ICompilationUnit unit= createUnit(appendSource());
CompilationUnit root= parse(unit);
ContainerLocalRewritePlan original= plan(unit, root, 1, 1, 0, 0);
ContainerLocalRewritePlan wrongUnit= new ContainerLocalRewritePlan(
"other-unit", //$NON-NLS-1$
original.bindingKey(),
original.targetInterfaceType(),
original.targetImplementationType(),
original.targetContract(),
original.edits());
CoreException exception= assertThrows(CoreException.class, () ->
ContainerLocalRewriteFix.create(unit, root, wrongUnit));
assertTrue(exception.getMessage().contains("handle 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 ContainerLocalRewritePlan plan(
ICompilationUnit unit,
CompilationUnit root,
int growthCount,
int appendCount,
int lengthCount,
int iterationCount) {
List<LocalEdit> edits= new ArrayList<>();
edits.add(new LocalEdit(EditKind.CHANGE_LOCAL_DECLARATION, 1, 1));
edits.add(new LocalEdit(EditKind.REPLACE_EMPTY_ARRAY_INITIALIZER, 1, 1));
addEdits(edits, EditKind.REMOVE_ARRAY_GROWTH, growthCount, 10);
addEdits(edits, EditKind.REPLACE_TAIL_WRITE_WITH_ADD, appendCount, 20);
addEdits(edits, EditKind.REPLACE_LENGTH_WITH_SIZE, lengthCount, 30);
addEdits(edits, EditKind.VERIFY_ENCOUNTER_ITERATION, iterationCount, 40);
return new ContainerLocalRewritePlan(
unit.getHandleIdentifier(),
bindingKey(root),
"java.util.List", //$NON-NLS-1$
"java.util.ArrayList", //$NON-NLS-1$
targetContract(),
edits);
}
private static void addEdits(
List<LocalEdit> edits,
EditKind kind,
int count,
int offset) {
for (int index= 0; index < count; index++) {
edits.add(new LocalEdit(kind, offset + index, 1));
}
}
private static String bindingKey(CompilationUnit root) {
String[] result= { null };
root.accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
if ("values".equals(node.getName().getIdentifier())) { //$NON-NLS-1$
IVariableBinding binding= node.resolveBinding();
result[0]= binding == null ? null : binding.getVariableDeclaration().getKey();
}
return true;
}
});
if (result[0] == null) {
throw new IllegalStateException("Missing values binding"); //$NON-NLS-1$
}
return result[0];
}
private static String appendSource() {
return """
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;
}
}
""";
}
private static int occurrences(String source, String token) {
int count= 0;
int offset= 0;
while ((offset= source.indexOf(token, offset)) >= 0) {
count++;
offset+= token.length();
}
return count;
}
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$
}
}