ContainerParameterRewriteResolver.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.HashSet;
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.IJavaElement;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.ArrayType;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
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.ContainerShape;
import org.sandbox.jdt.container.api.TargetContainerContract.Mutability;
/** Binding-based, fail-closed resolution of a closed-source parameter rewrite. */
final class ContainerParameterRewriteResolver {
private static final String PLUGIN_ID= "sandbox_common"; //$NON-NLS-1$
private static final String LIST= "java.util.List"; //$NON-NLS-1$
private ContainerParameterRewriteResolver() {
}
static ResolvedPlan resolve(
ICompilationUnit unit,
org.eclipse.jdt.core.dom.CompilationUnit root,
ContainerParameterRewritePlan 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$
}
MethodDeclaration method= findMethod(unit, root, plan.methodJavaElementHandle());
if (plan.parameterIndex() >= method.parameters().size()) {
throw stale(unit, "parameter index changed"); //$NON-NLS-1$
}
SingleVariableDeclaration parameter=
(SingleVariableDeclaration) method.parameters().get(plan.parameterIndex());
if (parameter.isVarargs()
|| !parameter.extraDimensions().isEmpty()
|| !(parameter.getType() instanceof ArrayType arrayType)
|| arrayType.dimensions().size() != 1
|| !isReferenceComponent(arrayType)) {
throw stale(unit, "parameter is no longer a one-dimensional reference array"); //$NON-NLS-1$
}
IVariableBinding parameterBinding= parameter.resolveBinding();
if (parameterBinding == null
|| !plan.parameterBindingKey().equals(
parameterBinding.getVariableDeclaration().getKey())) {
throw stale(unit, "parameter binding changed"); //$NON-NLS-1$
}
List<SimpleName> references= matchingReferences(method, plan.parameterBindingKey());
List<ResolvedLength> lengths= new ArrayList<>();
List<Expression> encounterIterations= new ArrayList<>();
for (SimpleName name : references) {
if (parameter.getName() == name) {
continue;
}
if (crossesExecutableBoundary(name, method)) {
throw stale(unit, "parameter is captured across an executable boundary"); //$NON-NLS-1$
}
Expression reference= completeReferenceExpression(name);
ResolvedLength length= lengthRead(reference);
if (length != null) {
lengths.add(length);
continue;
}
if (reference.getParent() instanceof EnhancedForStatement enhanced
&& enhanced.getExpression() == reference) {
encounterIterations.add(reference);
continue;
}
throw stale(unit, "unexpected parameter use at source offset " //$NON-NLS-1$
+ name.getStartPosition());
}
verifyEditRanges(
unit,
plan,
EditKind.REPLACE_LENGTH_WITH_SIZE,
lengths.stream().map(ResolvedLength::expression).toList(),
"array length occurrence count or source ranges changed"); //$NON-NLS-1$
verifyEditRanges(
unit,
plan,
EditKind.VERIFY_ENCOUNTER_ITERATION,
encounterIterations,
"encounter iteration occurrence count or source ranges changed"); //$NON-NLS-1$
lengths.sort(Comparator.comparingInt(length -> length.expression().getStartPosition()));
return new ResolvedPlan(plan, method, parameter, arrayType, lengths);
}
private static MethodDeclaration findMethod(
ICompilationUnit unit,
org.eclipse.jdt.core.dom.CompilationUnit root,
String methodHandle) throws CoreException {
List<MethodDeclaration> matches= new ArrayList<>();
root.accept(new ASTVisitor() {
@Override
public boolean visit(MethodDeclaration method) {
if (methodHandle.equals(javaElementHandle(method.resolveBinding()))) {
matches.add(method);
}
return true;
}
});
if (matches.size() != 1) {
throw stale(unit, "exact method declaration could not be resolved"); //$NON-NLS-1$
}
return matches.get(0);
}
private static List<SimpleName> matchingReferences(
MethodDeclaration method,
String bindingKey) {
List<SimpleName> references= new ArrayList<>();
method.accept(new ASTVisitor() {
@Override
public boolean visit(SimpleName name) {
if (name.resolveBinding() instanceof IVariableBinding binding
&& bindingKey.equals(binding.getVariableDeclaration().getKey())) {
references.add(name);
}
return true;
}
});
return references;
}
private static boolean crossesExecutableBoundary(
ASTNode reference,
MethodDeclaration expectedMethod) {
for (ASTNode current= reference.getParent(); current != null;
current= current.getParent()) {
if (current instanceof LambdaExpression
|| current instanceof AnonymousClassDeclaration
|| current instanceof AbstractTypeDeclaration) {
return true;
}
if (current instanceof MethodDeclaration method) {
return method != expectedMethod;
}
}
return true;
}
private static Expression completeReferenceExpression(SimpleName name) {
Expression reference= name;
while (reference.getParent() instanceof ParenthesizedExpression parenthesized) {
reference= parenthesized;
}
return reference;
}
private static ResolvedLength lengthRead(Expression reference) {
ASTNode parent= reference.getParent();
if (parent instanceof QualifiedName qualified
&& qualified.getQualifier() == reference
&& "length".equals(qualified.getName().getIdentifier())) { //$NON-NLS-1$
return new ResolvedLength(qualified, reference);
}
if (parent instanceof FieldAccess fieldAccess
&& fieldAccess.getExpression() == reference
&& "length".equals(fieldAccess.getName().getIdentifier())) { //$NON-NLS-1$
return new ResolvedLength(fieldAccess, reference);
}
return null;
}
private static void verifyEditRanges(
ICompilationUnit unit,
ContainerParameterRewritePlan plan,
EditKind kind,
List<? extends ASTNode> resolvedNodes,
String staleMessage) throws CoreException {
List<ContainerParameterRewritePlan.ParameterEdit> planned= plan.edits().stream()
.filter(edit -> edit.kind() == kind)
.toList();
Set<SourceRange> expected= HashSet.newHashSet(planned.size());
for (ContainerParameterRewritePlan.ParameterEdit edit : planned) {
expected.add(new SourceRange(edit.sourceStart(), edit.sourceLength()));
}
Set<SourceRange> actual= HashSet.newHashSet(resolvedNodes.size());
for (ASTNode node : resolvedNodes) {
actual.add(new SourceRange(node.getStartPosition(), node.getLength()));
}
if (expected.size() != planned.size()
|| actual.size() != resolvedNodes.size()
|| !expected.equals(actual)) {
throw stale(unit, staleMessage);
}
}
private static boolean isReferenceComponent(ArrayType arrayType) {
ITypeBinding component= arrayType.getElementType().resolveBinding();
return component != null && !component.isPrimitive();
}
private static boolean isExpectedTarget(ContainerParameterRewritePlan plan) {
return LIST.equals(plan.targetInterfaceType())
&& plan.targetContract().shape() == ContainerShape.LIST
&& plan.targetContract().mutability() == Mutability.MUTABLE;
}
private static String javaElementHandle(IMethodBinding binding) {
IJavaElement element= binding == null
? null : binding.getMethodDeclaration().getJavaElement();
return element == null ? "" : element.getHandleIdentifier(); //$NON-NLS-1$
}
private static CoreException stale(ICompilationUnit unit, String reason) {
return new CoreException(new Status(
IStatus.ERROR,
PLUGIN_ID,
"Container parameter rewrite plan is stale for " //$NON-NLS-1$
+ unit.getElementName() + ": " + reason)); //$NON-NLS-1$
}
static record ResolvedPlan(
ContainerParameterRewritePlan plan,
MethodDeclaration method,
SingleVariableDeclaration parameter,
ArrayType arrayType,
List<ResolvedLength> lengths) {
ResolvedPlan {
Objects.requireNonNull(plan, "plan"); //$NON-NLS-1$
Objects.requireNonNull(method, "method"); //$NON-NLS-1$
Objects.requireNonNull(parameter, "parameter"); //$NON-NLS-1$
Objects.requireNonNull(arrayType, "arrayType"); //$NON-NLS-1$
lengths= List.copyOf(Objects.requireNonNull(lengths, "lengths")); //$NON-NLS-1$
}
}
static record ResolvedLength(Expression expression, Expression arrayExpression) {
ResolvedLength {
Objects.requireNonNull(expression, "expression"); //$NON-NLS-1$
Objects.requireNonNull(arrayExpression, "arrayExpression"); //$NON-NLS-1$
}
}
private record SourceRange(int start, int length) {
}
}