ContainerAnalysisService.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.ui.helper.views;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import org.eclipse.jdt.core.ICompilationUnit;
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.analysis.AppendOnlyArraySeedDetector;
import org.sandbox.jdt.container.analysis.ContainerContractInferrer;
import org.sandbox.jdt.container.analysis.LocalArrayUsageAnalyzer;
import org.sandbox.jdt.container.analysis.LocalDequeSequenceAnalyzer;
import org.sandbox.jdt.container.analysis.LocalEnumIndexedMembershipAnalyzer;
import org.sandbox.jdt.container.analysis.LocalUniqueSequenceAnalyzer;
import org.sandbox.jdt.container.api.ContainerRecommendation;
import org.sandbox.jdt.container.api.ContainerRecommendation.ContractAssessment;
import org.sandbox.jdt.container.api.ContainerRecommendation.Preservation;
import org.sandbox.jdt.container.api.ContainerUsageProfile;
import org.sandbox.jdt.container.api.ContainerUsageProfile.AnalysisCompleteness;
import org.sandbox.jdt.container.api.TargetContainerContract;
import org.sandbox.jdt.container.api.UsageEvidence;

/**
 * Read-only adapter from the shared semantic container analysis to Usage View
 * presentation rows. It deliberately owns no rewrite, readiness or migration
 * policy.
 */
final class ContainerAnalysisService {

	private final AppendOnlyArraySeedDetector arraySeedDetector= new AppendOnlyArraySeedDetector();
	private final LocalArrayUsageAnalyzer arrayUsageAnalyzer= new LocalArrayUsageAnalyzer();
	private final LocalUniqueSequenceAnalyzer uniqueSequenceAnalyzer= new LocalUniqueSequenceAnalyzer();
	private final LocalDequeSequenceAnalyzer dequeSequenceAnalyzer= new LocalDequeSequenceAnalyzer();
	private final LocalEnumIndexedMembershipAnalyzer enumMembershipAnalyzer=
			new LocalEnumIndexedMembershipAnalyzer();
	private final ContainerContractInferrer contractInferrer= new ContainerContractInferrer();

	List<ContainerAnalysisRow> analyze(ICompilationUnit unit) {
		if (unit == null || !unit.exists()) {
			return List.of();
		}
		return analyze(parse(unit), unit.getPrimary().getHandleIdentifier());
	}

	List<ContainerAnalysisRow> analyze(CompilationUnit root, String compilationUnitHandle) {
		if (root == null || compilationUnitHandle == null || compilationUnitHandle.isBlank()) {
			return List.of();
		}
		Map<String, ContainerUsageProfile> profiles= new LinkedHashMap<>();
		for (ContainerUsageProfile seed : arraySeedDetector.findSeeds(root)) {
			addProfile(profiles, arrayUsageAnalyzer.analyze(root, seed));
		}
		uniqueSequenceAnalyzer.analyze(root).forEach(profile -> addProfile(profiles, profile));
		dequeSequenceAnalyzer.analyze(root).forEach(profile -> addProfile(profiles, profile));
		enumMembershipAnalyzer.analyze(root).forEach(profile -> addProfile(profiles, profile));

		List<ContainerAnalysisRow> rows= new ArrayList<>();
		for (ContainerUsageProfile profile : profiles.values()) {
			Optional<ContainerRecommendation> recommendation= contractInferrer.infer(profile);
			if (profile.evidence().isEmpty()) {
				rows.add(row(compilationUnitHandle, profile, recommendation, null));
				continue;
			}
			for (UsageEvidence evidence : profile.evidence()) {
				rows.add(row(compilationUnitHandle, profile, recommendation, evidence));
			}
		}
		rows.sort(Comparator.comparingInt(ContainerAnalysisRow::sourceStart)
				.thenComparing(ContainerAnalysisRow::candidate)
				.thenComparing(ContainerAnalysisRow::evidenceKind));
		return List.copyOf(rows);
	}

	private static void addProfile(Map<String, ContainerUsageProfile> profiles,
			ContainerUsageProfile profile) {
		profiles.merge(profile.identity().stableId(), profile, ContainerAnalysisService::preferProfile);
	}

	private static ContainerUsageProfile preferProfile(
			ContainerUsageProfile existing, ContainerUsageProfile candidate) {
		int existingRank= completenessRank(existing.completeness());
		int candidateRank= completenessRank(candidate.completeness());
		if (candidateRank != existingRank) {
			return candidateRank > existingRank ? candidate : existing;
		}
		return candidate.evidence().size() > existing.evidence().size() ? candidate : existing;
	}

	private static int completenessRank(AnalysisCompleteness completeness) {
		return switch (completeness) {
			case FLOW_COMPLETE -> 4;
			case LOCAL_USAGE_COMPLETE -> 3;
			case LOCAL_SEED -> 2;
			case REJECTED -> 1;
		};
	}

	private static ContainerAnalysisRow row(String compilationUnitHandle,
			ContainerUsageProfile profile, Optional<ContainerRecommendation> recommendation,
			UsageEvidence evidence) {
		String target= recommendation.map(ContainerAnalysisService::targetSummary)
				.orElse("No proven target"); //$NON-NLS-1$
		String confidence= recommendation.map(value -> value.confidence().name())
				.orElse("N/A"); //$NON-NLS-1$
		String status= recommendation.map(value -> value.automationLevel().name())
				.orElseGet(() -> profile.completeness().name());
		String semanticSummary= recommendation.map(ContainerAnalysisService::semanticSummary)
				.orElseGet(() -> rejectionSummary(profile));
		int sourceStart= evidence == null ? profile.identity().sourceStart() : evidence.sourceStart();
		int sourceLength= evidence == null ? profile.identity().sourceLength() : evidence.sourceLength();
		String evidenceKind= evidence == null ? "CANDIDATE" : evidence.kind().name(); //$NON-NLS-1$
		String evidenceSummary= evidence == null
				? "Container candidate has no additional evidence rows" //$NON-NLS-1$
				: evidence.summary();
		return new ContainerAnalysisRow(
				compilationUnitHandle,
				profile.identity().displayName(),
				profile.currentShape().name(),
				target,
				confidence,
				status,
				evidenceKind,
				evidenceSummary,
				semanticSummary,
				sourceStart,
				sourceLength);
	}

	private static String targetSummary(ContainerRecommendation recommendation) {
		TargetContainerContract target= recommendation.targetContract();
		return "%s; order=%s; uniqueness=%s; mutability=%s; nulls=%s".formatted( //$NON-NLS-1$
				target.shape(), target.orderRequirement(), target.uniquenessRequirement(),
				target.mutability(), target.nullContract());
	}

	private static String semanticSummary(ContainerRecommendation recommendation) {
		String assessments= recommendation.assessments().stream()
				.filter(assessment -> assessment.preservation() != Preservation.PRESERVED)
				.map(ContainerAnalysisService::assessmentSummary)
				.collect(Collectors.joining(System.lineSeparator()));
		if (assessments.isBlank()) {
			assessments= recommendation.assessments().stream()
					.map(ContainerAnalysisService::assessmentSummary)
					.collect(Collectors.joining(System.lineSeparator()));
		}
		return recommendation.targetContract().rationale()
				+ (assessments.isBlank() ? "" : System.lineSeparator() + assessments); //$NON-NLS-1$
	}

	private static String assessmentSummary(ContractAssessment assessment) {
		return "%s: %s — %s".formatted(assessment.property(), assessment.preservation(), //$NON-NLS-1$
				assessment.explanation());
	}

	private static String rejectionSummary(ContainerUsageProfile profile) {
		return profile.evidence().stream()
				.filter(evidence -> evidence.kind() == UsageEvidence.Kind.REJECTION_BOUNDARY
						|| evidence.kind() == UsageEvidence.Kind.UNSAFE_ESCAPE
						|| evidence.kind() == UsageEvidence.Kind.UNRESOLVED_BINDING
						|| evidence.kind() == UsageEvidence.Kind.UNCLASSIFIED_USAGE
						|| evidence.kind() == UsageEvidence.Kind.UNSUPPORTED_CONTINUATION
						|| evidence.kind() == UsageEvidence.Kind.CAPTURED_USAGE
						|| evidence.kind() == UsageEvidence.Kind.ARRAY_IDENTITY
						|| evidence.kind() == UsageEvidence.Kind.ARRAY_LENGTH_READ)
				.map(UsageEvidence::summary)
				.distinct()
				.collect(Collectors.joining(System.lineSeparator()));
	}

	private static CompilationUnit parse(ICompilationUnit unit) {
		ASTParser parser= ASTParser.newParser(AST.getJLSLatest());
		parser.setKind(ASTParser.K_COMPILATION_UNIT);
		parser.setSource(unit);
		parser.setResolveBindings(true);
		parser.setBindingsRecovery(true);
		parser.setStatementsRecovery(true);
		parser.setCompilerOptions(unit.getJavaProject().getOptions(true));
		return (CompilationUnit) parser.createAST(null);
	}

	record ContainerAnalysisRow(
			String compilationUnitHandle,
			String candidate,
			String currentShape,
			String targetContract,
			String confidence,
			String status,
			String evidenceKind,
			String evidenceSummary,
			String semanticSummary,
			int sourceStart,
			int sourceLength) {

		String details() {
			return "Candidate: %s%nCurrent: %s%nTarget: %s%nConfidence: %s%nStatus: %s%nEvidence: %s — %s%n%n%s" //$NON-NLS-1$
					.formatted(candidate, currentShape, targetContract, confidence, status,
							evidenceKind, evidenceSummary, semanticSummary);
		}
	}
}