ClosedSourceParameterMigrationPlanner.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.Collection;
import java.util.List;
import java.util.Objects;
import org.sandbox.jdt.container.api.ClosedSourceParameterMigrationPlan;
import org.sandbox.jdt.container.api.ClosedSourceParameterMigrationPlan.DiagnosticKind;
import org.sandbox.jdt.container.api.ClosedSourceParameterMigrationPlan.PlanningDiagnostic;
import org.sandbox.jdt.container.api.ClosedSourceParameterMigrationPlan.PlanningResult;
import org.sandbox.jdt.container.api.ContainerFlowComponent;
import org.sandbox.jdt.container.api.ContainerFlowComponent.LocatedFlowEdge;
import org.sandbox.jdt.container.api.ContainerFlowGraph.ClosureStatus;
import org.sandbox.jdt.container.api.ContainerFlowGraph.EdgeKind;
import org.sandbox.jdt.container.api.ContainerFlowGraph.FlowNode;
import org.sandbox.jdt.container.api.ContainerFlowGraph.NodeKind;
import org.sandbox.jdt.container.api.ContainerLocalRewritePlan;
import org.sandbox.jdt.container.api.ContainerLocalRewritePlan.ArgumentTransfer;
import org.sandbox.jdt.container.api.ContainerMigrationReadiness;
import org.sandbox.jdt.container.api.ContainerParameterRewritePlan;
import org.sandbox.jdt.container.api.ContainerRecommendation;
import org.sandbox.jdt.container.api.ContainerSignatureMigrationPlan;
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.SignatureMember;
import org.sandbox.jdt.container.api.ContainerUsageProfile;
/**
* Builds the first aggregate two-compilation-unit caller/parameter rewrite plan.
*
* <p>The accepted topology is deliberately narrow: one local array source, one direct
* {@code ARGUMENT_TO_PARAMETER} edge and one source-resolved parameter declaration in
* a different compilation unit. The unchanged argument is tied to the exact target
* method handle and parameter index before either member plan is emitted.</p>
*/
public final class ClosedSourceParameterMigrationPlanner {
private final ContainerLocalRewritePlanner localPlanner=
new ContainerLocalRewritePlanner();
private final ContainerParameterRewritePlanner parameterPlanner=
new ContainerParameterRewritePlanner();
/** Builds one immutable aggregate plan or complete rejection diagnostics. */
public PlanningResult plan(
ContainerFlowComponent component,
ContainerSignatureMigrationPlan signaturePlan,
ContainerRecommendation recommendation,
ContainerMigrationReadiness readiness,
List<ContainerUsageProfile> memberProfiles) {
Objects.requireNonNull(component, "component"); //$NON-NLS-1$
Objects.requireNonNull(signaturePlan, "signaturePlan"); //$NON-NLS-1$
Objects.requireNonNull(recommendation, "recommendation"); //$NON-NLS-1$
Objects.requireNonNull(readiness, "readiness"); //$NON-NLS-1$
memberProfiles= List.copyOf(
Objects.requireNonNull(memberProfiles, "memberProfiles")); //$NON-NLS-1$
List<PlanningDiagnostic> diagnostics= new ArrayList<>();
Topology topology= topology(component);
if (topology == null) {
diagnostics.add(diagnostic(
DiagnosticKind.UNSUPPORTED_FLOW_TOPOLOGY,
"The first aggregate rewrite requires one local caller and one direct source parameter edge.")); //$NON-NLS-1$
return PlanningResult.rejected(diagnostics);
}
Signature signature= signature(signaturePlan, topology.parameter());
if (signature == null) {
diagnostics.add(diagnostic(
DiagnosticKind.SIGNATURE_PLAN_MISMATCH,
"The automatic signature plan does not contain the exact parameter member.")); //$NON-NLS-1$
return PlanningResult.rejected(diagnostics);
}
ContainerUsageProfile callerProfile= profile(
memberProfiles, topology.caller().bindingKey());
ContainerUsageProfile parameterProfile= profile(
memberProfiles, topology.parameter().bindingKey());
if (callerProfile == null || parameterProfile == null) {
diagnostics.add(diagnostic(
DiagnosticKind.PROFILE_NOT_FOUND,
"Caller and parameter profiles must both be present exactly once.")); //$NON-NLS-1$
return PlanningResult.rejected(diagnostics);
}
if (!recommendation.sourceProfile().equals(callerProfile)
|| !recommendation.targetContract().equals(signaturePlan.targetContract())) {
diagnostics.add(diagnostic(
DiagnosticKind.RECOMMENDATION_MISMATCH,
"The recommendation does not describe the exact aggregate caller and signature target.")); //$NON-NLS-1$
return PlanningResult.rejected(diagnostics);
}
ArgumentTransfer transfer= new ArgumentTransfer(
signature.member().javaElementHandle(),
signature.group().signatureIndex(),
topology.edge().sourceStart(),
topology.edge().sourceLength());
ContainerLocalRewritePlan.PlanningResult callerResult= localPlanner.plan(
localComponent(topology.caller()),
recommendation,
readiness,
List.of(transfer));
if (!callerResult.ready()) {
callerResult.diagnostics().forEach(item -> diagnostics.add(diagnostic(
DiagnosticKind.LOCAL_REWRITE_REJECTED,
item.kind() + ": " + item.message()))); //$NON-NLS-1$
}
ContainerParameterRewritePlan.PlanningResult parameterResult=
parameterPlanner.plan(
component,
signaturePlan,
signature.group(),
signature.member(),
parameterProfile,
readiness);
if (!parameterResult.ready()) {
parameterResult.diagnostics().forEach(item -> diagnostics.add(diagnostic(
DiagnosticKind.PARAMETER_REWRITE_REJECTED,
item.kind() + ": " + item.message()))); //$NON-NLS-1$
}
if (!diagnostics.isEmpty()) {
return PlanningResult.rejected(diagnostics);
}
ContainerLocalRewritePlan callerPlan= callerResult.plan().orElseThrow();
ContainerParameterRewritePlan parameterPlan=
parameterResult.plan().orElseThrow();
if (callerPlan.compilationUnitHandle()
.equals(parameterPlan.compilationUnitHandle())) {
return PlanningResult.rejected(List.of(diagnostic(
DiagnosticKind.SAME_COMPILATION_UNIT,
"The first aggregate slice requires caller and parameter in distinct units."))); //$NON-NLS-1$
}
return PlanningResult.accepted(new ClosedSourceParameterMigrationPlan(
recommendation.targetContract(), callerPlan, parameterPlan));
}
private static Topology topology(ContainerFlowComponent component) {
if (component.closureStatus() != ClosureStatus.LOCAL_CLOSED
|| !component.diagnostics().isEmpty()
|| component.nodes().size() != 2
|| component.edges().size() != 1) {
return null;
}
LocatedFlowEdge edge= component.edges().get(0);
if (edge.kind() != EdgeKind.ARGUMENT_TO_PARAMETER) {
return null;
}
FlowNode source= component.node(edge.sourceNodeId()).orElse(null);
FlowNode target= component.node(edge.targetNodeId()).orElse(null);
if (source == null || target == null
|| source.kind() != NodeKind.LOCAL_VARIABLE
|| target.kind() != NodeKind.PARAMETER
|| !source.sourceResolved()
|| !target.sourceResolved()
|| source.compilationUnitHandle().equals(target.compilationUnitHandle())
|| !edge.compilationUnitHandle().equals(source.compilationUnitHandle())) {
return null;
}
return new Topology(source, target, edge);
}
private static Signature signature(
ContainerSignatureMigrationPlan plan,
FlowNode parameter) {
if (plan.status() != PlanningStatus.CLOSED_SOURCE_AUTOMATIC
|| plan.groups().size() != 1) {
return null;
}
SignatureAtomicityGroup group= plan.groups().get(0);
if (group.positionKind() != PositionKind.PARAMETER
|| group.signatureIndex() != parameter.signatureIndex()
|| group.members().size() != 1) {
return null;
}
SignatureMember member= group.members().get(0);
return member.flowNodeId().equals(parameter.stableId())
&& member.compilationUnitHandle()
.equals(parameter.compilationUnitHandle())
&& member.javaElementHandle().equals(parameter.javaElementHandle())
? new Signature(group, member) : null;
}
private static ContainerUsageProfile profile(
Collection<ContainerUsageProfile> profiles,
String bindingKey) {
List<ContainerUsageProfile> matches= profiles.stream()
.filter(profile -> profile.identity().bindingKey().equals(bindingKey))
.toList();
return matches.size() == 1 ? matches.get(0) : null;
}
private static ContainerFlowComponent localComponent(FlowNode local) {
return new ContainerFlowComponent(
local.stableId(),
List.of(local),
List.of(),
ClosureStatus.LOCAL_CLOSED,
List.of());
}
private static PlanningDiagnostic diagnostic(
DiagnosticKind kind,
String message) {
return new PlanningDiagnostic(kind, message);
}
private record Topology(
FlowNode caller,
FlowNode parameter,
LocatedFlowEdge edge) {
}
private record Signature(
SignatureAtomicityGroup group,
SignatureMember member) {
}
}