ContainerFlowComponent.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;
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.FlowNode;
/**
* One canonical value-flow component assembled from several compilation-unit-local
* {@link ContainerFlowGraph} fragments.
*
* <p>Unlike a local fragment, every edge and diagnostic carries the compilation-unit
* handle needed to interpret its source range. The component remains immutable and
* contains no AST nodes.</p>
*/
public record ContainerFlowComponent(
String rootNodeId,
List<FlowNode> nodes,
List<LocatedFlowEdge> edges,
ClosureStatus closureStatus,
List<LocatedFlowDiagnostic> diagnostics) {
public ContainerFlowComponent {
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$
validate(rootNodeId, nodes, edges);
}
/** Returns one canonical node by identifier. */
public Optional<FlowNode> node(String stableId) {
return nodes.stream().filter(candidate -> candidate.stableId().equals(stableId)).findFirst();
}
/** Returns outgoing edges in deterministic assembly order. */
public List<LocatedFlowEdge> outgoing(String stableId) {
return edges.stream().filter(edge -> edge.sourceNodeId().equals(stableId)).toList();
}
/** One flow transfer together with the source unit containing the expression. */
public record LocatedFlowEdge(
String compilationUnitHandle,
String sourceNodeId,
String targetNodeId,
EdgeKind kind,
int sourceStart,
int sourceLength) {
public LocatedFlowEdge {
compilationUnitHandle= requiredText(compilationUnitHandle, "compilationUnitHandle"); //$NON-NLS-1$
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 closure or rejection explanation together with its source unit. */
public record LocatedFlowDiagnostic(
String compilationUnitHandle,
DiagnosticKind kind,
String message,
int sourceStart,
int sourceLength) {
public LocatedFlowDiagnostic {
compilationUnitHandle= requiredText(compilationUnitHandle, "compilationUnitHandle"); //$NON-NLS-1$
Objects.requireNonNull(kind, "kind"); //$NON-NLS-1$
message= requiredText(message, "message"); //$NON-NLS-1$
validateRange(sourceStart, sourceLength);
}
}
private static void validate(
String rootNodeId,
List<FlowNode> nodes,
List<LocatedFlowEdge> edges) {
Set<String> identifiers= new HashSet<>();
for (FlowNode node : nodes) {
if (!identifiers.add(node.stableId())) {
throw new IllegalArgumentException("Duplicate component node id: " + node.stableId()); //$NON-NLS-1$
}
}
if (!identifiers.contains(rootNodeId)) {
throw new IllegalArgumentException("Component root is not present in node list"); //$NON-NLS-1$
}
for (LocatedFlowEdge edge : edges) {
if (!identifiers.contains(edge.sourceNodeId())
|| !identifiers.contains(edge.targetNodeId())) {
throw new IllegalArgumentException(
"Component edge references an unknown node: " + edge); //$NON-NLS-1$
}
}
}
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 void validateRange(int sourceStart, int sourceLength) {
if (sourceStart < 0 || sourceLength < 0) {
throw new IllegalArgumentException("Source range must not be negative"); //$NON-NLS-1$
}
}
}