IteratorLoopBindings.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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.core.dom.*;
import org.eclipse.jdt.internal.corext.dom.Bindings;
import org.sandbox.jdt.internal.corext.fix.helper.IteratorPatternDetector.IteratorPattern;

/** Proves the iterator is used only by its declaration, condition and initial next. */
final class IteratorLoopBindings {
	private static final String REFERENCES = IteratorLoopBindings.class.getName();
	private record References(Map<String, List<SimpleName>> byBinding) { }
	private IteratorLoopBindings() { }

	static String sourceElementType(ITypeBinding type) {
		ITypeBinding iterable = Bindings.findTypeInHierarchy(type, "java.lang.Iterable"); //$NON-NLS-1$
		return iterable.getTypeArguments().length == 1 ? iterable.getTypeArguments()[0].getQualifiedName() : "java.lang.Object"; //$NON-NLS-1$
	}

	static boolean denotableIterator(ITypeBinding source) {
		ITypeBinding iterable = Bindings.findTypeInHierarchy(source, "java.lang.Iterable"); //$NON-NLS-1$
		for (ITypeBinding argument : iterable.getTypeArguments()) {
			// An expression captures wildcards; the declaration must keep the original wildcard.
			if (argument.isCapture()) argument = argument.getWildcard();
			if (!(argument.isWildcardType() ? argument.getBound() == null || JdtStreamExtractor.denotable(argument.getBound())
					: JdtStreamExtractor.denotable(argument))) return false;
		}
		return true;
	}

	static VariableDeclarationFragment element(Statement loop, Statement previous, IteratorPattern pattern) {
		if (pattern == null || LoopConversionService.hasErrors(loop) || previous != null && LoopConversionService.hasErrors(previous)
				|| !(pattern.loopBody() instanceof Block body) || body.statements().isEmpty()
				|| !(body.statements().get(0) instanceof VariableDeclarationStatement item) || item.fragments().size() != 1) return null;
		ITypeBinding source = pattern.collectionExpression().resolveTypeBinding();
		if (source == null || source.isRecovered() || Bindings.findTypeInHierarchy(source, "java.lang.Iterable") == null) return null; //$NON-NLS-1$
		VariableDeclarationFragment element = (VariableDeclarationFragment) item.fragments().get(0);
		if (element.resolveBinding() == null || item.getType().isVar() && !JdtStreamExtractor.denotable(element.resolveBinding().getType())) return null;
		if (!(element.getInitializer() instanceof MethodInvocation next) || !next.arguments().isEmpty()
				|| !"next".equals(next.getName().getIdentifier()) || !(next.getExpression() instanceof SimpleName nextReceiver)) return null; //$NON-NLS-1$
		Expression condition = loop instanceof WhileStatement whileLoop ? whileLoop.getExpression() : ((ForStatement) loop).getExpression();
		if (!(condition instanceof MethodInvocation hasNext) || !hasNext.arguments().isEmpty()
				|| !(hasNext.getExpression() instanceof SimpleName conditionReceiver)) return null;
		VariableDeclarationFragment declaration = loop instanceof ForStatement forLoop
				? (VariableDeclarationFragment) ((VariableDeclarationExpression) forLoop.initializers().get(0)).fragments().get(0)
				: (VariableDeclarationFragment) ((VariableDeclarationStatement) previous).fragments().get(0);
		IVariableBinding iterator = declaration.resolveBinding();
		if (iterator == null || iterator.isRecovered() || !iterator.isEqualTo(nextReceiver.resolveBinding())
				|| !iterator.isEqualTo(conditionReceiver.resolveBinding()) || Bindings.findTypeInHierarchy(iterator.getType(), "java.util.Iterator") == null) return null; //$NON-NLS-1$
		Set<SimpleName> protocol = Set.of(declaration.getName(), conditionReceiver, nextReceiver);
		return references(loop.getRoot(), iterator).stream().allMatch(protocol::contains) ? element : null;
	}

	/** Index immutable source bindings once per AST, instead of rescanning the unit per loop. */
	private static List<SimpleName> references(ASTNode root, IVariableBinding variable) {
		synchronized (root) {
			if (!(root.getProperty(REFERENCES) instanceof References)) {
				Map<String, List<SimpleName>> index = new HashMap<>();
				root.accept(new ASTVisitor() {
					@Override
					public boolean visit(SimpleName name) {
						if (name.resolveBinding() instanceof IVariableBinding binding) {
							index.computeIfAbsent(binding.getVariableDeclaration().getKey(), key -> new ArrayList<>()).add(name);
						}
						return true;
					}
				});
				root.setProperty(REFERENCES, new References(index));
			}
			return ((References) root.getProperty(REFERENCES)).byBinding().getOrDefault(variable.getVariableDeclaration().getKey(), List.of());
		}
	}
}