LocalArrayUsageAnalyzer.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 static org.sandbox.jdt.container.analysis.ContainerAstFacts.isArrayLength;
import static org.sandbox.jdt.container.analysis.ContainerAstFacts.isOne;
import static org.sandbox.jdt.container.analysis.ContainerAstFacts.sameVariable;
import static org.sandbox.jdt.container.analysis.ContainerAstFacts.unwrap;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.ArrayAccess;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.CastExpression;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.InstanceofExpression;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.SuperFieldAccess;
import org.eclipse.jdt.core.dom.SynchronizedStatement;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.sandbox.jdt.container.api.ContainerShape;
import org.sandbox.jdt.container.api.ContainerUsageProfile;
import org.sandbox.jdt.container.api.ContainerUsageProfile.AccessProfile;
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.MutationLifecycle;
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;
import org.sandbox.jdt.internal.common.AstProcessing;
import org.sandbox.jdt.internal.common.ReferenceHolder;
/**
* Classifies every binding-resolved use of one array candidate inside a compilation
* unit.
*
* <p>The first implementation intentionally supports a narrow, explainable set of
* uses: declarations, recognised array growth, tail append, {@code length}, indexed
* read/write and enhanced-for iteration. Any other use is retained as source-backed
* rejection evidence. This fail-closed boundary prevents a local seed from becoming
* an executable migration candidate merely because most uses looked familiar.</p>
*/
public final class LocalArrayUsageAnalyzer {
/** Analyzes one seed profile in the supplied compilation unit. */
public ContainerUsageProfile analyze(
CompilationUnit compilationUnit,
ContainerUsageProfile seed) {
Objects.requireNonNull(compilationUnit, "compilationUnit"); //$NON-NLS-1$
Objects.requireNonNull(seed, "seed"); //$NON-NLS-1$
if (seed.currentShape() != ContainerShape.ARRAY) {
throw new IllegalArgumentException("LocalArrayUsageAnalyzer requires an array profile"); //$NON-NLS-1$
}
Accumulator accumulator= new Accumulator(seed);
if (!seed.identity().hasResolvedBinding()) {
accumulator.reject(Kind.UNRESOLVED_BINDING,
"Local usage analysis requires a resolved variable binding", //$NON-NLS-1$
seed.identity().sourceStart(), seed.identity().sourceLength());
return accumulator.toProfile();
}
ReferenceHolder<String, Object> traversalState= ReferenceHolder.create();
AstProcessing.independent(traversalState)
.on(SimpleName.class, (name, holder) -> {
classifyMatchingReference(name, seed.identity().bindingKey(), accumulator);
return true;
})
.build(compilationUnit);
if (!accumulator.bindingSeen()) {
accumulator.reject(Kind.UNRESOLVED_BINDING,
"The candidate binding was not found in the analyzed compilation unit", //$NON-NLS-1$
seed.identity().sourceStart(), seed.identity().sourceLength());
}
return accumulator.toProfile();
}
/** Analyzes profiles in deterministic input order. */
public List<ContainerUsageProfile> analyzeAll(
CompilationUnit compilationUnit,
List<ContainerUsageProfile> seeds) {
Objects.requireNonNull(seeds, "seeds"); //$NON-NLS-1$
return seeds.stream().map(seed -> analyze(compilationUnit, seed)).toList();
}
private static void classifyMatchingReference(
SimpleName name,
String targetBindingKey,
Accumulator accumulator) {
IBinding resolved= name.resolveBinding();
if (!(resolved instanceof IVariableBinding variable)
|| !targetBindingKey.equals(variable.getVariableDeclaration().getKey())) {
return;
}
IVariableBinding declaration= variable.getVariableDeclaration();
accumulator.observeBinding(declaration);
if (isDeclarationName(name)) {
return;
}
if (crossesExecutableBoundary(name, declaration)) {
accumulator.reject(Kind.CAPTURED_USAGE,
"Array value is captured by a lambda, nested type, or different method body", name); //$NON-NLS-1$
return;
}
Expression reference= completeReferenceExpression(name, targetBindingKey);
ASTNode parent= reference.getParent();
if (isLengthRead(reference, parent)) {
accumulator.lengthRead(reference);
} else if (parent instanceof ArrayAccess access && access.getArray() == reference) {
classifyArrayAccess(access, reference, accumulator);
} else if (parent instanceof EnhancedForStatement enhancedFor
&& enhancedFor.getExpression() == reference) {
accumulator.encounterIteration(reference);
} else if (parent instanceof MethodInvocation invocation) {
classifyMethodInvocation(reference, invocation, accumulator);
} else if (parent instanceof Assignment assignment) {
classifyAssignment(reference, assignment, accumulator);
} else if (parent instanceof VariableDeclarationFragment fragment
&& fragment.getInitializer() == reference) {
accumulator.reject(Kind.UNSAFE_ESCAPE,
"Array value is assigned to another local variable", reference); //$NON-NLS-1$
} else if (parent instanceof ReturnStatement) {
accumulator.reject(Kind.UNSAFE_ESCAPE,
"Array value escapes through a return statement", reference); //$NON-NLS-1$
} else if (parent instanceof InfixExpression infix && isIdentityComparison(infix)) {
accumulator.reject(Kind.ARRAY_IDENTITY,
"Array identity is observed by a reference comparison", reference); //$NON-NLS-1$
} else if (parent instanceof SynchronizedStatement synchronizedStatement
&& synchronizedStatement.getExpression() == reference) {
accumulator.reject(Kind.ARRAY_IDENTITY,
"Array identity is used as a synchronization monitor", reference); //$NON-NLS-1$
} else if (parent instanceof CastExpression || parent instanceof InstanceofExpression) {
accumulator.reject(Kind.ARRAY_IDENTITY,
"Runtime array representation is observed by a type operation", reference); //$NON-NLS-1$
} else {
accumulator.reject(Kind.UNCLASSIFIED_USAGE,
"Array use is not yet classified by local container analysis", reference); //$NON-NLS-1$
}
}
private static boolean crossesExecutableBoundary(
ASTNode reference,
IVariableBinding declaration) {
IMethodBinding declaringMethod= declaration.getDeclaringMethod();
if (declaringMethod == null || declaration.isField()) {
return false;
}
String declaringKey= methodKey(declaringMethod);
ASTNode current= reference.getParent();
while (current != null) {
if (current instanceof LambdaExpression
|| current instanceof AnonymousClassDeclaration
|| current instanceof AbstractTypeDeclaration) {
return true;
}
if (current instanceof MethodDeclaration method) {
IMethodBinding currentBinding= method.resolveBinding();
return currentBinding == null
|| !declaringKey.equals(methodKey(currentBinding));
}
current= current.getParent();
}
return true;
}
private static String methodKey(IMethodBinding method) {
IMethodBinding declaration= method.getMethodDeclaration();
String key= declaration.getKey();
return key == null ? "" : key; //$NON-NLS-1$
}
private static void classifyArrayAccess(
ArrayAccess access,
Expression reference,
Accumulator accumulator) {
Assignment assignment= enclosingAssignment(access);
if (assignment != null && unwrap(assignment.getLeftHandSide()) == access) {
if (isTailAppend(access, reference)) {
accumulator.appendObserved();
} else {
accumulator.indexedWrite(access);
}
} else {
accumulator.indexedRead(access);
}
}
private static void classifyMethodInvocation(
Expression reference,
MethodInvocation invocation,
Accumulator accumulator) {
if (invocation.arguments().contains(reference)
&& isRecognizedCopySource(invocation, reference)) {
return;
}
if (invocation.arguments().contains(reference)) {
accumulator.reject(Kind.UNSAFE_ESCAPE,
"Array value is passed to another method", reference); //$NON-NLS-1$
} else {
accumulator.reject(Kind.UNCLASSIFIED_USAGE,
"Method invocation observes array-specific behavior", reference); //$NON-NLS-1$
}
}
private static void classifyAssignment(
Expression reference,
Assignment assignment,
Accumulator accumulator) {
if (assignment.getLeftHandSide() == reference
&& isRecognizedGrowthAssignment(assignment, reference)) {
return;
}
if (assignment.getRightHandSide() == reference) {
accumulator.reject(Kind.UNSAFE_ESCAPE,
"Array value is assigned to another variable or field", reference); //$NON-NLS-1$
} else {
accumulator.reject(Kind.UNCLASSIFIED_USAGE,
"Array reference is reassigned outside the recognised growth pattern", reference); //$NON-NLS-1$
}
}
private static boolean isDeclarationName(SimpleName name) {
ASTNode parent= name.getParent();
return parent instanceof VariableDeclarationFragment fragment && fragment.getName() == name
|| parent instanceof SingleVariableDeclaration declaration
&& declaration.getName() == name;
}
private static Expression completeReferenceExpression(SimpleName name, String bindingKey) {
Expression reference= name;
ASTNode parent= reference.getParent();
if (parent instanceof QualifiedName qualified
&& qualified.getName() == reference
&& hasBindingKey(qualified.resolveBinding(), bindingKey)) {
reference= qualified;
} else if (parent instanceof FieldAccess fieldAccess
&& fieldAccess.getName() == reference
&& hasBindingKey(fieldAccess.resolveFieldBinding(), bindingKey)) {
reference= fieldAccess;
} else if (parent instanceof SuperFieldAccess superFieldAccess
&& superFieldAccess.getName() == reference
&& hasBindingKey(superFieldAccess.resolveFieldBinding(), bindingKey)) {
reference= superFieldAccess;
}
while (reference.getParent() instanceof ParenthesizedExpression parenthesized) {
reference= parenthesized;
}
return reference;
}
private static boolean hasBindingKey(IBinding binding, String bindingKey) {
return binding instanceof IVariableBinding variable
&& bindingKey.equals(variable.getVariableDeclaration().getKey());
}
private static boolean isLengthRead(Expression reference, ASTNode parent) {
if (parent instanceof QualifiedName qualifiedName) {
return qualifiedName.getQualifier() == reference
&& "length".equals(qualifiedName.getName().getIdentifier()); //$NON-NLS-1$
}
if (parent instanceof FieldAccess fieldAccess) {
return fieldAccess.getExpression() == reference
&& "length".equals(fieldAccess.getName().getIdentifier()); //$NON-NLS-1$
}
return false;
}
private static boolean isRecognizedCopySource(
MethodInvocation invocation,
Expression reference) {
return isArraysCopyOf(invocation)
&& !invocation.arguments().isEmpty()
&& invocation.arguments().get(0) == reference;
}
private static boolean isRecognizedGrowthAssignment(
Assignment assignment,
Expression reference) {
Expression right= unwrap(assignment.getRightHandSide());
if (!(right instanceof MethodInvocation copyOf)
|| !isArraysCopyOf(copyOf)
|| copyOf.arguments().size() != 2) {
return false;
}
Expression source= (Expression) copyOf.arguments().get(0);
Expression length= (Expression) copyOf.arguments().get(1);
return sameVariable(reference, source) && isLengthPlusOne(length, reference);
}
private static boolean isArraysCopyOf(MethodInvocation invocation) {
if (!"copyOf".equals(invocation.getName().getIdentifier())) { //$NON-NLS-1$
return false;
}
IMethodBinding binding= invocation.resolveMethodBinding();
if (binding != null && binding.getDeclaringClass() != null) {
return "java.util.Arrays".equals( //$NON-NLS-1$
binding.getDeclaringClass().getErasure().getQualifiedName());
}
Expression owner= invocation.getExpression();
return owner != null && ("Arrays".equals(owner.toString()) //$NON-NLS-1$
|| "java.util.Arrays".equals(owner.toString())); //$NON-NLS-1$
}
private static boolean isTailAppend(ArrayAccess access, Expression array) {
return isLengthMinusOne(access.getIndex(), array);
}
private static boolean isLengthPlusOne(Expression expression, Expression array) {
Expression unwrapped= unwrap(expression);
if (!(unwrapped instanceof InfixExpression infix)
|| infix.getOperator() != InfixExpression.Operator.PLUS
|| !infix.extendedOperands().isEmpty()) {
return false;
}
return isArrayLength(infix.getLeftOperand(), array) && isOne(infix.getRightOperand())
|| isOne(infix.getLeftOperand()) && isArrayLength(infix.getRightOperand(), array);
}
private static boolean isLengthMinusOne(Expression expression, Expression array) {
Expression unwrapped= unwrap(expression);
if (!(unwrapped instanceof InfixExpression infix)
|| infix.getOperator() != InfixExpression.Operator.MINUS
|| !infix.extendedOperands().isEmpty()) {
return false;
}
return isArrayLength(infix.getLeftOperand(), array) && isOne(infix.getRightOperand());
}
private static Assignment enclosingAssignment(ASTNode node) {
ASTNode current= node;
while (current.getParent() instanceof ParenthesizedExpression) {
current= current.getParent();
}
return current.getParent() instanceof Assignment assignment ? assignment : null;
}
private static boolean isIdentityComparison(InfixExpression expression) {
return expression.getOperator() == InfixExpression.Operator.EQUALS
|| expression.getOperator() == InfixExpression.Operator.NOT_EQUALS;
}
private static UsageEvidence evidence(Kind kind, String summary, ASTNode node) {
return new UsageEvidence(kind, summary, node.getStartPosition(), node.getLength());
}
private static final class Accumulator {
private final ContainerUsageProfile seed;
private final List<UsageEvidence> evidence;
private boolean bindingSeen;
private boolean field;
private boolean parameter;
private boolean indexedRead;
private boolean indexedWrite;
private boolean positionalWrite;
private boolean append;
private boolean encounterIteration;
private boolean rejected;
Accumulator(ContainerUsageProfile seed) {
this.seed= seed;
evidence= new ArrayList<>(seed.evidence());
indexedRead= seed.access().indexedRead();
indexedWrite= seed.access().indexedWrite();
append= seed.access().append();
}
void observeBinding(IVariableBinding binding) {
bindingSeen= true;
field|= binding.isField();
parameter|= binding.isParameter();
}
boolean bindingSeen() {
return bindingSeen;
}
void lengthRead(ASTNode node) {
evidence.add(LocalArrayUsageAnalyzer.evidence(Kind.ARRAY_LENGTH_READ,
"Array length is observed", node)); //$NON-NLS-1$
}
void indexedRead(ASTNode node) {
indexedRead= true;
evidence.add(LocalArrayUsageAnalyzer.evidence(Kind.INDEXED_READ,
"Array element is read by index", node)); //$NON-NLS-1$
}
void indexedWrite(ASTNode node) {
indexedWrite= true;
positionalWrite= true;
evidence.add(LocalArrayUsageAnalyzer.evidence(Kind.INDEXED_WRITE,
"Existing array position is written by index", node)); //$NON-NLS-1$
}
void appendObserved() {
append= true;
}
void encounterIteration(ASTNode node) {
encounterIteration= true;
evidence.add(LocalArrayUsageAnalyzer.evidence(Kind.ENCOUNTER_ITERATION,
"Array is traversed in encounter order", node)); //$NON-NLS-1$
}
void reject(Kind kind, String summary, ASTNode node) {
reject(kind, summary, node.getStartPosition(), node.getLength());
}
void reject(Kind kind, String summary, int start, int length) {
rejected= true;
evidence.add(new UsageEvidence(kind, summary, start, length));
}
ContainerUsageProfile toProfile() {
AnalysisCompleteness completeness= rejected
? AnalysisCompleteness.REJECTED
: AnalysisCompleteness.LOCAL_USAGE_COMPLETE;
if (!rejected) {
evidence.add(new UsageEvidence(Kind.LOCAL_USAGE_COMPLETE,
"Every local use of the array binding was classified", //$NON-NLS-1$
seed.identity().sourceStart(), seed.identity().sourceLength()));
}
OrderRequirement order= indexedRead || positionalWrite
? OrderRequirement.POSITIONAL
: encounterIteration ? OrderRequirement.ENCOUNTER : seed.orderRequirement();
EscapeLevel escape= field
? EscapeLevel.FIELD
: parameter ? EscapeLevel.METHOD_BOUNDARY : EscapeLevel.LOCAL;
AliasingContract aliasing= rejected
? AliasingContract.UNKNOWN
: AliasingContract.NO_OBSERVED_ALIAS;
ConcurrencyProfile concurrency= !rejected && escape == EscapeLevel.LOCAL
? new ConcurrencyProfile(
ThreadExposure.THREAD_CONFINED,
SynchronizationKind.NONE,
IterationSemantics.LIVE,
AtomicityRequirement.INDIVIDUAL_OPERATIONS,
WorkloadShape.UNKNOWN)
: seed.concurrency();
return new ContainerUsageProfile(
seed.identity(),
seed.currentShape(),
seed.elementDomain(),
new AccessProfile(
indexedRead,
indexedWrite,
append,
false,
false,
seed.access().membershipQuery(),
seed.access().keyLookup()),
order,
seed.uniquenessRequirement(),
MutationLifecycle.CONTINUOUSLY_MUTABLE,
seed.nullContract(),
aliasing,
escape,
concurrency,
completeness,
evidence);
}
}
}