ContainerFlowGraphAccumulator.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 java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.dom.ASTNode;
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.Modifier;
import org.eclipse.jdt.core.dom.SimpleName;
import org.sandbox.jdt.container.analysis.ContainerFlowIndex.BindingInfo;
import org.sandbox.jdt.container.api.ContainerFlowGraph;
import org.sandbox.jdt.container.api.ContainerFlowGraph.ClosureStatus;
import org.sandbox.jdt.container.api.ContainerFlowGraph.DiagnosticKind;
import org.sandbox.jdt.container.api.ContainerFlowGraph.EdgeKind;
import org.sandbox.jdt.container.api.ContainerFlowGraph.FlowDiagnostic;
import org.sandbox.jdt.container.api.ContainerFlowGraph.FlowEdge;
import org.sandbox.jdt.container.api.ContainerFlowGraph.FlowNode;
import org.sandbox.jdt.container.api.ContainerFlowGraph.NodeKind;
import org.sandbox.jdt.container.api.ContainerUsageProfile;
/** Mutable graph assembly used only during one flow-analysis pass. */
final class ContainerFlowGraphAccumulator {
private final ContainerUsageProfile profile;
private final String compilationUnitHandle;
private final Map<String, FlowNode> nodes= new LinkedHashMap<>();
private final List<FlowEdge> edges= new ArrayList<>();
private final Set<String> edgeKeys= new LinkedHashSet<>();
private final List<FlowDiagnostic> diagnostics= new ArrayList<>();
private final Set<String> diagnosticKeys= new LinkedHashSet<>();
private ClosureStatus closureStatus= ClosureStatus.LOCAL_CLOSED;
private String rootNodeId;
ContainerFlowGraphAccumulator(
ContainerUsageProfile profile,
String compilationUnitHandle) {
this.profile= profile;
this.compilationUnitHandle= compilationUnitHandle;
}
FlowNode ensureVariableNode(BindingInfo info) {
IVariableBinding binding= info.binding();
String stableId= variableNodeId(binding.getKey());
SimpleName anchor= info.anchor();
int start= anchor == null ? profile.identity().sourceStart() : anchor.getStartPosition();
int length= anchor == null ? profile.identity().sourceLength() : anchor.getLength();
FlowNode node= nodes.computeIfAbsent(stableId, ignored -> new FlowNode(
stableId,
nodeKind(binding),
binding.getKey(),
ownerKey(binding),
info.compilationUnitHandle(),
javaElementHandle(binding),
info.signatureIndex(),
true,
start,
length));
if (rootNodeId == null && binding.getKey().equals(profile.identity().bindingKey())) {
rootNodeId= stableId;
}
return node;
}
FlowNode ensureReturnNode(
String methodKey,
String methodElementHandle,
String unitHandle,
ASTNode anchor) {
String stableId= "return:" + methodKey; //$NON-NLS-1$
return nodes.computeIfAbsent(stableId, ignored -> new FlowNode(
stableId,
NodeKind.RETURN_POSITION,
"", //$NON-NLS-1$
methodKey,
unitHandle,
methodElementHandle,
-1,
true,
anchor.getStartPosition(),
anchor.getLength()));
}
FlowNode ensureReturnNode(String methodKey, String unitHandle, ASTNode anchor) {
return ensureReturnNode(methodKey, "", unitHandle, anchor); //$NON-NLS-1$
}
void addParameterBoundary(
FlowNode sourceNode,
IMethodBinding method,
int parameterIndex,
ASTNode anchor) {
ITypeBinding declaringClass= method.getDeclaringClass();
boolean sourceType= declaringClass != null && declaringClass.isFromSource();
String methodKey= methodKey(method);
String stableId= "parameter:" + methodKey + ':' + parameterIndex; //$NON-NLS-1$
FlowNode target= nodes.computeIfAbsent(stableId, ignored -> new FlowNode(
stableId,
sourceType ? NodeKind.PARAMETER : NodeKind.EXTERNAL_PARAMETER,
"", //$NON-NLS-1$
methodKey,
"", //$NON-NLS-1$
javaElementHandle(method),
parameterIndex,
false,
anchor.getStartPosition(),
anchor.getLength()));
addEdge(sourceNode, target, EdgeKind.ARGUMENT_TO_PARAMETER, anchor);
if (sourceType) {
requireScopeExpansion(
"Parameter declaration is in another source scope", anchor); //$NON-NLS-1$
} else {
raise(ClosureStatus.EXTERNAL_BOUNDARY);
addDiagnostic(
DiagnosticKind.EXTERNAL_OR_BINARY_TARGET,
"Container value reaches an external or binary method parameter", anchor); //$NON-NLS-1$
}
}
void addEdge(FlowNode source, FlowNode target, EdgeKind kind, ASTNode anchor) {
String edgeKey= source.stableId() + '|' + target.stableId() + '|'
+ kind + '|' + anchor.getStartPosition();
if (edgeKeys.add(edgeKey)) {
edges.add(new FlowEdge(
source.stableId(),
target.stableId(),
kind,
anchor.getStartPosition(),
anchor.getLength()));
}
}
void classifySignatureExposure(BindingInfo info) {
IVariableBinding binding= info.binding();
if (binding.isParameter()) {
requireScopeExpansion(
"Method parameter requires caller and override discovery", info.anchor()); //$NON-NLS-1$
} else if (binding.isField() && !Modifier.isPrivate(binding.getModifiers())) {
requireScopeExpansion(
"Non-private field requires project-wide reference discovery", info.anchor()); //$NON-NLS-1$
}
}
void requireScopeExpansion(String message, ASTNode anchor) {
raise(ClosureStatus.REQUIRES_SCOPE_EXPANSION);
addDiagnostic(DiagnosticKind.SCOPE_EXPANSION_REQUIRED, message, anchor);
}
void unresolvedBinding(String bindingKey, int start, int length) {
raise(ClosureStatus.REJECTED);
String suffix= bindingKey == null || bindingKey.isBlank()
? "" : ": " + bindingKey; //$NON-NLS-1$ //$NON-NLS-2$
addDiagnostic(
DiagnosticKind.UNRESOLVED_BINDING,
"A required flow binding could not be resolved" + suffix, //$NON-NLS-1$
start,
length);
}
void unclassifiedFlow(ASTNode anchor) {
raise(ClosureStatus.REJECTED);
addDiagnostic(
DiagnosticKind.UNCLASSIFIED_FLOW,
"Container flow reaches an unsupported expression shape", anchor); //$NON-NLS-1$
}
void addUnresolvedRoot() {
String stableId= "unknown:" + profile.identity().stableId(); //$NON-NLS-1$
rootNodeId= stableId;
nodes.put(stableId, new FlowNode(
stableId,
NodeKind.UNKNOWN_BOUNDARY,
profile.identity().bindingKey(),
"", //$NON-NLS-1$
compilationUnitHandle,
"", //$NON-NLS-1$
-1,
false,
profile.identity().sourceStart(),
profile.identity().sourceLength()));
unresolvedBinding(
profile.identity().bindingKey(),
profile.identity().sourceStart(),
profile.identity().sourceLength());
}
ContainerFlowGraph toGraph() {
return new ContainerFlowGraph(
rootNodeId,
List.copyOf(nodes.values()),
edges,
closureStatus,
diagnostics);
}
private void addDiagnostic(DiagnosticKind kind, String message, ASTNode anchor) {
if (anchor == null) {
addDiagnostic(
kind,
message,
profile.identity().sourceStart(),
profile.identity().sourceLength());
} else {
addDiagnostic(kind, message, anchor.getStartPosition(), anchor.getLength());
}
}
private void addDiagnostic(
DiagnosticKind kind,
String message,
int start,
int length) {
String key= kind + "|" + message + '|' + start; //$NON-NLS-1$
if (diagnosticKeys.add(key)) {
diagnostics.add(new FlowDiagnostic(kind, message, start, length));
}
}
private void raise(ClosureStatus candidate) {
if (rank(candidate) > rank(closureStatus)) {
closureStatus= candidate;
}
}
private static NodeKind nodeKind(IVariableBinding binding) {
if (binding.isField()) {
return NodeKind.FIELD;
}
if (binding.isParameter()) {
return NodeKind.PARAMETER;
}
return NodeKind.LOCAL_VARIABLE;
}
private static String ownerKey(IVariableBinding binding) {
IMethodBinding declaringMethod= binding.getDeclaringMethod();
if (declaringMethod != null) {
return methodKey(declaringMethod);
}
ITypeBinding declaringClass= binding.getDeclaringClass();
return declaringClass == null ? "" : declaringClass.getTypeDeclaration().getKey(); //$NON-NLS-1$
}
private static String methodKey(IMethodBinding method) {
String key= method.getMethodDeclaration().getKey();
if (key != null && !key.isBlank()) {
return key;
}
ITypeBinding owner= method.getDeclaringClass();
String ownerName= owner == null ? "" : owner.getErasure().getQualifiedName(); //$NON-NLS-1$
return ownerName + '#' + method.getName() + '/' + method.getParameterTypes().length;
}
private static String javaElementHandle(IVariableBinding binding) {
if (binding.isParameter() && binding.getDeclaringMethod() != null) {
return javaElementHandle(binding.getDeclaringMethod());
}
return javaElementHandle(binding.getJavaElement());
}
private static String javaElementHandle(IMethodBinding binding) {
return javaElementHandle(binding.getMethodDeclaration().getJavaElement());
}
private static String javaElementHandle(IJavaElement element) {
return element == null ? "" : element.getHandleIdentifier(); //$NON-NLS-1$
}
private static String variableNodeId(String bindingKey) {
return "variable:" + bindingKey; //$NON-NLS-1$
}
private static int rank(ClosureStatus status) {
return switch (status) {
case LOCAL_CLOSED -> 0;
case REQUIRES_SCOPE_EXPANSION -> 1;
case EXTERNAL_BOUNDARY -> 2;
case REJECTED -> 3;
};
}
}