LocalEnumIndexedMembershipAnalyzer.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer and others.
*
* 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.List;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
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.ArrayCreation;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.BooleanLiteral;
import org.eclipse.jdt.core.dom.CompilationUnit;
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.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.InfixExpression;
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.SynchronizedStatement;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
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.ContainerIdentity;
import org.sandbox.jdt.container.api.ContainerUsageProfile.ElementDomain;
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.NullContract;
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.UniquenessRequirement;
import org.sandbox.jdt.container.api.ContainerUsageProfile.WorkloadShape;
import org.sandbox.jdt.container.api.UsageEvidence;
import org.sandbox.jdt.container.api.UsageEvidence.Kind;
/**
* Proves a narrow local enum-membership table encoded as {@code boolean[]}.
*
* <p>The array must be created with exactly {@code SomeEnum.values().length} slots,
* and every element access must use {@code value.ordinal()} for that same enum type.
* Reads represent membership queries; literal {@code true}/{@code false} writes
* represent enable/disable operations. Array length, numeric indexing, aliases,
* escapes, captures and computed writes reject the profile.</p>
*
* <p>The source profile deliberately remains physically accurate: the current
* container is a primitive {@code boolean[]} with indexed reads/writes. The fact
* that those positions encode enum membership is retained as explicit evidence and
* consumed only by the specialized contract inferrer.</p>
*/
public final class LocalEnumIndexedMembershipAnalyzer {
/** Returns source-ordered complete or rejected local enum-membership profiles. */
public List<ContainerUsageProfile> analyze(CompilationUnit unit) {
Objects.requireNonNull(unit, "unit"); //$NON-NLS-1$
List<ContainerUsageProfile> result= new ArrayList<>();
unit.accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment fragment) {
candidate(fragment).map(LocalEnumIndexedMembershipAnalyzer::analyzeCandidate)
.ifPresent(result::add);
return true;
}
});
result.sort(Comparator.comparingInt(profile -> profile.identity().sourceStart()));
return List.copyOf(result);
}
private static Optional<Candidate> candidate(VariableDeclarationFragment fragment) {
IVariableBinding binding= fragment.resolveBinding();
if (binding == null || binding.isField() || binding.isParameter()
|| !(fragment.getParent() instanceof VariableDeclarationStatement declaration)
|| declaration.fragments().size() != 1
|| !(fragment.getInitializer() instanceof ArrayCreation creation)
|| creation.getInitializer() != null
|| creation.dimensions().size() != 1
|| !isBooleanArray(declaration.getType().resolveBinding())
|| !isBooleanArray(creation.getType().resolveBinding())) {
return Optional.empty();
}
Expression dimension= (Expression) creation.dimensions().get(0);
ITypeBinding enumType= enumTypeFromValuesLength(dimension);
MethodDeclaration method= enclosingMethod(fragment);
String bindingKey= binding.getVariableDeclaration().getKey();
if (enumType == null || method == null || method.getBody() == null
|| bindingKey == null || bindingKey.isBlank()) {
return Optional.empty();
}
return Optional.of(new Candidate(
fragment, method, binding.getVariableDeclaration(), bindingKey,
enumType.getTypeDeclaration(), dimension));
}
private static ContainerUsageProfile analyzeCandidate(Candidate candidate) {
Observations observations= new Observations(candidate);
candidate.method().getBody().accept(new ASTVisitor() {
@Override
public boolean visit(SimpleName name) {
if (hasBinding(name.resolveBinding(), candidate.bindingKey())) {
classify(name, candidate, observations);
}
return true;
}
});
return observations.profile();
}
private static void classify(
SimpleName name,
Candidate candidate,
Observations observations) {
observations.bindingSeen= true;
if (candidate.fragment().getName() == name) {
return;
}
if (crossesExecutableBoundary(name, candidate.binding())) {
observations.reject(Kind.CAPTURED_USAGE,
"Enum membership table is captured across an executable boundary", name); //$NON-NLS-1$
return;
}
Expression reference= completeReferenceExpression(name, candidate.bindingKey());
ASTNode parent= reference.getParent();
if (parent instanceof ArrayAccess access && access.getArray() == reference) {
classifyArrayAccess(access, candidate, observations);
} else if (isLengthRead(reference, parent)) {
observations.reject(Kind.ARRAY_LENGTH_READ,
"Array cardinality is observed; set size would represent enabled members instead", //$NON-NLS-1$
reference);
} else if (parent instanceof VariableDeclarationFragment fragment
&& fragment.getInitializer() == reference) {
observations.reject(Kind.UNSAFE_ESCAPE,
"Enum membership table is aliased to another local variable", reference); //$NON-NLS-1$
} else if (parent instanceof Assignment assignment
&& assignment.getRightHandSide() == reference) {
observations.reject(Kind.UNSAFE_ESCAPE,
"Enum membership table is assigned to another variable or field", reference); //$NON-NLS-1$
} else if (parent instanceof ReturnStatement) {
observations.reject(Kind.UNSAFE_ESCAPE,
"Enum membership table escapes through a return statement", reference); //$NON-NLS-1$
} else if (parent instanceof MethodInvocation invocation
&& invocation.arguments().contains(reference)) {
observations.reject(Kind.UNSAFE_ESCAPE,
"Enum membership table is passed to another method", reference); //$NON-NLS-1$
} else if (parent instanceof InfixExpression infix && isIdentityComparison(infix)) {
observations.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) {
observations.reject(Kind.ARRAY_IDENTITY,
"Array identity is used as a synchronization monitor", reference); //$NON-NLS-1$
} else {
observations.reject(Kind.UNCLASSIFIED_USAGE,
"Array use is outside the enum-membership contract", reference); //$NON-NLS-1$
}
}
private static void classifyArrayAccess(
ArrayAccess access,
Candidate candidate,
Observations observations) {
if (!isOrdinalIndex(access.getIndex(), candidate.enumType())) {
observations.reject(Kind.REJECTION_BOUNDARY,
"Array index is not ordinal() of the enum that defines table cardinality", //$NON-NLS-1$
access.getIndex());
return;
}
observations.add(Kind.ENUM_ORDINAL_INDEX,
"Membership slot is selected by ordinal() of " //$NON-NLS-1$
+ candidate.enumType().getQualifiedName(),
access.getIndex());
ASTNode parent= access.getParent();
if (parent instanceof Assignment assignment
&& assignment.getLeftHandSide() == access) {
if (assignment.getOperator() != Assignment.Operator.ASSIGN
|| !(unwrap(assignment.getRightHandSide()) instanceof BooleanLiteral literal)) {
observations.reject(Kind.REJECTION_BOUNDARY,
"Membership writes must assign literal true or false", assignment); //$NON-NLS-1$
return;
}
observations.write(literal.booleanValue(), assignment);
return;
}
observations.read(access);
}
private static ITypeBinding enumTypeFromValuesLength(Expression expression) {
Expression unwrapped= unwrap(expression);
Expression receiver;
if (unwrapped instanceof FieldAccess field
&& "length".equals(field.getName().getIdentifier())) { //$NON-NLS-1$
receiver= field.getExpression();
} else {
return null;
}
Expression valuesExpression= unwrap(receiver);
if (!(valuesExpression instanceof MethodInvocation values)
|| !"values".equals(values.getName().getIdentifier()) //$NON-NLS-1$
|| !values.arguments().isEmpty()) {
return null;
}
IMethodBinding method= values.resolveMethodBinding();
if (method == null || method.getDeclaringClass() == null
|| !method.getDeclaringClass().isEnum()) {
return null;
}
ITypeBinding returnType= method.getReturnType();
if (returnType == null || !returnType.isArray()) {
return null;
}
ITypeBinding component= returnType.getComponentType();
return component != null && sameType(component, method.getDeclaringClass())
? component.getTypeDeclaration() : null;
}
private static boolean isOrdinalIndex(Expression expression, ITypeBinding enumType) {
Expression unwrapped= unwrap(expression);
if (!(unwrapped instanceof MethodInvocation ordinal)
|| !"ordinal".equals(ordinal.getName().getIdentifier()) //$NON-NLS-1$
|| !ordinal.arguments().isEmpty()
|| ordinal.getExpression() == null) {
return false;
}
ITypeBinding receiverType= ordinal.getExpression().resolveTypeBinding();
IMethodBinding method= ordinal.resolveMethodBinding();
return receiverType != null && sameType(receiverType, enumType)
&& method != null && method.getDeclaringClass() != null
&& "java.lang.Enum".equals( //$NON-NLS-1$
method.getDeclaringClass().getErasure().getQualifiedName());
}
private static boolean isBooleanArray(ITypeBinding type) {
if (type == null || !type.isArray() || type.getDimensions() != 1) {
return false;
}
ITypeBinding component= type.getComponentType();
return component != null && component.isPrimitive()
&& "boolean".equals(component.getName()); //$NON-NLS-1$
}
private static boolean sameType(ITypeBinding left, ITypeBinding right) {
if (left == null || right == null) {
return false;
}
return left.getTypeDeclaration().isEqualTo(right.getTypeDeclaration());
}
private static MethodDeclaration enclosingMethod(ASTNode node) {
for (ASTNode current= node.getParent(); current != null; current= current.getParent()) {
if (current instanceof MethodDeclaration) {
return (MethodDeclaration) current;
}
}
return null;
}
private static boolean crossesExecutableBoundary(
ASTNode reference,
IVariableBinding declaration) {
IMethodBinding declaringMethod= declaration.getDeclaringMethod();
if (declaringMethod == null) {
return true;
}
for (ASTNode current= reference.getParent(); current != null; current= current.getParent()) {
if (current instanceof LambdaExpression
|| current instanceof AnonymousClassDeclaration
|| current instanceof AbstractTypeDeclaration) {
return true;
}
if (current instanceof MethodDeclaration method) {
return !sameMethod(declaringMethod, method.resolveBinding());
}
}
return true;
}
private static boolean sameMethod(IMethodBinding left, IMethodBinding right) {
if (left == null || right == null) {
return false;
}
String leftKey= left.getMethodDeclaration().getKey();
String rightKey= right.getMethodDeclaration().getKey();
return leftKey != null && leftKey.equals(rightKey);
}
private static boolean hasBinding(IBinding binding, String key) {
return binding instanceof IVariableBinding variable
&& key.equals(variable.getVariableDeclaration().getKey());
}
private static Expression completeReferenceExpression(SimpleName name, String bindingKey) {
Expression reference= name;
ASTNode parent= reference.getParent();
if (parent instanceof QualifiedName qualified
&& qualified.getName() == reference
&& hasBinding(qualified.resolveBinding(), bindingKey)) {
reference= qualified;
} else if (parent instanceof FieldAccess field
&& field.getName() == reference
&& hasBinding(field.resolveFieldBinding(), bindingKey)) {
reference= field;
}
while (reference.getParent() instanceof ParenthesizedExpression parenthesized) {
reference= parenthesized;
}
return reference;
}
private static boolean isLengthRead(Expression reference, ASTNode parent) {
if (parent instanceof QualifiedName qualified) {
return qualified.getQualifier() == reference
&& "length".equals(qualified.getName().getIdentifier()); //$NON-NLS-1$
}
return parent instanceof FieldAccess field
&& field.getExpression() == reference
&& "length".equals(field.getName().getIdentifier()); //$NON-NLS-1$
}
private static boolean isIdentityComparison(InfixExpression expression) {
return expression.getOperator() == InfixExpression.Operator.EQUALS
|| expression.getOperator() == InfixExpression.Operator.NOT_EQUALS;
}
private static Expression unwrap(Expression expression) {
Expression current= expression;
while (current instanceof ParenthesizedExpression parenthesized) {
current= parenthesized.getExpression();
}
return current;
}
private record Candidate(
VariableDeclarationFragment fragment,
MethodDeclaration method,
IVariableBinding binding,
String bindingKey,
ITypeBinding enumType,
Expression cardinalityExpression) {
}
private static final class Observations {
private final Candidate candidate;
private final List<UsageEvidence> evidence= new ArrayList<>();
private boolean rejected;
private boolean bindingSeen;
private int reads;
private int writes;
Observations(Candidate candidate) {
this.candidate= candidate;
add(Kind.ENUM_CARDINALITY,
"Boolean table cardinality is defined by " //$NON-NLS-1$
+ candidate.enumType().getQualifiedName() + ".values().length", //$NON-NLS-1$
candidate.cardinalityExpression());
}
void read(ASTNode node) {
reads++;
add(Kind.ENUM_MEMBERSHIP_QUERY,
"Boolean table lookup observes membership of one enum constant", node); //$NON-NLS-1$
}
void write(boolean enabled, ASTNode node) {
writes++;
add(enabled ? Kind.ENUM_MEMBERSHIP_ENABLE : Kind.ENUM_MEMBERSHIP_DISABLE,
enabled
? "Literal true enables membership of one enum constant" //$NON-NLS-1$
: "Literal false disables membership of one enum constant", //$NON-NLS-1$
node);
}
void reject(Kind kind, String summary, ASTNode node) {
rejected= true;
add(kind, summary, node);
}
void add(Kind kind, String summary, ASTNode node) {
evidence.add(new UsageEvidence(kind, summary, node.getStartPosition(), node.getLength()));
}
ContainerUsageProfile profile() {
boolean complete= bindingSeen && reads > 0 && writes > 0 && !rejected;
if (reads == 0) {
reject(Kind.REJECTION_BOUNDARY,
"No enum membership query was observed", candidate.fragment()); //$NON-NLS-1$
complete= false;
}
if (writes == 0) {
reject(Kind.REJECTION_BOUNDARY,
"No enum membership state write was observed", candidate.fragment()); //$NON-NLS-1$
complete= false;
}
if (complete) {
add(Kind.LOCAL_USAGE_COMPLETE,
"Every use of the enum-indexed membership table was classified", //$NON-NLS-1$
candidate.fragment());
}
evidence.sort(Comparator.comparingInt(UsageEvidence::sourceStart)
.thenComparing(item -> item.kind().ordinal()));
return new ContainerUsageProfile(
new ContainerIdentity(
candidate.bindingKey(), candidate.fragment().getName().getIdentifier(),
candidate.fragment().getStartPosition(), candidate.fragment().getLength()),
ContainerShape.ARRAY,
ElementDomain.PRIMITIVE,
new AccessProfile(reads > 0, writes > 0, false, false, false, reads > 0, false),
OrderRequirement.NONE,
UniquenessRequirement.REQUIRED,
MutationLifecycle.CONTINUOUSLY_MUTABLE,
NullContract.NOT_APPLICABLE,
complete ? AliasingContract.NO_OBSERVED_ALIAS : AliasingContract.UNKNOWN,
EscapeLevel.LOCAL,
complete ? threadConfined() : ConcurrencyProfile.unknown(),
complete ? AnalysisCompleteness.LOCAL_USAGE_COMPLETE
: AnalysisCompleteness.REJECTED,
evidence);
}
}
private static ConcurrencyProfile threadConfined() {
return new ConcurrencyProfile(
ThreadExposure.THREAD_CONFINED,
SynchronizationKind.NONE,
IterationSemantics.UNKNOWN,
AtomicityRequirement.INDIVIDUAL_OPERATIONS,
WorkloadShape.UNKNOWN);
}
}