EnumIndexedMembershipContractIntegrationTest.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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import org.eclipse.core.runtime.CoreException;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.CompilationUnit;

import org.sandbox.jdt.container.api.ContainerRecommendation;
import org.sandbox.jdt.container.api.ContainerRecommendation.AutomationLevel;
import org.sandbox.jdt.container.api.ContainerRecommendation.Confidence;
import org.sandbox.jdt.container.api.ContainerRecommendation.ContractProperty;
import org.sandbox.jdt.container.api.ContainerRecommendation.Preservation;
import org.sandbox.jdt.container.api.ContainerShape;
import org.sandbox.jdt.container.api.ContainerUsageProfile;
import org.sandbox.jdt.container.api.ContainerUsageProfile.AnalysisCompleteness;
import org.sandbox.jdt.container.api.ContainerUsageProfile.ElementDomain;
import org.sandbox.jdt.container.api.ContainerUsageProfile.NullContract;
import org.sandbox.jdt.container.api.UsageEvidence.Kind;
import org.sandbox.jdt.ui.tests.quickfix.rules.AbstractEclipseJava;
import org.sandbox.jdt.ui.tests.quickfix.rules.EclipseJava22;

class EnumIndexedMembershipContractIntegrationTest {

	@RegisterExtension
	final AbstractEclipseJava context= new EclipseJava22();

	private final LocalEnumIndexedMembershipAnalyzer analyzer=
			new LocalEnumIndexedMembershipAnalyzer();
	private final ContainerContractInferrer inferrer= new ContainerContractInferrer();

	@Test
	void infersReportOnlySetFromEnumIndexedBooleanTable() throws CoreException {
		ContainerUsageProfile profile= analyze("""
			package test;
			class Sample {
				enum Flag { A, B, C }
				boolean update(Flag flag) {
					boolean[] enabled = new boolean[Flag.values().length];
					enabled[flag.ordinal()] = true;
					boolean present = enabled[flag.ordinal()];
					enabled[flag.ordinal()] = false;
					return present;
				}
			}
			""");

		assertEquals(AnalysisCompleteness.LOCAL_USAGE_COMPLETE, profile.completeness());
		assertEquals(ContainerShape.ARRAY, profile.currentShape());
		assertEquals(ElementDomain.PRIMITIVE, profile.elementDomain());
		assertEquals(NullContract.NOT_APPLICABLE, profile.nullContract());
		assertTrue(profile.access().indexedRead());
		assertTrue(profile.access().indexedWrite());
		assertTrue(profile.access().membershipQuery());
		assertTrue(hasEvidence(profile, Kind.ENUM_CARDINALITY));
		assertTrue(hasEvidence(profile, Kind.ENUM_ORDINAL_INDEX));
		assertTrue(hasEvidence(profile, Kind.ENUM_MEMBERSHIP_QUERY));
		assertTrue(hasEvidence(profile, Kind.ENUM_MEMBERSHIP_ENABLE));
		assertTrue(hasEvidence(profile, Kind.ENUM_MEMBERSHIP_DISABLE));
		assertTrue(hasEvidence(profile, Kind.LOCAL_USAGE_COMPLETE));

		ContainerRecommendation recommendation= inferrer.infer(profile).orElseThrow();
		assertEquals(ContainerShape.SET, recommendation.targetContract().shape());
		assertEquals(AutomationLevel.REPORT_ONLY, recommendation.automationLevel());
		assertEquals(Confidence.HIGH, recommendation.confidence());
		assertEquals(ContainerRuleRegistry.ENUM_INDEXED_MEMBERSHIP_SET,
				recommendation.rule().ruleId());
		assertEquals(Preservation.REQUIRES_PROOF,
				recommendation.assessments().stream()
						.filter(assessment -> assessment.property() == ContractProperty.NULLS)
						.findFirst().orElseThrow().preservation());
	}

	@Test
	void numericIndexRejectsOrdinalMembershipInference() throws CoreException {
		ContainerUsageProfile profile= analyze("""
			package test;
			class Sample {
				enum Flag { A, B }
				boolean update(Flag flag) {
					boolean[] enabled = new boolean[Flag.values().length];
					enabled[0] = true;
					return enabled[flag.ordinal()];
				}
			}
			""");

		assertRejected(profile, Kind.REJECTION_BOUNDARY);
	}

	@Test
	void ordinalFromDifferentEnumRejectsMembershipInference() throws CoreException {
		ContainerUsageProfile profile= analyze("""
			package test;
			class Sample {
				enum Flag { A, B }
				enum Other { A, B }
				boolean update(Flag flag, Other other) {
					boolean[] enabled = new boolean[Flag.values().length];
					enabled[other.ordinal()] = true;
					return enabled[flag.ordinal()];
				}
			}
			""");

		assertRejected(profile, Kind.REJECTION_BOUNDARY);
	}

	@Test
	void ordinalFromDifferentLocalEnumRejectsMembershipInference() throws CoreException {
		ContainerUsageProfile profile= analyze("""
			package test;
			class Sample {
				boolean update() {
					enum Flag { A, B }
					enum Other { A, B }
					Flag flag = Flag.A;
					Other other = Other.A;
					boolean[] enabled = new boolean[Flag.values().length];
					enabled[other.ordinal()] = true;
					return enabled[flag.ordinal()];
				}
			}
			""");

		assertRejected(profile, Kind.REJECTION_BOUNDARY);
	}

	@Test
	void arrayLengthObservationRejectsSetContract() throws CoreException {
		ContainerUsageProfile profile= analyze("""
			package test;
			class Sample {
				enum Flag { A, B }
				boolean update(Flag flag) {
					boolean[] enabled = new boolean[Flag.values().length];
					enabled[flag.ordinal()] = true;
					int capacity = enabled.length;
					return enabled[flag.ordinal()] && capacity > 0;
				}
			}
			""");

		assertRejected(profile, Kind.ARRAY_LENGTH_READ);
	}

	@Test
	void localAliasRejectsSetContract() throws CoreException {
		ContainerUsageProfile profile= analyze("""
			package test;
			class Sample {
				enum Flag { A, B }
				boolean update(Flag flag) {
					boolean[] enabled = new boolean[Flag.values().length];
					boolean[] alias = enabled;
					enabled[flag.ordinal()] = true;
					return alias[flag.ordinal()];
				}
			}
			""");

		assertRejected(profile, Kind.UNSAFE_ESCAPE);
	}

	@Test
	void lambdaCaptureRejectsSetContract() throws CoreException {
		ContainerUsageProfile profile= analyze("""
			package test;
			import java.util.function.BooleanSupplier;
			class Sample {
				enum Flag { A, B }
				boolean update(Flag flag) {
					boolean[] enabled = new boolean[Flag.values().length];
					enabled[flag.ordinal()] = true;
					BooleanSupplier reader = () -> enabled[flag.ordinal()];
					return reader.getAsBoolean();
				}
			}
			""");

		assertRejected(profile, Kind.CAPTURED_USAGE);
	}

	@Test
	void arrayIdentityComparisonRejectsSetContract() throws CoreException {
		ContainerUsageProfile profile= analyze("""
			package test;
			class Sample {
				enum Flag { A, B }
				boolean update(Flag flag) {
					boolean[] enabled = new boolean[Flag.values().length];
					enabled[flag.ordinal()] = true;
					boolean same = enabled == enabled;
					return same && enabled[flag.ordinal()];
				}
			}
			""");

		assertRejected(profile, Kind.ARRAY_IDENTITY);
	}

	@Test
	void synchronizationOnArrayIdentityRejectsSetContract() throws CoreException {
		ContainerUsageProfile profile= analyze("""
			package test;
			class Sample {
				enum Flag { A, B }
				boolean update(Flag flag) {
					boolean[] enabled = new boolean[Flag.values().length];
					enabled[flag.ordinal()] = true;
					synchronized (enabled) {
						return enabled[flag.ordinal()];
					}
				}
			}
			""");

		assertRejected(profile, Kind.ARRAY_IDENTITY);
	}

	@Test
	void writeOnlyTableIsNotPromotedToMembershipContract() throws CoreException {
		ContainerUsageProfile profile= analyze("""
			package test;
			class Sample {
				enum Flag { A, B }
				void update(Flag flag) {
					boolean[] enabled = new boolean[Flag.values().length];
					enabled[flag.ordinal()] = true;
				}
			}
			""");

		assertRejected(profile, Kind.REJECTION_BOUNDARY);
		assertTrue(profile.evidence().stream()
				.anyMatch(evidence -> evidence.summary().contains("No enum membership query"))); //$NON-NLS-1$
	}

	@Test
	void computedMembershipWriteIsRejected() throws CoreException {
		ContainerUsageProfile profile= analyze("""
			package test;
			class Sample {
				enum Flag { A, B }
				boolean update(Flag flag, boolean state) {
					boolean[] enabled = new boolean[Flag.values().length];
					enabled[flag.ordinal()] = state;
					return enabled[flag.ordinal()];
				}
			}
			""");

		assertRejected(profile, Kind.REJECTION_BOUNDARY);
	}

	private ContainerUsageProfile analyze(String source) throws CoreException {
		ICompilationUnit unit= createUnit(source);
		List<ContainerUsageProfile> profiles= analyzer.analyze(parse(unit));
		assertEquals(1, profiles.size());
		return profiles.get(0);
	}

	private void assertRejected(ContainerUsageProfile profile, Kind expectedEvidence) {
		assertEquals(AnalysisCompleteness.REJECTED, profile.completeness());
		assertTrue(hasEvidence(profile, expectedEvidence));
		assertFalse(inferrer.infer(profile).isPresent());
	}

	private static boolean hasEvidence(ContainerUsageProfile profile, Kind kind) {
		return profile.evidence().stream().anyMatch(evidence -> evidence.kind() == kind);
	}

	private ICompilationUnit createUnit(String source) throws CoreException {
		IPackageFragment fragment= context.getSourceFolder()
				.createPackageFragment("test", false, null); //$NON-NLS-1$
		return fragment.createCompilationUnit("Sample.java", source, true, null); //$NON-NLS-1$
	}

	private CompilationUnit parse(ICompilationUnit unit) {
		ASTParser parser= ASTParser.newParser(AST.getJLSLatest());
		parser.setProject(context.getJavaProject());
		parser.setSource(unit);
		parser.setResolveBindings(true);
		parser.setBindingsRecovery(true);
		parser.setStatementsRecovery(true);
		return (CompilationUnit) parser.createAST(null);
	}
}