LocalContainerFlowGraphBuilder.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.ArrayDeque;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ArrayAccess;
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.EnhancedForStatement;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.IBinding;
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.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.QualifiedName;
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.SuperFieldAccess;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.sandbox.jdt.container.analysis.ContainerFlowIndex.BindingInfo;
import org.sandbox.jdt.container.analysis.ContainerFlowIndex.MethodInfo;
import org.sandbox.jdt.container.api.ContainerFlowGraph;
import org.sandbox.jdt.container.api.ContainerFlowGraph.EdgeKind;
import org.sandbox.jdt.container.api.ContainerFlowGraph.FlowNode;
import org.sandbox.jdt.container.api.ContainerUsageProfile;
/**
* Discovers value-flow edges reachable from one container binding inside a compilation
* unit.
*
* <p>The builder follows local variable aliases to a fixed point. Signature edges are
* represented even when their declarations are outside the current compilation unit;
* the closure status and diagnostics then instruct the multi-file planner to expand
* scope or stop at an external boundary.</p>
*
* <p>Index construction and graph accumulation are separate package-private
* components so this class remains focused on flow rules.</p>
*/
public final class LocalContainerFlowGraphBuilder {
/** Builds a local flow graph for the supplied candidate profile. */
public ContainerFlowGraph build(
CompilationUnit compilationUnit,
ContainerUsageProfile profile) {
Objects.requireNonNull(compilationUnit, "compilationUnit"); //$NON-NLS-1$
Objects.requireNonNull(profile, "profile"); //$NON-NLS-1$
ContainerFlowIndex index= ContainerFlowIndex.create(compilationUnit);
ContainerFlowGraphAccumulator graph=
new ContainerFlowGraphAccumulator(profile, index.compilationUnitHandle());
String rootBindingKey= profile.identity().bindingKey();
if (rootBindingKey.isBlank()) {
graph.addUnresolvedRoot();
return graph.toGraph();
}
BindingInfo root= index.bindings().get(rootBindingKey);
if (root == null) {
graph.addUnresolvedRoot();
return graph.toGraph();
}
Deque<String> pendingBindings= new ArrayDeque<>();
Set<String> visitedBindings= new LinkedHashSet<>();
pendingBindings.add(rootBindingKey);
graph.ensureVariableNode(root);
while (!pendingBindings.isEmpty()) {
String bindingKey= pendingBindings.removeFirst();
if (!visitedBindings.add(bindingKey)) {
continue;
}
BindingInfo source= index.bindings().get(bindingKey);
if (source == null) {
graph.unresolvedBinding(
bindingKey,
profile.identity().sourceStart(),
profile.identity().sourceLength());
continue;
}
FlowNode sourceNode= graph.ensureVariableNode(source);
graph.classifySignatureExposure(source);
for (SimpleName referenceName :
index.references().getOrDefault(bindingKey, List.of())) {
if (isDeclarationName(referenceName)) {
continue;
}
Expression reference= completeReferenceExpression(referenceName, bindingKey);
classifyReference(reference, sourceNode, index, graph, pendingBindings);
}
}
return graph.toGraph();
}
private static void classifyReference(
Expression reference,
FlowNode sourceNode,
ContainerFlowIndex index,
ContainerFlowGraphAccumulator graph,
Deque<String> pendingBindings) {
ASTNode parent= reference.getParent();
if (parent instanceof VariableDeclarationFragment fragment
&& fragment.getInitializer() == reference) {
connectVariable(
sourceNode,
fragment.resolveBinding(),
EdgeKind.INITIALIZER,
reference,
index,
graph,
pendingBindings);
} else if (parent instanceof Assignment assignment
&& assignment.getRightHandSide() == reference) {
connectExpression(
sourceNode,
assignment.getLeftHandSide(),
EdgeKind.ASSIGNMENT,
reference,
index,
graph,
pendingBindings);
} else if (parent instanceof MethodInvocation invocation
&& invocation.arguments().contains(reference)) {
if (!isRepresentationInternalCopy(invocation, reference)) {
connectArguments(
sourceNode,
invocation.resolveMethodBinding(),
invocation.arguments(),
reference,
index,
graph,
pendingBindings);
}
} else if (parent instanceof ClassInstanceCreation creation
&& creation.arguments().contains(reference)) {
connectArguments(
sourceNode,
creation.resolveConstructorBinding(),
creation.arguments(),
reference,
index,
graph,
pendingBindings);
} else if (parent instanceof ReturnStatement) {
connectReturn(sourceNode, reference, index, graph);
} else if (!isInternalNonFlowUse(reference, parent)) {
graph.unclassifiedFlow(reference);
}
}
private static void connectExpression(
FlowNode sourceNode,
Expression targetExpression,
EdgeKind edgeKind,
ASTNode evidence,
ContainerFlowIndex index,
ContainerFlowGraphAccumulator graph,
Deque<String> pendingBindings) {
Optional<IVariableBinding> target= variableBinding(targetExpression);
if (target.isPresent()) {
connectVariable(
sourceNode,
target.get(),
edgeKind,
evidence,
index,
graph,
pendingBindings);
} else {
graph.unclassifiedFlow(evidence);
}
}
private static void connectVariable(
FlowNode sourceNode,
IVariableBinding targetBinding,
EdgeKind edgeKind,
ASTNode evidence,
ContainerFlowIndex index,
ContainerFlowGraphAccumulator graph,
Deque<String> pendingBindings) {
if (targetBinding == null || targetBinding.getVariableDeclaration().getKey() == null) {
graph.unresolvedBinding(
"", evidence.getStartPosition(), evidence.getLength()); //$NON-NLS-1$
return;
}
String targetKey= targetBinding.getVariableDeclaration().getKey();
BindingInfo target= index.bindings().get(targetKey);
if (target == null) {
graph.unresolvedBinding(
targetKey, evidence.getStartPosition(), evidence.getLength());
return;
}
FlowNode targetNode= graph.ensureVariableNode(target);
graph.addEdge(sourceNode, targetNode, edgeKind, evidence);
pendingBindings.addLast(targetKey);
}
private static void connectArguments(
FlowNode sourceNode,
IMethodBinding invokedMethod,
List<?> arguments,
Expression reference,
ContainerFlowIndex index,
ContainerFlowGraphAccumulator graph,
Deque<String> pendingBindings) {
if (invokedMethod == null) {
graph.unresolvedBinding(
"", reference.getStartPosition(), reference.getLength()); //$NON-NLS-1$
return;
}
IMethodBinding declaration= invokedMethod.getMethodDeclaration();
String methodKey= declaration.getKey();
for (int argumentIndex= 0; argumentIndex < arguments.size(); argumentIndex++) {
if (arguments.get(argumentIndex) != reference) {
continue;
}
int parameterIndex= parameterIndex(declaration, argumentIndex);
MethodInfo localMethod= methodKey == null ? null : index.methods().get(methodKey);
if (localMethod != null && parameterIndex < localMethod.parameterBindingKeys().size()) {
String parameterKey= localMethod.parameterBindingKeys().get(parameterIndex);
BindingInfo parameter= index.bindings().get(parameterKey);
if (parameter != null) {
FlowNode parameterNode= graph.ensureVariableNode(parameter);
graph.addEdge(
sourceNode,
parameterNode,
EdgeKind.ARGUMENT_TO_PARAMETER,
reference);
graph.requireScopeExpansion(
"Method parameter signature participates in the container flow", //$NON-NLS-1$
reference);
pendingBindings.addLast(parameterKey);
continue;
}
}
graph.addParameterBoundary(
sourceNode,
declaration,
parameterIndex,
reference);
}
}
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 void connectReturn(
FlowNode sourceNode,
Expression reference,
ContainerFlowIndex index,
ContainerFlowGraphAccumulator graph) {
MethodDeclaration method= ancestor(reference, MethodDeclaration.class);
IMethodBinding methodBinding= method == null ? null : method.resolveBinding();
if (methodBinding == null) {
graph.unresolvedBinding(
"", reference.getStartPosition(), reference.getLength()); //$NON-NLS-1$
return;
}
String methodKey= methodBinding.getMethodDeclaration().getKey();
if (methodKey == null || methodKey.isBlank()) {
graph.unresolvedBinding(
"", reference.getStartPosition(), reference.getLength()); //$NON-NLS-1$
return;
}
FlowNode returnNode= graph.ensureReturnNode(
methodKey,
index.compilationUnitHandle(),
reference);
graph.addEdge(sourceNode, returnNode, EdgeKind.RETURN_TO_METHOD, reference);
graph.requireScopeExpansion(
"Method return type and all call sites participate in the container flow", //$NON-NLS-1$
reference);
}
private static boolean isRepresentationInternalCopy(
MethodInvocation invocation,
Expression reference) {
if (!"copyOf".equals(invocation.getName().getIdentifier()) //$NON-NLS-1$
|| invocation.arguments().isEmpty()
|| invocation.arguments().get(0) != reference) {
return false;
}
IMethodBinding binding= invocation.resolveMethodBinding();
return binding != null
&& binding.getDeclaringClass() != null
&& "java.util.Arrays".equals( //$NON-NLS-1$
binding.getDeclaringClass().getErasure().getQualifiedName());
}
private static boolean isInternalNonFlowUse(Expression reference, ASTNode parent) {
return parent instanceof ArrayAccess access && access.getArray() == reference
|| parent instanceof QualifiedName qualified
&& qualified.getQualifier() == reference
&& "length".equals(qualified.getName().getIdentifier()) //$NON-NLS-1$
|| parent instanceof FieldAccess fieldAccess
&& fieldAccess.getExpression() == reference
&& "length".equals(fieldAccess.getName().getIdentifier()) //$NON-NLS-1$
|| parent instanceof EnhancedForStatement enhancedFor
&& enhancedFor.getExpression() == reference
|| parent instanceof Assignment assignment
&& assignment.getLeftHandSide() == reference;
}
private static boolean isDeclarationName(SimpleName name) {
ASTNode parent= name.getParent();
return parent instanceof VariableDeclarationFragment fragment && fragment.getName() == name
|| parent instanceof SingleVariableDeclaration declaration
&& declaration.getName() == name;
}
private static Expression completeReferenceExpression(SimpleName name, String bindingKey) {
Expression reference= name;
ASTNode parent= reference.getParent();
if (parent instanceof QualifiedName qualified
&& qualified.getName() == reference
&& hasBindingKey(qualified.resolveBinding(), bindingKey)) {
reference= qualified;
} else if (parent instanceof FieldAccess fieldAccess
&& fieldAccess.getName() == reference
&& hasBindingKey(fieldAccess.resolveFieldBinding(), bindingKey)) {
reference= fieldAccess;
} else if (parent instanceof SuperFieldAccess superFieldAccess
&& superFieldAccess.getName() == reference
&& hasBindingKey(superFieldAccess.resolveFieldBinding(), bindingKey)) {
reference= superFieldAccess;
}
while (reference.getParent() instanceof ParenthesizedExpression parenthesized) {
reference= parenthesized;
}
return reference;
}
private static boolean hasBindingKey(IBinding binding, String bindingKey) {
return binding instanceof IVariableBinding variable
&& bindingKey.equals(variable.getVariableDeclaration().getKey());
}
private static <N extends ASTNode> N ancestor(ASTNode node, Class<N> type) {
ASTNode current= node.getParent();
while (current != null && !type.isInstance(current)) {
current= current.getParent();
}
return type.isInstance(current) ? type.cast(current) : null;
}
}