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

import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

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.IField;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IType;
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.ContainerFlowContinuationPlan;
import org.sandbox.jdt.container.api.ContainerFlowContinuationPlan.ContinuationKind;
import org.sandbox.jdt.container.api.ContainerFlowContinuationPlan.DiagnosticKind;
import org.sandbox.jdt.container.api.ContainerFlowSearchPlan.SearchKind;
import org.sandbox.jdt.container.api.ResolvedContainerFlowSearchPlan;
import org.sandbox.jdt.container.api.ResolvedContainerFlowSearchPlan.ResolvedSearchTarget;
import org.sandbox.jdt.container.api.ResolvedContainerFlowSearchPlan.TargetKind;
import org.sandbox.jdt.ui.tests.quickfix.rules.AbstractEclipseJava;
import org.sandbox.jdt.ui.tests.quickfix.rules.EclipseJava22;

class ContainerFlowContinuationDetectorTest {

	@RegisterExtension
	final AbstractEclipseJava context= new EclipseJava22();

	private final ContainerFlowContinuationDetector detector=
			new ContainerFlowContinuationDetector();

	@Test
	void discoversTransparentFieldAndSignatureContinuations() throws CoreException {
		ICompilationUnit unit= createUnit("""
			package test;
			class Sample {
				String[] field = new String[0];

				void consume(String[] input) {
					String[] alias = input;
				}

				String[] produce() {
					String[] result = field;
					return result;
				}

				void caller(String[] values) {
					consume(values);
					String[] received = produce();
				}
			}
			""");
		IType type= unit.getType("Sample"); //$NON-NLS-1$
		IField field= type.getField("field"); //$NON-NLS-1$
		IMethod consume= method(type, "consume"); //$NON-NLS-1$
		IMethod produce= method(type, "produce"); //$NON-NLS-1$

		ResolvedContainerFlowSearchPlan targets= new ResolvedContainerFlowSearchPlan(List.of(
				fieldTarget(field),
				methodTarget("parameter:consume:0", consume, //$NON-NLS-1$
						SearchKind.METHOD_OVERRIDE_FAMILY, 0),
				methodTarget("parameter:consume:0", consume, //$NON-NLS-1$
						SearchKind.METHOD_CALLERS, 0),
				methodTarget("return:produce", produce, //$NON-NLS-1$
						SearchKind.METHOD_OVERRIDE_FAMILY, -1),
				methodTarget("return:produce", produce, //$NON-NLS-1$
						SearchKind.METHOD_CALLERS, -1)));

		ContainerFlowContinuationPlan plan= detector.detect(parse(unit), targets);

		assertTrue(plan.complete());
		Set<ContinuationKind> kinds= plan.roots().stream()
				.map(ContainerFlowContinuationPlan.ContinuationRoot::kind)
				.collect(Collectors.toSet());
		assertEquals(Set.of(
				ContinuationKind.FIELD,
				ContinuationKind.PARAMETER_DECLARATION,
				ContinuationKind.CALL_ARGUMENT,
				ContinuationKind.RETURN_EXPRESSION,
				ContinuationKind.RETURN_CONSUMER), kinds);
		assertEquals(5, plan.roots().size());
		assertTrue(plan.roots().stream()
				.allMatch(root -> root.profile().identity().hasResolvedBinding()));
	}

	@Test
	void rejectsComplexTransfersAndIgnoresLambdaReturns() throws CoreException {
		ICompilationUnit unit= createUnit("""
			package test;
			import java.util.function.Supplier;
			class Sample {
				void consume(String[] input) { }

				String[] produce() {
					Supplier<String[]> nested = () -> {
						return new String[0];
					};
					return new String[0];
				}

				void caller() {
					consume(new String[0]);
					int length = produce().length;
					java.util.function.Consumer<String[]> reference = this::consume;
				}
			}
			""");
		IType type= unit.getType("Sample"); //$NON-NLS-1$
		IMethod consume= method(type, "consume"); //$NON-NLS-1$
		IMethod produce= method(type, "produce"); //$NON-NLS-1$
		ResolvedContainerFlowSearchPlan targets= new ResolvedContainerFlowSearchPlan(List.of(
				methodTarget("parameter:consume:0", consume, //$NON-NLS-1$
						SearchKind.METHOD_CALLERS, 0),
				methodTarget("return:produce", produce, //$NON-NLS-1$
						SearchKind.METHOD_OVERRIDE_FAMILY, -1),
				methodTarget("return:produce", produce, //$NON-NLS-1$
						SearchKind.METHOD_CALLERS, -1)));

		ContainerFlowContinuationPlan plan= detector.detect(parse(unit), targets);

		assertFalse(plan.complete());
		Set<DiagnosticKind> kinds= plan.diagnostics().stream()
				.map(ContainerFlowContinuationPlan.ContinuationDiagnostic::kind)
				.collect(Collectors.toSet());
		assertTrue(kinds.contains(DiagnosticKind.UNSUPPORTED_ARGUMENT));
		assertTrue(kinds.contains(DiagnosticKind.UNSUPPORTED_RETURN_EXPRESSION));
		assertTrue(kinds.contains(DiagnosticKind.UNSUPPORTED_RETURN_CONSUMER));
		assertTrue(kinds.contains(DiagnosticKind.METHOD_REFERENCE));
		long returnDiagnostics= plan.diagnostics().stream()
				.filter(diagnostic -> diagnostic.kind()
						== DiagnosticKind.UNSUPPORTED_RETURN_EXPRESSION)
				.count();
		assertEquals(1, returnDiagnostics,
				"The return inside the lambda must not be attributed to produce()."); //$NON-NLS-1$
	}

	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);
	}

	private static IMethod method(IType type, String name) throws CoreException {
		return Arrays.stream(type.getMethods())
				.filter(candidate -> candidate.getElementName().equals(name))
				.findFirst()
				.orElseThrow(() -> new IllegalStateException("Missing method: " + name)); //$NON-NLS-1$
	}

	private static ResolvedSearchTarget fieldTarget(IField field) {
		return new ResolvedSearchTarget(
				"field:field", //$NON-NLS-1$
				SearchKind.FIELD_REFERENCES,
				TargetKind.FIELD,
				"field-binding", //$NON-NLS-1$
				"type-key", //$NON-NLS-1$
				field.getHandleIdentifier(),
				-1,
				"Continue field flow"); //$NON-NLS-1$
	}

	private static ResolvedSearchTarget methodTarget(
			String sourceNodeId,
			IMethod method,
			SearchKind searchKind,
			int signatureIndex) {
		return new ResolvedSearchTarget(
				sourceNodeId,
				searchKind,
				TargetKind.METHOD,
				"parameter-binding", //$NON-NLS-1$
				"method-key", //$NON-NLS-1$
				method.getHandleIdentifier(),
				signatureIndex,
				"Continue method flow"); //$NON-NLS-1$
	}
}