UniqueSequenceLocalRewriteResolver.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 java.util.ArrayList;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.sandbox.jdt.container.analysis.UniqueSequencePattern;
import org.sandbox.jdt.container.analysis.UniqueSequencePattern.GuardedAdd;
import org.sandbox.jdt.container.api.ContainerShape;
import org.sandbox.jdt.container.api.ContainerUsageProfile.OrderRequirement;
import org.sandbox.jdt.container.api.ContainerUsageProfile.UniquenessRequirement;
import org.sandbox.jdt.container.api.TargetContainerContract.Mutability;
import org.sandbox.jdt.container.api.UniqueSequenceLocalRewritePlan;
import org.sandbox.jdt.container.api.UniqueSequenceLocalRewritePlan.EditKind;
/** Re-resolves and verifies one local unique-sequence rewrite plan. */
final class UniqueSequenceLocalRewriteResolver {
private static final String PLUGIN_ID= "sandbox_common"; //$NON-NLS-1$
private static final String ARRAY_LIST= "java.util.ArrayList"; //$NON-NLS-1$
private static final String LIST= "java.util.List"; //$NON-NLS-1$
private static final String STRING= "java.lang.String"; //$NON-NLS-1$
private static final String SET= "java.util.Set"; //$NON-NLS-1$
private static final String LINKED_HASH_SET= "java.util.LinkedHashSet"; //$NON-NLS-1$
private UniqueSequenceLocalRewriteResolver() {
}
static ResolvedPlan resolve(
ICompilationUnit unit,
org.eclipse.jdt.core.dom.CompilationUnit root,
UniqueSequenceLocalRewritePlan plan) throws CoreException {
Objects.requireNonNull(unit, "unit"); //$NON-NLS-1$
Objects.requireNonNull(root, "root"); //$NON-NLS-1$
Objects.requireNonNull(plan, "plan"); //$NON-NLS-1$
if (!plan.compilationUnitHandle().equals(unit.getHandleIdentifier())) {
throw stale(unit, "compilation-unit handle changed"); //$NON-NLS-1$
}
if (!isExpectedTarget(plan)) {
throw stale(unit, "target strategy changed"); //$NON-NLS-1$
}
CollectedAst collected= collect(root, plan.bindingKey());
VariableDeclarationFragment fragment= collected.declaration();
if (fragment == null
|| !(fragment.getParent() instanceof VariableDeclarationStatement declaration)
|| declaration.fragments().size() != 1
|| !(declaration.getType() instanceof ParameterizedType declarationType)
|| declarationType.typeArguments().size() != 1
|| !isSupportedSourceType(declaration.getType().resolveBinding())
|| !hasStableHash(declarationType)
|| !(fragment.getInitializer() instanceof ClassInstanceCreation initializer)
|| !isEmptyArrayList(initializer)) {
throw stale(unit,
"local list declaration, element contract, or empty ArrayList initializer changed"); //$NON-NLS-1$
}
List<GuardedAdd> guards= collected.guards().stream()
.sorted(Comparator.comparingInt(guard -> guard.statement().getStartPosition()))
.toList();
if (guards.size() != editCount(plan, EditKind.REPLACE_DUPLICATE_GUARD)) {
throw stale(unit, "duplicate-guard occurrence count changed"); //$NON-NLS-1$
}
Set<MethodInvocation> recognizedInvocations=
java.util.Collections.newSetFromMap(new IdentityHashMap<>());
for (GuardedAdd guard : guards) {
recognizedInvocations.add(guard.contains());
recognizedInvocations.add(guard.add());
}
int encounterIterations= 0;
for (SimpleName reference : collected.references()) {
if (fragment.getName() == reference) {
continue;
}
ASTNode parent= reference.getParent();
if (parent instanceof MethodInvocation invocation
&& invocation.getExpression() == reference) {
if (recognizedInvocations.contains(invocation)
|| isReadOnlyCollectionQuery(invocation)) {
continue;
}
}
if (parent instanceof EnhancedForStatement enhanced
&& enhanced.getExpression() == reference) {
encounterIterations++;
continue;
}
throw stale(unit, "unexpected use of local sequence binding at source offset " //$NON-NLS-1$
+ reference.getStartPosition());
}
if (encounterIterations != editCount(plan, EditKind.VERIFY_ENCOUNTER_ITERATION)) {
throw stale(unit, "encounter-iteration occurrence count changed"); //$NON-NLS-1$
}
return new ResolvedPlan(
plan,
declaration,
fragment,
declarationType,
initializer,
guards);
}
private static boolean isExpectedTarget(UniqueSequenceLocalRewritePlan plan) {
return SET.equals(plan.targetInterfaceType())
&& LINKED_HASH_SET.equals(plan.targetImplementationType())
&& plan.targetContract().shape() == ContainerShape.SET
&& plan.targetContract().mutability() == Mutability.MUTABLE
&& plan.targetContract().orderRequirement() == OrderRequirement.ENCOUNTER
&& plan.targetContract().uniquenessRequirement()
== UniquenessRequirement.REQUIRED;
}
private static CollectedAst collect(
org.eclipse.jdt.core.dom.CompilationUnit root,
String bindingKey) {
List<SimpleName> references= new ArrayList<>();
List<GuardedAdd> guards= new ArrayList<>();
VariableDeclarationFragment[] declaration= { null };
root.accept(new ASTVisitor() {
@Override
public boolean visit(SimpleName name) {
IVariableBinding binding= variableBinding(name.resolveBinding());
if (binding == null
|| !bindingKey.equals(binding.getVariableDeclaration().getKey())) {
return true;
}
references.add(name);
if (name.getParent() instanceof VariableDeclarationFragment fragment
&& fragment.getName() == name) {
declaration[0]= fragment;
}
return true;
}
@Override
public boolean visit(IfStatement statement) {
UniqueSequencePattern.match(statement, bindingKey).ifPresent(guards::add);
return true;
}
});
return new CollectedAst(declaration[0], references, guards);
}
private static boolean isSupportedSourceType(ITypeBinding type) {
if (type == null) {
return false;
}
String name= type.getErasure().getQualifiedName();
return LIST.equals(name) || ARRAY_LIST.equals(name);
}
private static boolean hasStableHash(ParameterizedType declarationType) {
Type elementType= (Type) declarationType.typeArguments().get(0);
ITypeBinding binding= elementType.resolveBinding();
return binding != null
&& (binding.isEnum()
|| STRING.equals(binding.getErasure().getQualifiedName()));
}
private static boolean isEmptyArrayList(ClassInstanceCreation creation) {
ITypeBinding type= creation.resolveTypeBinding();
return type != null
&& ARRAY_LIST.equals(type.getErasure().getQualifiedName())
&& creation.arguments().isEmpty()
&& creation.getAnonymousClassDeclaration() == null;
}
private static boolean isReadOnlyCollectionQuery(MethodInvocation invocation) {
String method= invocation.getName().getIdentifier();
return ("size".equals(method) || "isEmpty".equals(method)) //$NON-NLS-1$ //$NON-NLS-2$
&& invocation.arguments().isEmpty();
}
private static int editCount(
UniqueSequenceLocalRewritePlan plan,
EditKind kind) {
return (int) plan.edits().stream()
.filter(edit -> edit.kind() == kind)
.count();
}
private static IVariableBinding variableBinding(IBinding binding) {
return binding instanceof IVariableBinding variable ? variable : null;
}
private static CoreException stale(ICompilationUnit unit, String reason) {
return new CoreException(new Status(
IStatus.ERROR,
PLUGIN_ID,
"Stale unique-sequence rewrite plan for " //$NON-NLS-1$
+ unit.getElementName() + ": " + reason)); //$NON-NLS-1$
}
static record ResolvedPlan(
UniqueSequenceLocalRewritePlan plan,
VariableDeclarationStatement declaration,
VariableDeclarationFragment fragment,
ParameterizedType declarationType,
ClassInstanceCreation initializer,
List<GuardedAdd> guards) {
ResolvedPlan {
guards= List.copyOf(guards);
}
}
private record CollectedAst(
VariableDeclarationFragment declaration,
List<SimpleName> references,
List<GuardedAdd> guards) {
CollectedAst {
references= List.copyOf(references);
guards= List.copyOf(guards);
}
}
}