ClosedFlowArrayUsageRefiner.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.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
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.ContainerShape;
import org.sandbox.jdt.container.api.ContainerUsageProfile;
import org.sandbox.jdt.container.api.ContainerUsageProfile.AliasingContract;
import org.sandbox.jdt.container.api.ContainerUsageProfile.AnalysisCompleteness;
import org.sandbox.jdt.container.api.ContainerUsageProfile.AtomicityRequirement;
import org.sandbox.jdt.container.api.ContainerUsageProfile.ConcurrencyProfile;
import org.sandbox.jdt.container.api.ContainerUsageProfile.EscapeLevel;
import org.sandbox.jdt.container.api.ContainerUsageProfile.IterationSemantics;
import org.sandbox.jdt.container.api.ContainerUsageProfile.OrderRequirement;
import org.sandbox.jdt.container.api.ContainerUsageProfile.SynchronizationKind;
import org.sandbox.jdt.container.api.ContainerUsageProfile.ThreadExposure;
import org.sandbox.jdt.container.api.ContainerUsageProfile.WorkloadShape;
import org.sandbox.jdt.container.api.UsageEvidence;
import org.sandbox.jdt.container.api.UsageEvidence.Kind;
/**
* Reclassifies exact method-argument escapes after the multi-file flow is closed.
*
* <p>The first refinement accepts only one local source and source-resolved parameter
* nodes joined by {@link EdgeKind#ARGUMENT_TO_PARAMETER}. Each parameter must also
* have a complete, rejection-free usage profile. Assignments, fields, returns,
* captures and every unmatched escape remain rejected.</p>
*/
public final class ClosedFlowArrayUsageRefiner {
private static final Set<Kind> REJECTION_EVIDENCE= EnumSet.of(
Kind.UNSUPPORTED_CONTINUATION,
Kind.CAPTURED_USAGE,
Kind.UNSAFE_ESCAPE,
Kind.ARRAY_IDENTITY,
Kind.UNCLASSIFIED_USAGE,
Kind.UNRESOLVED_BINDING,
Kind.REJECTION_BOUNDARY);
/** Refines one rejected local profile from exact closed-flow evidence. */
public ContainerUsageProfile refine(
String compilationUnitHandle,
ContainerUsageProfile localProfile,
ContainerFlowComponent component,
List<ContainerUsageProfile> parameterProfiles) {
String unitHandle= requiredText(
compilationUnitHandle, "compilationUnitHandle"); //$NON-NLS-1$
Objects.requireNonNull(localProfile, "localProfile"); //$NON-NLS-1$
Objects.requireNonNull(component, "component"); //$NON-NLS-1$
parameterProfiles= List.copyOf(
Objects.requireNonNull(parameterProfiles, "parameterProfiles")); //$NON-NLS-1$
if (localProfile.currentShape() != ContainerShape.ARRAY) {
throw new IllegalArgumentException(
"ClosedFlowArrayUsageRefiner requires an array profile"); //$NON-NLS-1$
}
List<UsageEvidence> evidence= new ArrayList<>(localProfile.evidence());
List<FlowNode> sourceNodes= component.nodes().stream()
.filter(node -> node.compilationUnitHandle().equals(unitHandle))
.filter(node -> node.bindingKey().equals(
localProfile.identity().bindingKey()))
.toList();
if (sourceNodes.size() != 1) {
return rejected(localProfile, evidence,
"Closed flow does not contain one exact local source node"); //$NON-NLS-1$
}
FlowNode sourceNode= sourceNodes.get(0);
if (!eligibleComponent(component, sourceNode, parameterProfiles)) {
return rejected(localProfile, evidence,
"Closed flow or parameter usage evidence is incomplete"); //$NON-NLS-1$
}
Set<SourceRange> allowedTransfers= HashSet.newHashSet(component.edges().size());
for (LocatedFlowEdge edge : component.outgoing(sourceNode.stableId())) {
if (edge.kind() != EdgeKind.ARGUMENT_TO_PARAMETER
|| !edge.compilationUnitHandle().equals(unitHandle)) {
return rejected(localProfile, evidence,
"Closed flow contains an unsupported transfer kind"); //$NON-NLS-1$
}
allowedTransfers.add(new SourceRange(edge.sourceStart(), edge.sourceLength()));
}
if (allowedTransfers.isEmpty()) {
return rejected(localProfile, evidence,
"No closed argument-to-parameter transfer was found"); //$NON-NLS-1$
}
Set<SourceRange> discharged= HashSet.newHashSet(allowedTransfers.size());
List<UsageEvidence> refined= new ArrayList<>(evidence.size() + 1);
for (UsageEvidence item : evidence) {
SourceRange range= new SourceRange(item.sourceStart(), item.sourceLength());
if (item.kind() == Kind.UNSAFE_ESCAPE && allowedTransfers.contains(range)) {
discharged.add(range);
refined.add(new UsageEvidence(
Kind.FLOW_CONTINUATION_ROOT,
"Method argument transfer is covered by the closed source flow", //$NON-NLS-1$
item.sourceStart(), item.sourceLength()));
} else if (item.kind() != Kind.LOCAL_USAGE_COMPLETE) {
refined.add(item);
}
}
if (!discharged.equals(allowedTransfers)
|| refined.stream().anyMatch(item -> REJECTION_EVIDENCE.contains(item.kind()))) {
return rejected(localProfile, refined,
"Not every rejected escape is explained by the closed source flow"); //$NON-NLS-1$
}
refined.add(new UsageEvidence(
Kind.LOCAL_USAGE_COMPLETE,
"Every local operation and closed-flow transfer was classified", //$NON-NLS-1$
localProfile.identity().sourceStart(),
localProfile.identity().sourceLength()));
refined.sort(Comparator.comparingInt(UsageEvidence::sourceStart)
.thenComparing(item -> item.kind().ordinal()));
return new ContainerUsageProfile(
localProfile.identity(),
localProfile.currentShape(),
localProfile.elementDomain(),
localProfile.access(),
aggregateOrder(localProfile, parameterProfiles),
localProfile.uniquenessRequirement(),
localProfile.mutationLifecycle(),
localProfile.nullContract(),
AliasingContract.NO_OBSERVED_ALIAS,
EscapeLevel.METHOD_BOUNDARY,
threadConfined(),
AnalysisCompleteness.FLOW_COMPLETE,
refined);
}
private static boolean eligibleComponent(
ContainerFlowComponent component,
FlowNode sourceNode,
List<ContainerUsageProfile> parameterProfiles) {
if (component.closureStatus() != ClosureStatus.LOCAL_CLOSED
|| !component.diagnostics().isEmpty()
|| sourceNode.kind() != NodeKind.LOCAL_VARIABLE
|| !sourceNode.sourceResolved()
|| component.nodes().stream().filter(node ->
node.kind() == NodeKind.LOCAL_VARIABLE).count() != 1
|| component.nodes().stream().anyMatch(node -> !node.sourceResolved()
|| node.kind() != NodeKind.LOCAL_VARIABLE
&& node.kind() != NodeKind.PARAMETER)
|| component.edges().stream().anyMatch(edge ->
edge.kind() != EdgeKind.ARGUMENT_TO_PARAMETER
|| !edge.sourceNodeId().equals(sourceNode.stableId()))) {
return false;
}
List<FlowNode> parameters= component.nodes().stream()
.filter(node -> node.kind() == NodeKind.PARAMETER)
.toList();
if (parameters.isEmpty() || parameters.size() != parameterProfiles.size()) {
return false;
}
for (FlowNode parameter : parameters) {
List<ContainerUsageProfile> matches= parameterProfiles.stream()
.filter(profile -> profile.identity().bindingKey().equals(
parameter.bindingKey()))
.toList();
if (matches.size() != 1 || !safeParameterProfile(matches.get(0))) {
return false;
}
}
return component.edges().stream().allMatch(edge -> component.node(edge.targetNodeId())
.filter(node -> node.kind() == NodeKind.PARAMETER)
.isPresent());
}
private static boolean safeParameterProfile(ContainerUsageProfile profile) {
return profile.currentShape() == ContainerShape.ARRAY
&& profile.escapeLevel() == EscapeLevel.METHOD_BOUNDARY
&& profile.aliasingContract() == AliasingContract.NO_OBSERVED_ALIAS
&& (profile.completeness() == AnalysisCompleteness.LOCAL_USAGE_COMPLETE
|| profile.completeness() == AnalysisCompleteness.FLOW_COMPLETE)
&& profile.evidence().stream().noneMatch(evidence ->
REJECTION_EVIDENCE.contains(evidence.kind()));
}
private static OrderRequirement aggregateOrder(
ContainerUsageProfile localProfile,
List<ContainerUsageProfile> parameterProfiles) {
boolean positional= false;
boolean sorted= false;
boolean encounter= false;
boolean none= false;
boolean unknown= false;
List<ContainerUsageProfile> profiles= new ArrayList<>(parameterProfiles.size() + 1);
profiles.add(localProfile);
profiles.addAll(parameterProfiles);
for (ContainerUsageProfile profile : profiles) {
switch (profile.orderRequirement()) {
case POSITIONAL -> positional= true;
case SORTED -> sorted= true;
case ENCOUNTER -> encounter= true;
case NONE -> none= true;
case UNKNOWN -> unknown= true;
}
}
if (positional) {
return OrderRequirement.POSITIONAL;
}
if (sorted) {
return OrderRequirement.SORTED;
}
if (encounter) {
return OrderRequirement.ENCOUNTER;
}
return none && !unknown ? OrderRequirement.NONE : OrderRequirement.UNKNOWN;
}
private static ContainerUsageProfile rejected(
ContainerUsageProfile source,
List<UsageEvidence> evidence,
String message) {
List<UsageEvidence> rejected= new ArrayList<>(evidence.size() + 1);
rejected.addAll(evidence);
rejected.add(new UsageEvidence(
Kind.REJECTION_BOUNDARY,
message,
source.identity().sourceStart(),
source.identity().sourceLength()));
rejected.sort(Comparator.comparingInt(UsageEvidence::sourceStart)
.thenComparing(item -> item.kind().ordinal()));
return new ContainerUsageProfile(
source.identity(),
source.currentShape(),
source.elementDomain(),
source.access(),
source.orderRequirement(),
source.uniquenessRequirement(),
source.mutationLifecycle(),
source.nullContract(),
AliasingContract.UNKNOWN,
source.escapeLevel(),
ConcurrencyProfile.unknown(),
AnalysisCompleteness.REJECTED,
rejected);
}
private static ConcurrencyProfile threadConfined() {
return new ConcurrencyProfile(
ThreadExposure.THREAD_CONFINED,
SynchronizationKind.NONE,
IterationSemantics.LIVE,
AtomicityRequirement.INDIVIDUAL_OPERATIONS,
WorkloadShape.UNKNOWN);
}
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 record SourceRange(int start, int length) {
private SourceRange {
if (start < 0 || length < 0) {
throw new IllegalArgumentException("Source range must not be negative"); //$NON-NLS-1$
}
}
}
}