ContainerFlowContinuationLinker.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.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.sandbox.jdt.container.api.ContainerFlowComponent;
import org.sandbox.jdt.container.api.ContainerFlowComponent.LocatedFlowDiagnostic;
import org.sandbox.jdt.container.api.ContainerFlowComponent.LocatedFlowEdge;
import org.sandbox.jdt.container.api.ContainerFlowContinuationPlan;
import org.sandbox.jdt.container.api.ContainerFlowContinuationPlan.ContinuationKind;
import org.sandbox.jdt.container.api.ContainerFlowContinuationPlan.ContinuationRoot;
import org.sandbox.jdt.container.api.ContainerFlowContinuationPlan.Relationship;
import org.sandbox.jdt.container.api.ContainerFlowGraph.ClosureStatus;
import org.sandbox.jdt.container.api.ContainerFlowGraph.DiagnosticKind;
import org.sandbox.jdt.container.api.ContainerFlowGraph.FlowNode;
import org.sandbox.jdt.container.api.ContainerFlowGraph.NodeKind;
import org.sandbox.jdt.container.api.ContainerFlowSearchPlan.SearchKind;
import org.sandbox.jdt.container.api.ResolvedContainerFlowSearchPlan;
import org.sandbox.jdt.container.api.ResolvedContainerFlowSearchPlan.ResolvedSearchTarget;
/**
* Connects newly assembled local continuation roots to the canonical boundary nodes
* that initiated workspace scope expansion.
*
* <p>The linker never parses stable identifiers. Parameter and return positions are
* selected from the exact resolved search target, using the Java-model handle and
* signature index retained by the search layer.</p>
*/
public final class ContainerFlowContinuationLinker {
/** Links all supported continuation roots and recomputes component closure. */
public ContainerFlowComponent link(
ContainerFlowComponent component,
ContainerFlowContinuationPlan continuations,
ResolvedContainerFlowSearchPlan resolvedPlan) {
Objects.requireNonNull(component, "component"); //$NON-NLS-1$
Objects.requireNonNull(continuations, "continuations"); //$NON-NLS-1$
Objects.requireNonNull(resolvedPlan, "resolvedPlan"); //$NON-NLS-1$
Map<String, FlowNode> nodesById= new LinkedHashMap<>();
for (FlowNode node : component.nodes()) {
nodesById.put(node.stableId(), node);
}
List<LocatedFlowEdge> edges= new ArrayList<>(component.edges());
Set<String> edgeKeys= new LinkedHashSet<>();
component.edges().forEach(edge -> edgeKeys.add(edgeKey(edge)));
List<LocatedFlowDiagnostic> diagnostics= new ArrayList<>(component.diagnostics());
Set<String> diagnosticKeys= new LinkedHashSet<>();
component.diagnostics().forEach(diagnostic -> diagnosticKeys.add(diagnosticKey(diagnostic)));
boolean rejected= !continuations.complete();
for (ContinuationRoot root : continuations.roots()) {
Optional<FlowNode> continuationNode= findContinuationNode(component.nodes(), root);
Optional<ResolvedSearchTarget> target= findTarget(resolvedPlan, root);
if (continuationNode.isEmpty()) {
rejected= true;
addDiagnostic(diagnostics, diagnosticKeys, root,
"The continuation binding is not present in the assembled flow component."); //$NON-NLS-1$
continue;
}
if (target.isEmpty()) {
rejected= true;
addDiagnostic(diagnostics, diagnosticKeys, root,
"No exact resolved search target matches the continuation root."); //$NON-NLS-1$
continue;
}
Optional<FlowNode> boundaryNode= findBoundaryNode(
component.nodes(), nodesById, root, target.get(), continuationNode.get());
if (boundaryNode.isEmpty()) {
rejected= true;
addDiagnostic(diagnostics, diagnosticKeys, root,
"The canonical signature or field boundary is not present in the component."); //$NON-NLS-1$
continue;
}
if (root.relationship() == Relationship.SAME_NODE) {
if (!continuationNode.get().stableId().equals(boundaryNode.get().stableId())) {
rejected= true;
addDiagnostic(diagnostics, diagnosticKeys, root,
"A same-node continuation was not canonicalized to its boundary node."); //$NON-NLS-1$
}
continue;
}
String sourceId= root.relationship() == Relationship.ROOT_TO_BOUNDARY
? continuationNode.get().stableId()
: boundaryNode.get().stableId();
String targetId= root.relationship() == Relationship.ROOT_TO_BOUNDARY
? boundaryNode.get().stableId()
: continuationNode.get().stableId();
LocatedFlowEdge edge= new LocatedFlowEdge(
root.compilationUnitHandle(),
sourceId,
targetId,
root.transferKind(),
root.profile().identity().sourceStart(),
root.profile().identity().sourceLength());
if (edgeKeys.add(edgeKey(edge))) {
edges.add(edge);
}
}
ClosureStatus status= closureStatus(component, continuations, rejected);
if (status == ClosureStatus.LOCAL_CLOSED) {
diagnostics.removeIf(diagnostic ->
diagnostic.kind() == DiagnosticKind.SCOPE_EXPANSION_REQUIRED);
}
return new ContainerFlowComponent(
component.rootNodeId(),
component.nodes(),
edges,
status,
diagnostics);
}
private static Optional<FlowNode> findContinuationNode(
List<FlowNode> nodes,
ContinuationRoot root) {
List<FlowNode> matches= nodes.stream()
.filter(node -> node.bindingKey().equals(root.profile().identity().bindingKey()))
.filter(node -> node.compilationUnitHandle().equals(root.compilationUnitHandle()))
.toList();
return matches.size() == 1 ? Optional.of(matches.get(0)) : Optional.empty();
}
private static Optional<ResolvedSearchTarget> findTarget(
ResolvedContainerFlowSearchPlan plan,
ContinuationRoot root) {
return plan.targets().stream()
.filter(target -> target.sourceNodeId().equals(root.boundaryNodeId()))
.filter(target -> target.javaElementHandle().equals(root.exactTargetHandle()))
.filter(target -> targetMatchesKind(target, root.kind()))
.sorted(Comparator.comparing(target -> target.searchKind().ordinal()))
.findFirst();
}
private static boolean targetMatchesKind(
ResolvedSearchTarget target,
ContinuationKind kind) {
return switch (kind) {
case FIELD -> target.searchKind() == SearchKind.FIELD_REFERENCES;
case PARAMETER_DECLARATION ->
target.searchKind() != SearchKind.METHOD_CALLERS
&& target.signatureIndex() >= 0;
case CALL_ARGUMENT -> target.searchKind() == SearchKind.METHOD_CALLERS
&& target.signatureIndex() >= 0;
case RETURN_EXPRESSION -> target.searchKind() != SearchKind.METHOD_CALLERS
&& target.signatureIndex() < 0;
case RETURN_CONSUMER -> target.searchKind() == SearchKind.METHOD_CALLERS
&& target.signatureIndex() < 0;
};
}
private static Optional<FlowNode> findBoundaryNode(
List<FlowNode> nodes,
Map<String, FlowNode> nodesById,
ContinuationRoot root,
ResolvedSearchTarget target,
FlowNode continuationNode) {
if (root.relationship() == Relationship.SAME_NODE) {
return Optional.of(continuationNode);
}
FlowNode exactId= nodesById.get(root.boundaryNodeId());
if (exactId != null && compatibleBoundary(exactId, target)) {
return Optional.of(exactId);
}
List<FlowNode> matches= nodes.stream()
.filter(node -> node.javaElementHandle().equals(target.javaElementHandle()))
.filter(node -> compatibleBoundary(node, target))
.toList();
return matches.size() == 1 ? Optional.of(matches.get(0)) : Optional.empty();
}
private static boolean compatibleBoundary(
FlowNode node,
ResolvedSearchTarget target) {
if (target.searchKind() == SearchKind.FIELD_REFERENCES) {
return node.kind() == NodeKind.FIELD;
}
if (target.signatureIndex() >= 0) {
return (node.kind() == NodeKind.PARAMETER
|| node.kind() == NodeKind.EXTERNAL_PARAMETER)
&& node.signatureIndex() == target.signatureIndex();
}
return node.kind() == NodeKind.RETURN_POSITION;
}
private static ClosureStatus closureStatus(
ContainerFlowComponent component,
ContainerFlowContinuationPlan continuations,
boolean rejected) {
if (component.closureStatus() == ClosureStatus.REJECTED || rejected) {
return ClosureStatus.REJECTED;
}
if (component.closureStatus() == ClosureStatus.EXTERNAL_BOUNDARY) {
return ClosureStatus.EXTERNAL_BOUNDARY;
}
boolean unresolvedSourceBoundary= component.nodes().stream()
.anyMatch(node -> (node.kind() == NodeKind.PARAMETER
|| node.kind() == NodeKind.RETURN_POSITION)
&& !node.sourceResolved());
return unresolvedSourceBoundary || !continuations.complete()
? ClosureStatus.REQUIRES_SCOPE_EXPANSION
: ClosureStatus.LOCAL_CLOSED;
}
private static void addDiagnostic(
List<LocatedFlowDiagnostic> diagnostics,
Set<String> keys,
ContinuationRoot root,
String message) {
LocatedFlowDiagnostic diagnostic= new LocatedFlowDiagnostic(
root.compilationUnitHandle(),
DiagnosticKind.UNCLASSIFIED_FLOW,
message,
root.profile().identity().sourceStart(),
root.profile().identity().sourceLength());
if (keys.add(diagnosticKey(diagnostic))) {
diagnostics.add(diagnostic);
}
}
private static String edgeKey(LocatedFlowEdge edge) {
return edge.compilationUnitHandle() + '|' + edge.sourceNodeId() + '|'
+ edge.targetNodeId() + '|' + edge.kind() + '|' + edge.sourceStart();
}
private static String diagnosticKey(LocatedFlowDiagnostic diagnostic) {
return diagnostic.compilationUnitHandle() + '|' + diagnostic.kind() + '|'
+ diagnostic.message() + '|' + diagnostic.sourceStart();
}
}