ContainerSignatureAtomicityPlanner.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.List;
import java.util.Map;
import java.util.Objects;
import org.sandbox.jdt.container.api.ContainerFlowComponent;
import org.sandbox.jdt.container.api.ContainerFlowGraph.ClosureStatus;
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.ContainerRecommendation;
import org.sandbox.jdt.container.api.ContainerSignatureMigrationPlan;
import org.sandbox.jdt.container.api.ContainerSignatureMigrationPlan.BridgeFeasibility;
import org.sandbox.jdt.container.api.ContainerSignatureMigrationPlan.DiagnosticKind;
import org.sandbox.jdt.container.api.ContainerSignatureMigrationPlan.PlanningStatus;
import org.sandbox.jdt.container.api.ContainerSignatureMigrationPlan.PositionKind;
import org.sandbox.jdt.container.api.ContainerSignatureMigrationPlan.SignatureAtomicityGroup;
import org.sandbox.jdt.container.api.ContainerSignatureMigrationPlan.SignatureDiagnostic;
import org.sandbox.jdt.container.api.ContainerSignatureMigrationPlan.SignatureMember;
import org.sandbox.jdt.container.api.ResolvedContainerFlowSearchPlan;
import org.sandbox.jdt.container.api.ResolvedContainerFlowSearchPlan.ResolvedSearchTarget;
/**
* Derives atomic method-signature groups from a closed container flow.
*
* <p>The ordinary planning entry point remains report-only and records Java-level
* coexistence constraints for compatibility-policy analysis. The explicit
* closed-source entry point marks only the currently executable single-parameter
* group as directly automatic.</p>
*/
public final class ContainerSignatureAtomicityPlanner {
/** Builds an immutable report-only signature plan for one closed flow. */
public ContainerSignatureMigrationPlan plan(
ContainerFlowComponent component,
ResolvedContainerFlowSearchPlan resolvedPlan,
ContainerRecommendation recommendation) {
return createPlan(
component, resolvedPlan, recommendation, PlanningStatus.REPORT_ONLY);
}
/**
* Builds an immutable direct-migration plan for the implemented closed-source
* parameter slice. No compatibility bridge is implied by this mode.
*/
public ContainerSignatureMigrationPlan planClosedSource(
ContainerFlowComponent component,
ResolvedContainerFlowSearchPlan resolvedPlan,
ContainerRecommendation recommendation) {
return createPlan(
component,
resolvedPlan,
recommendation,
PlanningStatus.CLOSED_SOURCE_AUTOMATIC);
}
private static ContainerSignatureMigrationPlan createPlan(
ContainerFlowComponent component,
ResolvedContainerFlowSearchPlan resolvedPlan,
ContainerRecommendation recommendation,
PlanningStatus completedStatus) {
Objects.requireNonNull(component, "component"); //$NON-NLS-1$
Objects.requireNonNull(resolvedPlan, "resolvedPlan"); //$NON-NLS-1$
Objects.requireNonNull(recommendation, "recommendation"); //$NON-NLS-1$
Objects.requireNonNull(completedStatus, "completedStatus"); //$NON-NLS-1$
if (completedStatus != PlanningStatus.REPORT_ONLY
&& completedStatus != PlanningStatus.CLOSED_SOURCE_AUTOMATIC) {
throw new IllegalArgumentException("Unsupported completed signature status"); //$NON-NLS-1$
}
List<SignatureDiagnostic> diagnostics= new ArrayList<>();
if (component.closureStatus() != ClosureStatus.LOCAL_CLOSED) {
diagnostics.add(new SignatureDiagnostic(
DiagnosticKind.FLOW_NOT_CLOSED,
component.rootNodeId(),
"", //$NON-NLS-1$
"Signature planning requires a closed source flow component.")); //$NON-NLS-1$
return new ContainerSignatureMigrationPlan(
recommendation.targetContract(),
List.of(),
PlanningStatus.REJECTED,
diagnostics);
}
Map<GroupKey, List<ResolvedSearchTarget>> targetsByGroup= new LinkedHashMap<>();
for (ResolvedSearchTarget target : resolvedPlan.targets()) {
if (!declarationTarget(target)) {
continue;
}
GroupKey key= new GroupKey(target.sourceNodeId(), target.signatureIndex());
targetsByGroup.computeIfAbsent(key, ignored -> new ArrayList<>()).add(target);
}
if (targetsByGroup.isEmpty()) {
return new ContainerSignatureMigrationPlan(
recommendation.targetContract(),
List.of(),
PlanningStatus.NO_SIGNATURE_CHANGE,
List.of());
}
List<SignatureAtomicityGroup> groups= new ArrayList<>();
for (Map.Entry<GroupKey, List<ResolvedSearchTarget>> entry : targetsByGroup.entrySet()) {
GroupKey key= entry.getKey();
Map<String, SignatureMember> membersByHandle= new LinkedHashMap<>();
for (ResolvedSearchTarget target : entry.getValue()) {
if (target.javaElementHandle().isBlank()) {
diagnostics.add(new SignatureDiagnostic(
DiagnosticKind.MISSING_METHOD_HANDLE,
target.sourceNodeId(),
"", //$NON-NLS-1$
"An exact method declaration handle is required.")); //$NON-NLS-1$
continue;
}
List<FlowNode> matches= matchingNodes(component, target);
if (matches.isEmpty()) {
diagnostics.add(new SignatureDiagnostic(
DiagnosticKind.MISSING_SIGNATURE_NODE,
target.sourceNodeId(),
target.javaElementHandle(),
"The resolved method has no matching signature node in the flow component.")); //$NON-NLS-1$
continue;
}
if (matches.size() > 1) {
diagnostics.add(new SignatureDiagnostic(
DiagnosticKind.AMBIGUOUS_SIGNATURE_NODE,
target.sourceNodeId(),
target.javaElementHandle(),
"Several flow nodes match the same method signature position.")); //$NON-NLS-1$
continue;
}
FlowNode node= matches.get(0);
membersByHandle.putIfAbsent(target.javaElementHandle(), new SignatureMember(
target.javaElementHandle(),
node.ownerKey(),
node.compilationUnitHandle(),
node.stableId()));
}
if (!membersByHandle.isEmpty()) {
PositionKind position= key.signatureIndex() >= 0
? PositionKind.PARAMETER : PositionKind.RETURN;
BridgeFeasibility bridge= position == PositionKind.PARAMETER
? BridgeFeasibility.OVERLOAD_POSSIBLE_POLICY_REQUIRED
: BridgeFeasibility.SAME_NAME_RETURN_BRIDGE_IMPOSSIBLE;
String explanation= position == PositionKind.PARAMETER
? "Old and new parameter types can coexist as overloads, but any adapter must prove order, aliasing, mutation, null and duplicate semantics." //$NON-NLS-1$
: "Java cannot retain the old and new signatures under the same name because methods cannot be overloaded solely by return type."; //$NON-NLS-1$
List<SignatureMember> members= new ArrayList<>(membersByHandle.values());
members.sort(Comparator.comparing(SignatureMember::javaElementHandle));
groups.add(new SignatureAtomicityGroup(
groupId(key),
position,
key.signatureIndex(),
members,
bridge,
explanation));
}
}
groups.sort(Comparator.comparing(SignatureAtomicityGroup::groupId));
if (completedStatus == PlanningStatus.CLOSED_SOURCE_AUTOMATIC
&& !supportsAutomaticExecution(groups)) {
diagnostics.add(new SignatureDiagnostic(
DiagnosticKind.UNSUPPORTED_AUTOMATIC_GROUP,
component.rootNodeId(),
"", //$NON-NLS-1$
"Automatic signature execution currently supports exactly one source-resolved parameter declaration and no return or override family.")); //$NON-NLS-1$
}
return new ContainerSignatureMigrationPlan(
recommendation.targetContract(),
groups,
diagnostics.isEmpty() ? completedStatus : PlanningStatus.REJECTED,
diagnostics);
}
private static boolean supportsAutomaticExecution(
List<SignatureAtomicityGroup> groups) {
return groups.size() == 1
&& groups.get(0).positionKind() == PositionKind.PARAMETER
&& groups.get(0).members().size() == 1;
}
private static boolean declarationTarget(ResolvedSearchTarget target) {
return target.searchKind() == SearchKind.METHOD_DECLARATION
|| target.searchKind() == SearchKind.METHOD_OVERRIDE_FAMILY;
}
private static List<FlowNode> matchingNodes(
ContainerFlowComponent component,
ResolvedSearchTarget target) {
return component.nodes().stream()
.filter(node -> node.javaElementHandle().equals(target.javaElementHandle()))
.filter(node -> target.signatureIndex() >= 0
? (node.kind() == NodeKind.PARAMETER
|| node.kind() == NodeKind.EXTERNAL_PARAMETER)
&& node.signatureIndex() == target.signatureIndex()
: node.kind() == NodeKind.RETURN_POSITION)
.toList();
}
private static String groupId(GroupKey key) {
return key.sourceNodeId() + (key.signatureIndex() >= 0
? ":parameter:" + key.signatureIndex() //$NON-NLS-1$
: ":return"); //$NON-NLS-1$
}
private record GroupKey(String sourceNodeId, int signatureIndex) {
private GroupKey {
sourceNodeId= Objects.requireNonNull(sourceNodeId, "sourceNodeId"); //$NON-NLS-1$
}
}
}