ContainerFlowGraph.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.api;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
/**
* Stable, AST-free value-flow graph used to plan project-wide container migrations.
*
* @param rootNodeId stable identifier of the candidate root
* @param nodes graph nodes in deterministic discovery order
* @param edges graph edges in deterministic discovery order
* @param closureStatus current closure classification
* @param diagnostics source-backed boundary and rejection explanations
*/
public record ContainerFlowGraph(
String rootNodeId,
List<FlowNode> nodes,
List<FlowEdge> edges,
ClosureStatus closureStatus,
List<FlowDiagnostic> diagnostics) {
public ContainerFlowGraph {
rootNodeId= requiredText(rootNodeId, "rootNodeId"); //$NON-NLS-1$
nodes= List.copyOf(Objects.requireNonNull(nodes, "nodes")); //$NON-NLS-1$
edges= List.copyOf(Objects.requireNonNull(edges, "edges")); //$NON-NLS-1$
Objects.requireNonNull(closureStatus, "closureStatus"); //$NON-NLS-1$
diagnostics= List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics")); //$NON-NLS-1$
validateGraph(rootNodeId, nodes, edges);
}
/** Returns one node by stable identifier. */
public Optional<FlowNode> node(String stableId) {
return nodes.stream().filter(candidate -> candidate.stableId().equals(stableId)).findFirst();
}
/** Returns outgoing edges in deterministic discovery order. */
public List<FlowEdge> outgoing(String stableId) {
return edges.stream().filter(edge -> edge.sourceNodeId().equals(stableId)).toList();
}
private static void validateGraph(
String rootNodeId,
List<FlowNode> nodes,
List<FlowEdge> edges) {
Set<String> identifiers= new HashSet<>();
for (FlowNode node : nodes) {
if (!identifiers.add(node.stableId())) {
throw new IllegalArgumentException("Duplicate flow node id: " + node.stableId()); //$NON-NLS-1$
}
}
if (!identifiers.contains(rootNodeId)) {
throw new IllegalArgumentException("Flow root is not present in node list"); //$NON-NLS-1$
}
for (FlowEdge edge : edges) {
if (!identifiers.contains(edge.sourceNodeId())
|| !identifiers.contains(edge.targetNodeId())) {
throw new IllegalArgumentException(
"Flow edge references an unknown node: " + edge); //$NON-NLS-1$
}
}
}
/**
* One declaration, signature position or external boundary in the value flow.
*
* @param javaElementHandle stable Java-model handle for later workspace searches
* @param signatureIndex zero-based parameter index, or {@code -1} when the node
* is not a parameter position
*/
public record FlowNode(
String stableId,
NodeKind kind,
String bindingKey,
String ownerKey,
String compilationUnitHandle,
String javaElementHandle,
int signatureIndex,
boolean sourceResolved,
int sourceStart,
int sourceLength) {
public FlowNode {
stableId= requiredText(stableId, "stableId"); //$NON-NLS-1$
Objects.requireNonNull(kind, "kind"); //$NON-NLS-1$
bindingKey= optionalText(bindingKey);
ownerKey= optionalText(ownerKey);
compilationUnitHandle= optionalText(compilationUnitHandle);
javaElementHandle= optionalText(javaElementHandle);
if (signatureIndex < -1) {
throw new IllegalArgumentException("signatureIndex must be -1 or a parameter index"); //$NON-NLS-1$
}
validateRange(sourceStart, sourceLength);
}
/** Returns whether this node represents one concrete parameter position. */
public boolean isParameterPosition() {
return signatureIndex >= 0;
}
/** Returns whether the node can be resolved after the parser pass. */
public boolean hasJavaElementHandle() {
return !javaElementHandle.isEmpty();
}
}
/** One directed transfer of the represented container value. */
public record FlowEdge(
String sourceNodeId,
String targetNodeId,
EdgeKind kind,
int sourceStart,
int sourceLength) {
public FlowEdge {
sourceNodeId= requiredText(sourceNodeId, "sourceNodeId"); //$NON-NLS-1$
targetNodeId= requiredText(targetNodeId, "targetNodeId"); //$NON-NLS-1$
Objects.requireNonNull(kind, "kind"); //$NON-NLS-1$
validateRange(sourceStart, sourceLength);
}
}
/** One explanation why the currently analyzed scope is or is not closed. */
public record FlowDiagnostic(
DiagnosticKind kind,
String message,
int sourceStart,
int sourceLength) {
public FlowDiagnostic {
Objects.requireNonNull(kind, "kind"); //$NON-NLS-1$
message= requiredText(message, "message");
validateRange(sourceStart, sourceLength);
}
}
public enum NodeKind {
LOCAL_VARIABLE,
FIELD,
PARAMETER,
RETURN_POSITION,
EXTERNAL_PARAMETER,
UNKNOWN_BOUNDARY
}
public enum EdgeKind {
ASSIGNMENT,
INITIALIZER,
ARGUMENT_TO_PARAMETER,
RETURN_TO_METHOD
}
public enum ClosureStatus {
LOCAL_CLOSED,
REQUIRES_SCOPE_EXPANSION,
EXTERNAL_BOUNDARY,
REJECTED
}
public enum DiagnosticKind {
SCOPE_EXPANSION_REQUIRED,
EXTERNAL_OR_BINARY_TARGET,
UNRESOLVED_BINDING,
UNCLASSIFIED_FLOW
}
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 String optionalText(String value) {
return value == null ? "" : value.strip(); //$NON-NLS-1$
}
private static void validateRange(int sourceStart, int sourceLength) {
if (sourceStart < 0 || sourceLength < 0) {
throw new IllegalArgumentException("Source range must not be negative"); //$NON-NLS-1$
}
}
}