ContainerFlowContinuationDetector.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.container.analysis;
import static org.sandbox.jdt.container.analysis.ContainerAstFacts.variableBinding;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ConstructorInvocation;
import org.eclipse.jdt.core.dom.CreationReference;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionMethodReference;
import org.eclipse.jdt.core.dom.ExpressionStatement;
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.MethodInvocation;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.SuperConstructorInvocation;
import org.eclipse.jdt.core.dom.SuperMethodReference;
import org.eclipse.jdt.core.dom.TypeMethodReference;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.sandbox.jdt.container.api.ContainerFlowContinuationPlan;
import org.sandbox.jdt.container.api.ContainerFlowContinuationPlan.ContinuationDiagnostic;
import org.sandbox.jdt.container.api.ContainerFlowContinuationPlan.ContinuationKind;
import org.sandbox.jdt.container.api.ContainerFlowContinuationPlan.ContinuationRoot;
import org.sandbox.jdt.container.api.ContainerFlowContinuationPlan.DiagnosticKind;
import org.sandbox.jdt.container.api.ContainerFlowContinuationPlan.Relationship;
import org.sandbox.jdt.container.api.ContainerFlowGraph.EdgeKind;
import org.sandbox.jdt.container.api.ContainerFlowSearchPlan.SearchKind;
import org.sandbox.jdt.container.api.ContainerShape;
import org.sandbox.jdt.container.api.ContainerUsageProfile;
import org.sandbox.jdt.container.api.ContainerUsageProfile.AccessProfile;
import org.sandbox.jdt.container.api.ContainerUsageProfile.AliasingContract;
import org.sandbox.jdt.container.api.ContainerUsageProfile.AnalysisCompleteness;
import org.sandbox.jdt.container.api.ContainerUsageProfile.ConcurrencyProfile;
import org.sandbox.jdt.container.api.ContainerUsageProfile.ContainerIdentity;
import org.sandbox.jdt.container.api.ContainerUsageProfile.ElementDomain;
import org.sandbox.jdt.container.api.ContainerUsageProfile.EscapeLevel;
import org.sandbox.jdt.container.api.ContainerUsageProfile.MutationLifecycle;
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.ResolvedContainerFlowSearchPlan;
import org.sandbox.jdt.container.api.ResolvedContainerFlowSearchPlan.ResolvedSearchTarget;
import org.sandbox.jdt.container.api.ResolvedContainerFlowSearchPlan.TargetKind;
import org.sandbox.jdt.container.api.UsageEvidence;
import org.sandbox.jdt.container.api.UsageEvidence.Kind;
import org.sandbox.jdt.internal.common.AstProcessing;
import org.sandbox.jdt.internal.common.ReferenceHolder;
/**
* Finds exact, binding-resolved flow roots in compilation units admitted by workspace
* scope expansion.
*
* <p>Only transparent transfers are followed: exact fields and parameters, variables
* passed as arguments, variables returned by a method, and variables directly
* initialized or assigned from a method result. More complex expressions remain
* explicit diagnostics until a dedicated semantic rule can prove them safe.</p>
*/
public final class ContainerFlowContinuationDetector {
/** Detects continuations using the compilation unit's Java-model handle. */
public ContainerFlowContinuationPlan detect(
CompilationUnit compilationUnit,
ResolvedContainerFlowSearchPlan resolvedPlan) {
return detect(compilationUnit, compilationUnitHandle(compilationUnit), resolvedPlan);
}
/** Detects continuations with an explicit stable compilation-unit handle. */
public ContainerFlowContinuationPlan detect(
CompilationUnit compilationUnit,
String compilationUnitHandle,
ResolvedContainerFlowSearchPlan resolvedPlan) {
Objects.requireNonNull(compilationUnit, "compilationUnit"); //$NON-NLS-1$
Objects.requireNonNull(resolvedPlan, "resolvedPlan"); //$NON-NLS-1$
TargetIndex targets= TargetIndex.create(resolvedPlan);
Accumulator result= new Accumulator(requiredText(
compilationUnitHandle, "compilationUnitHandle")); //$NON-NLS-1$
AstProcessing.independent(ReferenceHolder.<String, Object>create())
.on(SimpleName.class, (name, holder) -> {
inspectField(name, targets, result);
return true;
})
.on(MethodDeclaration.class, (method, holder) -> {
inspectMethodDeclaration(method, targets, result);
return true;
})
.on(MethodInvocation.class, (invocation, holder) -> {
inspectExpressionCall(invocation, invocation.resolveMethodBinding(),
invocation.arguments(), targets, result);
return true;
})
.on(ClassInstanceCreation.class, (creation, holder) -> {
inspectExpressionCall(creation, creation.resolveConstructorBinding(),
creation.arguments(), targets, result);
return true;
})
.on(ConstructorInvocation.class, (invocation, holder) -> {
inspectStatementCall(invocation, invocation.resolveConstructorBinding(),
invocation.arguments(), targets, result);
return true;
})
.on(SuperConstructorInvocation.class, (invocation, holder) -> {
inspectStatementCall(invocation, invocation.resolveConstructorBinding(),
invocation.arguments(), targets, result);
return true;
})
.on(ExpressionMethodReference.class, (reference, holder) -> {
inspectMethodReference(reference, reference.resolveMethodBinding(), targets, result);
return true;
})
.on(TypeMethodReference.class, (reference, holder) -> {
inspectMethodReference(reference, reference.resolveMethodBinding(), targets, result);
return true;
})
.on(SuperMethodReference.class, (reference, holder) -> {
inspectMethodReference(reference, reference.resolveMethodBinding(), targets, result);
return true;
})
.on(CreationReference.class, (reference, holder) -> {
inspectMethodReference(reference, reference.resolveMethodBinding(), targets, result);
return true;
})
.build(compilationUnit);
return result.toPlan();
}
private static void inspectField(
SimpleName name,
TargetIndex targets,
Accumulator result) {
if (!(name.resolveBinding() instanceof IVariableBinding binding)) {
return;
}
IVariableBinding declaration= binding.getVariableDeclaration();
if (!declaration.isField()) {
return;
}
String handle= javaElementHandle(declaration.getJavaElement());
for (ResolvedSearchTarget target : targets.fields(handle)) {
result.addRoot(target, ContinuationKind.FIELD, Relationship.SAME_NODE, null,
declaration, name,
"Continue container flow from the exact field binding."); //$NON-NLS-1$
}
}
private static void inspectMethodDeclaration(
MethodDeclaration method,
TargetIndex targets,
Accumulator result) {
String handle= javaElementHandle(method.resolveBinding());
for (ResolvedSearchTarget target : targets.methods(handle)) {
if (target.searchKind() == SearchKind.METHOD_CALLERS) {
continue;
}
if (target.signatureIndex() >= 0) {
inspectParameter(method, target, result);
} else {
inspectReturns(method, target, result);
}
}
}
private static void inspectParameter(
MethodDeclaration method,
ResolvedSearchTarget target,
Accumulator result) {
int index= target.signatureIndex();
if (index < 0 || index >= method.parameters().size()) {
result.addDiagnostic(DiagnosticKind.INVALID_SIGNATURE_INDEX, target, method,
"The resolved method does not contain parameter index " + index + '.'); //$NON-NLS-1$
return;
}
SingleVariableDeclaration parameter=
(SingleVariableDeclaration) method.parameters().get(index);
IVariableBinding binding= parameter.resolveBinding();
if (binding == null) {
result.addDiagnostic(DiagnosticKind.UNRESOLVED_BINDING, target, parameter,
"The requested method parameter binding could not be resolved."); //$NON-NLS-1$
return;
}
result.addRoot(target, ContinuationKind.PARAMETER_DECLARATION,
Relationship.SAME_NODE, null, binding.getVariableDeclaration(),
parameter.getName(),
"Continue container flow from the exact method parameter position."); //$NON-NLS-1$
}
private static void inspectReturns(
MethodDeclaration method,
ResolvedSearchTarget target,
Accumulator result) {
if (method.getBody() == null) {
return;
}
AstProcessing.independent(ReferenceHolder.<String, Object>create())
.on(ReturnStatement.class, (statement, holder) -> {
if (!belongsToMethod(statement, method)) {
return false;
}
Expression expression= statement.getExpression();
if (expression == null) {
return false;
}
Optional<IVariableBinding> returned= variableBinding(expression);
if (returned.isEmpty()) {
result.addDiagnostic(DiagnosticKind.UNSUPPORTED_RETURN_EXPRESSION,
target, expression,
"The method returns a non-variable expression that requires " //$NON-NLS-1$
+ "a dedicated semantic migration rule."); //$NON-NLS-1$
return false;
}
result.addRoot(target, ContinuationKind.RETURN_EXPRESSION,
Relationship.ROOT_TO_BOUNDARY, EdgeKind.RETURN_TO_METHOD,
returned.get().getVariableDeclaration(), expression,
"Continue container flow from a variable returned by the method."); //$NON-NLS-1$
return false;
})
.build(method.getBody());
}
private static boolean belongsToMethod(ReturnStatement statement, MethodDeclaration method) {
ASTNode current= statement.getParent();
while (current != null) {
if (current instanceof LambdaExpression) {
return false;
}
if (current instanceof MethodDeclaration declaration) {
return declaration == method;
}
current= current.getParent();
}
return false;
}
private static void inspectExpressionCall(
Expression invocation,
IMethodBinding binding,
List<?> arguments,
TargetIndex targets,
Accumulator result) {
for (ResolvedSearchTarget target : targets.methods(javaElementHandle(binding))) {
if (target.searchKind() != SearchKind.METHOD_CALLERS) {
continue;
}
if (target.signatureIndex() >= 0) {
inspectArguments(invocation, binding, arguments, target, result);
} else {
inspectReturnConsumer(invocation, target, result);
}
}
}
private static void inspectStatementCall(
ASTNode invocation,
IMethodBinding binding,
List<?> arguments,
TargetIndex targets,
Accumulator result) {
for (ResolvedSearchTarget target : targets.methods(javaElementHandle(binding))) {
if (target.searchKind() == SearchKind.METHOD_CALLERS
&& target.signatureIndex() >= 0) {
inspectArguments(invocation, binding, arguments, target, result);
}
}
}
private static void inspectArguments(
ASTNode invocation,
IMethodBinding binding,
List<?> arguments,
ResolvedSearchTarget target,
Accumulator result) {
if (binding == null) {
result.addDiagnostic(DiagnosticKind.UNRESOLVED_BINDING, target, invocation,
"The call binding could not be resolved."); //$NON-NLS-1$
return;
}
boolean matched= false;
for (int argumentIndex= 0; argumentIndex < arguments.size(); argumentIndex++) {
if (parameterIndex(binding, argumentIndex) != target.signatureIndex()) {
continue;
}
matched= true;
Expression argument= (Expression) arguments.get(argumentIndex);
Optional<IVariableBinding> variable= variableBinding(argument);
if (variable.isEmpty()) {
result.addDiagnostic(DiagnosticKind.UNSUPPORTED_ARGUMENT, target, argument,
"The call passes a non-variable argument that requires a " //$NON-NLS-1$
+ "dedicated semantic migration rule."); //$NON-NLS-1$
continue;
}
result.addRoot(target, ContinuationKind.CALL_ARGUMENT,
Relationship.ROOT_TO_BOUNDARY, EdgeKind.ARGUMENT_TO_PARAMETER,
variable.get().getVariableDeclaration(), argument,
"Continue container flow from the variable passed to the parameter."); //$NON-NLS-1$
}
if (!matched) {
result.addDiagnostic(DiagnosticKind.INVALID_SIGNATURE_INDEX, target, invocation,
"The call contains no argument for parameter index " //$NON-NLS-1$
+ target.signatureIndex() + '.');
}
}
private static void inspectReturnConsumer(
Expression invocation,
ResolvedSearchTarget target,
Accumulator result) {
Expression complete= transparentParent(invocation);
ASTNode parent= complete.getParent();
if (parent instanceof VariableDeclarationFragment fragment
&& fragment.getInitializer() == complete) {
IVariableBinding binding= fragment.resolveBinding();
if (binding == null) {
result.addDiagnostic(DiagnosticKind.UNRESOLVED_BINDING, target, fragment,
"The variable receiving the method result could not be resolved."); //$NON-NLS-1$
return;
}
result.addRoot(target, ContinuationKind.RETURN_CONSUMER,
Relationship.BOUNDARY_TO_ROOT, EdgeKind.INITIALIZER,
binding.getVariableDeclaration(), fragment.getName(),
"Continue container flow from the variable initialized by the result."); //$NON-NLS-1$
return;
}
if (parent instanceof Assignment assignment
&& assignment.getOperator() == Assignment.Operator.ASSIGN
&& assignment.getRightHandSide() == complete) {
Optional<IVariableBinding> binding= variableBinding(assignment.getLeftHandSide());
if (binding.isEmpty()) {
result.addDiagnostic(DiagnosticKind.UNRESOLVED_BINDING, target, assignment,
"The assignment target receiving the result could not be resolved."); //$NON-NLS-1$
return;
}
result.addRoot(target, ContinuationKind.RETURN_CONSUMER,
Relationship.BOUNDARY_TO_ROOT, EdgeKind.ASSIGNMENT,
binding.get().getVariableDeclaration(), assignment.getLeftHandSide(),
"Continue container flow from the variable assigned the method result."); //$NON-NLS-1$
return;
}
if (parent instanceof ExpressionStatement) {
return;
}
result.addDiagnostic(DiagnosticKind.UNSUPPORTED_RETURN_CONSUMER, target, complete,
"The method result is consumed by an unsupported expression shape."); //$NON-NLS-1$
}
private static void inspectMethodReference(
ASTNode reference,
IMethodBinding binding,
TargetIndex targets,
Accumulator result) {
for (ResolvedSearchTarget target : targets.methods(javaElementHandle(binding))) {
result.addDiagnostic(DiagnosticKind.METHOD_REFERENCE, target, reference,
"A method reference participates in the migrated signature and " //$NON-NLS-1$
+ "requires dedicated target-type analysis."); //$NON-NLS-1$
}
}
private static int parameterIndex(IMethodBinding method, int argumentIndex) {
int parameterCount= method.getParameterTypes().length;
if (method.isVarargs() && parameterCount > 0 && argumentIndex >= parameterCount - 1) {
return parameterCount - 1;
}
return argumentIndex;
}
private static Expression transparentParent(Expression expression) {
Expression current= expression;
while (current.getParent() instanceof ParenthesizedExpression parenthesized) {
current= parenthesized;
}
return current;
}
private static String compilationUnitHandle(CompilationUnit compilationUnit) {
IJavaElement element= compilationUnit.getJavaElement();
return element == null
? "in-memory-compilation-unit" //$NON-NLS-1$
: requiredText(element.getHandleIdentifier(), "compilationUnitHandle"); //$NON-NLS-1$
}
private static String javaElementHandle(IMethodBinding binding) {
return binding == null
? "" //$NON-NLS-1$
: javaElementHandle(binding.getMethodDeclaration().getJavaElement());
}
private static String javaElementHandle(IJavaElement element) {
return element == null || element.getHandleIdentifier() == null
? "" //$NON-NLS-1$
: element.getHandleIdentifier();
}
private static String requiredText(String value, String fieldName) {
String text= Objects.requireNonNull(value, fieldName).strip();
if (text.isEmpty()) {
throw new IllegalArgumentException(fieldName + " must not be empty"); //$NON-NLS-1$
}
return text;
}
private static final class TargetIndex {
private final Map<String, List<ResolvedSearchTarget>> fieldsByHandle;
private final Map<String, List<ResolvedSearchTarget>> methodsByHandle;
private TargetIndex(
Map<String, List<ResolvedSearchTarget>> fieldsByHandle,
Map<String, List<ResolvedSearchTarget>> methodsByHandle) {
this.fieldsByHandle= fieldsByHandle;
this.methodsByHandle= methodsByHandle;
}
static TargetIndex create(ResolvedContainerFlowSearchPlan plan) {
Map<String, List<ResolvedSearchTarget>> fields= new LinkedHashMap<>();
Map<String, List<ResolvedSearchTarget>> methods= new LinkedHashMap<>();
for (ResolvedSearchTarget target : plan.targets()) {
Map<String, List<ResolvedSearchTarget>> selected=
target.targetKind() == TargetKind.FIELD ? fields : methods;
selected.computeIfAbsent(target.javaElementHandle(), ignored -> new ArrayList<>())
.add(target);
}
return new TargetIndex(immutableLists(fields), immutableLists(methods));
}
List<ResolvedSearchTarget> fields(String handle) {
return fieldsByHandle.getOrDefault(handle, List.of());
}
List<ResolvedSearchTarget> methods(String handle) {
return methodsByHandle.getOrDefault(handle, List.of());
}
private static Map<String, List<ResolvedSearchTarget>> immutableLists(
Map<String, List<ResolvedSearchTarget>> source) {
Map<String, List<ResolvedSearchTarget>> result= new LinkedHashMap<>();
source.forEach((key, value) -> result.put(key, List.copyOf(value)));
return Map.copyOf(result);
}
}
private static final class Accumulator {
private final String compilationUnitHandle;
private final Map<String, ContinuationRoot> rootsByKey= new LinkedHashMap<>();
private final Map<String, ContinuationDiagnostic> diagnosticsByKey=
new LinkedHashMap<>();
Accumulator(String compilationUnitHandle) {
this.compilationUnitHandle= compilationUnitHandle;
}
void addRoot(
ResolvedSearchTarget target,
ContinuationKind kind,
Relationship relationship,
EdgeKind transferKind,
IVariableBinding binding,
ASTNode anchor,
String summary) {
IVariableBinding declaration= binding.getVariableDeclaration();
ITypeBinding type= declaration.getType();
if (type == null || !type.isArray()) {
addDiagnostic(DiagnosticKind.NON_ARRAY_VALUE, target, anchor,
"The resolved continuation value is not an array."); //$NON-NLS-1$
return;
}
String bindingKey= declaration.getKey();
if (bindingKey == null || bindingKey.isBlank()) {
addDiagnostic(DiagnosticKind.UNRESOLVED_BINDING, target, anchor,
"The continuation variable has no stable binding key."); //$NON-NLS-1$
return;
}
ContinuationRoot root= new ContinuationRoot(
target.sourceNodeId(), kind, relationship, transferKind,
compilationUnitHandle, target.javaElementHandle(),
profile(declaration, anchor, summary));
rootsByKey.putIfAbsent(root.stableKey(), root);
}
void addDiagnostic(
DiagnosticKind kind,
ResolvedSearchTarget target,
ASTNode node,
String message) {
ContinuationDiagnostic diagnostic= new ContinuationDiagnostic(
kind, compilationUnitHandle, target.javaElementHandle(), message,
node.getStartPosition(), node.getLength());
String key= kind + "|" + target.stableKey() + '|' + node.getStartPosition(); //$NON-NLS-1$
diagnosticsByKey.putIfAbsent(key, diagnostic);
}
ContainerFlowContinuationPlan toPlan() {
List<ContinuationRoot> roots= new ArrayList<>(rootsByKey.values());
roots.sort(Comparator
.comparingInt((ContinuationRoot root) -> root.profile().identity().sourceStart())
.thenComparing(ContinuationRoot::stableKey));
List<ContinuationDiagnostic> diagnostics=
new ArrayList<>(diagnosticsByKey.values());
diagnostics.sort(Comparator
.comparingInt(ContinuationDiagnostic::sourceStart)
.thenComparing(ContinuationDiagnostic::message));
return new ContainerFlowContinuationPlan(roots, diagnostics);
}
private static ContainerUsageProfile profile(
IVariableBinding binding,
ASTNode anchor,
String summary) {
ITypeBinding component= binding.getType().getComponentType();
ElementDomain domain= component == null
? ElementDomain.UNKNOWN
: component.isPrimitive()
? ElementDomain.PRIMITIVE
: component.isEnum() ? ElementDomain.ENUM : ElementDomain.REFERENCE;
EscapeLevel escape= binding.isField()
? EscapeLevel.FIELD
: binding.isParameter() ? EscapeLevel.METHOD_BOUNDARY : EscapeLevel.LOCAL;
return new ContainerUsageProfile(
new ContainerIdentity(binding.getKey(), binding.getName(),
anchor.getStartPosition(), anchor.getLength()),
ContainerShape.ARRAY,
domain,
new AccessProfile(false, false, false, false, false, false, false),
OrderRequirement.UNKNOWN,
UniquenessRequirement.UNKNOWN,
MutationLifecycle.UNKNOWN,
NullContract.UNKNOWN,
AliasingContract.UNKNOWN,
escape,
ConcurrencyProfile.unknown(),
AnalysisCompleteness.LOCAL_SEED,
List.of(new UsageEvidence(Kind.FLOW_CONTINUATION_ROOT, summary,
anchor.getStartPosition(), anchor.getLength())));
}
}
}