LoopConversionService.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 is available at
 * https://www.eclipse.org/legal/epl-2.0/
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.helper;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.ForStatement;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.dom.WhileStatement;
import org.eclipse.jdt.core.refactoring.CompilationUnitChange;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation;
import org.sandbox.jdt.internal.corext.fix.UseFunctionalCallFixCore;
import org.sandbox.jdt.internal.corext.fix.UseFunctionalCallFixCore.LoopConversionOperation;

/** One semantic decision path for cleanup, Quick Assist and style diagnostics. */
public final class LoopConversionService {
	private static final String SCAN_ROOT = LoopConversionService.class.getName();

	private LoopConversionService() {
	}

	public static EnumSet<UseFunctionalCallFixCore> handlers(LoopTargetFormat target) {
		return switch (target) {
		case STREAM -> EnumSet.of(UseFunctionalCallFixCore.LOOP, UseFunctionalCallFixCore.ITERATOR_LOOP,
				UseFunctionalCallFixCore.TRADITIONAL_FOR_LOOP);
		case FOR_LOOP -> EnumSet.of(UseFunctionalCallFixCore.STREAM_TO_FOR, UseFunctionalCallFixCore.ITERATOR_TO_FOR);
		case WHILE_LOOP -> EnumSet.of(UseFunctionalCallFixCore.FOR_TO_ITERATOR, UseFunctionalCallFixCore.STREAM_TO_ITERATOR);
		};
	}

	public record Analysis(CompilationUnit unit, List<CompilationUnitRewriteOperation> operations, Set<String> names) {
		public Analysis {
			operations = List.copyOf(operations);
			names = Set.copyOf(names);
		}

		public CompilationUnitRewriteOperationsFixCore createFix(String label) {
			if (operations.isEmpty()) {
				return null;
			}
			return new CompilationUnitRewriteOperationsFixCore(label, unit,
					operations.toArray(CompilationUnitRewriteOperation[]::new)) {
				@Override
				public CompilationUnitChange createChange(IProgressMonitor monitor) throws CoreException {
					synchronized (unit) {
						try (var scope = new LoopVariableNames.Scope(unit, names)) {
							return super.createChange(monitor);
						}
					}
				}
			};
		}

		/** A caret targets the innermost enclosing loop, never a neighbouring loop. */
		public Analysis atSelection(int offset, int length) {
			ASTNode anchor = selection(unit, offset, length);
			List<CompilationUnitRewriteOperation> matches = operations.stream()
					.filter(operation -> operation instanceof LoopConversionOperation loop && loop.getAnchor() == anchor)
					.toList();
			return new Analysis(unit, matches, names);
		}
	}

	/** Scope editor discovery before overlap suppression can hide a selected inner loop. */
	public static Analysis analyzeSelection(CompilationUnit unit, EnumSet<UseFunctionalCallFixCore> handlers, int offset, int length) {
		synchronized (unit) {
			ASTNode selected = selection(unit, offset, length);
			if (selected == null) return new Analysis(unit, List.of(), Set.of());
			Object previous = unit.getProperty(SCAN_ROOT);
			unit.setProperty(SCAN_ROOT, selected);
			try {
				return analyze(unit, handlers).atSelection(offset, length);
			} finally {
				unit.setProperty(SCAN_ROOT, previous);
			}
		}
	}

	static ASTNode scanRoot(CompilationUnit unit) {
		return unit.getProperty(SCAN_ROOT) instanceof ASTNode root ? root : unit;
	}

	private static ASTNode selection(CompilationUnit unit, int offset, int length) {
		if (offset < 0 || length < 0 || (long) offset + length > unit.getLength()) return null;
		ASTNode selected = NodeFinder.perform(unit, offset, length);
		while (selected != null && !(selected instanceof EnhancedForStatement || selected instanceof ForStatement
				|| selected instanceof WhileStatement || isForEachStatement(selected))) selected = selected.getParent();
		return selected;
	}

	public static Analysis analyze(CompilationUnit unit, EnumSet<UseFunctionalCallFixCore> handlers) {
		synchronized (unit) {
			try (var scope = new LoopVariableNames.Scope(unit, Set.of())) {
				Set<CompilationUnitRewriteOperation> operations = new LinkedHashSet<>();
				Set<ASTNode> processed = new HashSet<>();
				for (UseFunctionalCallFixCore handler : handlers) {
					if (supportsSourceLevel(unit, handler)) {
						handler.findOperations(unit, operations, processed);
					}
				}
				List<CompilationUnitRewriteOperation> safe = new ArrayList<>(operations);
				safe.removeIf(operation -> operation instanceof LoopConversionOperation loop && hasErrors(loop.getAnchor()));
				safe.sort(Comparator.comparingInt(operation -> operation instanceof LoopConversionOperation loop
						? loop.getAnchor().getStartPosition() : 0));
				return new Analysis(unit, safe, LoopVariableNames.usedNames(unit));
			}
		}
	}

	private static boolean isForEachStatement(ASTNode node) {
		return node instanceof ExpressionStatement statement && statement.getExpression() instanceof MethodInvocation call
				&& Set.of("forEach", "forEachOrdered").contains(call.getName().getIdentifier()); //$NON-NLS-1$ //$NON-NLS-2$
	}

	public static boolean hasErrors(ASTNode node) {
		for (var problem : ((CompilationUnit) node.getRoot()).getProblems()) {
			if (problem.isError() && problem.getSourceStart() < node.getStartPosition() + node.getLength()
					&& problem.getSourceEnd() >= node.getStartPosition()) {
				return true;
			}
		}
		return false;
	}

	private static boolean supportsSourceLevel(CompilationUnit unit, UseFunctionalCallFixCore handler) {
		if (unit.getJavaElement() == null) {
			return true;
		}
		String source = unit.getJavaElement().getJavaProject().getOption(JavaCore.COMPILER_SOURCE, true);
		String minimum = handlers(LoopTargetFormat.STREAM).contains(handler) ? JavaCore.VERSION_1_8 : JavaCore.VERSION_1_5;
		return JavaCore.compareJavaVersions(source, minimum) >= 0;
	}
}