ConcreteCollectionFactory.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.internal.corext.fix.helper;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.sandbox.functional.core.terminal.CollectTerminal;
/** Resolves behavior-preserving suppliers for fresh collection accumulators. */
final class ConcreteCollectionFactory {
private static final String JAVA_UTIL_LIST= "java.util.List"; //$NON-NLS-1$
private static final String JAVA_UTIL_SET= "java.util.Set"; //$NON-NLS-1$
private static final String JAVA_UTIL_TREE_SET_FACTORY= "java.util.TreeSet::new"; //$NON-NLS-1$
private static final Map<String, CollectTerminal.CollectorType> SUPPORTED= Map.of(
"java.util.ArrayList", CollectTerminal.CollectorType.TO_LIST, //$NON-NLS-1$
"java.util.LinkedList", CollectTerminal.CollectorType.TO_LIST, //$NON-NLS-1$
"java.util.HashSet", CollectTerminal.CollectorType.TO_SET, //$NON-NLS-1$
"java.util.LinkedHashSet", CollectTerminal.CollectorType.TO_SET, //$NON-NLS-1$
"java.util.TreeSet", CollectTerminal.CollectorType.TO_SET); //$NON-NLS-1$
private ConcreteCollectionFactory() {
}
/**
* Adds the proven constructor supplier to a collect terminal. Unsupported or
* construction-sensitive declarations fail closed by returning {@code null}.
*
* <p>For an exact {@link java.util.List} declaration, or an exact
* {@link java.util.Set} declaration backed by a modeled hash-based set, whose
* value is never referenced after the accumulating loop, the concrete
* implementation is not observable. In that case the existing interface
* collector remains sufficient and avoids changing established output for a
* dead local. A later read, return or subsequent loop keeps the explicit
* constructor supplier. {@link java.util.TreeSet} always keeps its supplier:
* comparison and null-handling failures remain observable even when the final
* set value is not read.</p>
*/
static CollectTerminal preserveFactory(VariableDeclarationStatement declaration, CollectTerminal terminal) {
String supplier= supplier(declaration, terminal);
if (supplier == null) {
return null;
}
if (canUseInterfaceCollector(declaration, terminal, supplier)) {
return terminal;
}
return new CollectTerminal(terminal.collectorType(), terminal.targetVariable(), supplier);
}
/**
* Returns whether the adjacent declaration uses one modeled JDK collection type.
* Constructor arguments are allowed because callers using this predicate retain
* the original declaration and only replace the loop with a direct forEach.
*/
static boolean hasSupportedConcreteType(VariableDeclarationStatement declaration, CollectTerminal terminal) {
if (declaration == null || terminal == null || declaration.fragments().size() != 1) {
return false;
}
VariableDeclarationFragment fragment= (VariableDeclarationFragment) declaration.fragments().get(0);
Expression initializer= fragment.getInitializer();
if (!(initializer instanceof ClassInstanceCreation creation) || hasAnonymousClass(creation)) {
return false;
}
ITypeBinding createdType= creation.resolveTypeBinding();
ITypeBinding declaredType= declaration.getType().resolveBinding();
if (createdType == null || declaredType == null) {
return false;
}
ITypeBinding createdErasure= createdType.getErasure();
if (createdErasure == null || declaredType.getErasure() == null) {
return false;
}
CollectTerminal.CollectorType supportedType= SUPPORTED.get(createdErasure.getQualifiedName());
return supportedType == terminal.collectorType()
&& createdType.isAssignmentCompatible(declaredType);
}
/**
* Returns a qualified constructor reference for one exactly modeled initializer.
* Constructor arguments, anonymous classes, custom implementations and unresolved
* bindings fail closed.
*/
static String supplier(VariableDeclarationStatement declaration, CollectTerminal terminal) {
if (declaration == null || terminal == null || declaration.fragments().size() != 1) {
return null;
}
VariableDeclarationFragment fragment= (VariableDeclarationFragment) declaration.fragments().get(0);
Expression initializer= fragment.getInitializer();
if (!(initializer instanceof ClassInstanceCreation creation)
|| !creation.arguments().isEmpty()
|| hasAnonymousClass(creation)) {
return null;
}
ITypeBinding createdType= creation.resolveTypeBinding();
ITypeBinding declaredType= declaration.getType().resolveBinding();
if (createdType == null || declaredType == null) {
return null;
}
ITypeBinding createdErasure= createdType.getErasure();
if (createdErasure == null || declaredType.getErasure() == null) {
return null;
}
String qualifiedName= createdErasure.getQualifiedName();
CollectTerminal.CollectorType supportedType= SUPPORTED.get(qualifiedName);
if (supportedType != terminal.collectorType()
|| !createdType.isAssignmentCompatible(declaredType)) {
return null;
}
return qualifiedName + "::new"; //$NON-NLS-1$
}
private static boolean canUseInterfaceCollector(VariableDeclarationStatement declaration,
CollectTerminal terminal, String supplier) {
if (JAVA_UTIL_TREE_SET_FACTORY.equals(supplier)) {
return false;
}
ITypeBinding declaredType= declaration.getType().resolveBinding();
ITypeBinding erasure= declaredType == null ? null : declaredType.getErasure();
if (erasure == null) {
return false;
}
String qualifiedName= erasure.getQualifiedName();
boolean exactInterface= terminal.collectorType() == CollectTerminal.CollectorType.TO_LIST
? JAVA_UTIL_LIST.equals(qualifiedName)
: terminal.collectorType() == CollectTerminal.CollectorType.TO_SET
&& JAVA_UTIL_SET.equals(qualifiedName);
return exactInterface && !isReferencedAfterAccumulation(declaration);
}
private static boolean isReferencedAfterAccumulation(VariableDeclarationStatement declaration) {
if (!(declaration.getParent() instanceof Block block) || declaration.fragments().size() != 1) {
return true;
}
VariableDeclarationFragment fragment= (VariableDeclarationFragment) declaration.fragments().get(0);
IVariableBinding binding= fragment.resolveBinding();
if (binding == null) {
return true;
}
@SuppressWarnings("unchecked") //$NON-NLS-1$
List<Statement> statements= block.statements();
int declarationIndex= statements.indexOf(declaration);
if (declarationIndex < 0) {
return true;
}
boolean accumulationFound= false;
for (int index= declarationIndex + 1; index < statements.size(); index++) {
boolean referencesAccumulator= references(statements.get(index), binding);
if (!accumulationFound) {
accumulationFound= referencesAccumulator;
} else if (referencesAccumulator) {
return true;
}
}
return !accumulationFound;
}
private static boolean references(Statement statement, IVariableBinding target) {
boolean[] found= { false };
statement.accept(new ASTVisitor() {
@Override
public boolean visit(SimpleName node) {
IBinding resolved= node.resolveBinding();
if (resolved instanceof IVariableBinding variable
&& target.getVariableDeclaration().isEqualTo(variable.getVariableDeclaration())) {
found[0]= true;
return false;
}
return !found[0];
}
});
return found[0];
}
private static boolean hasAnonymousClass(ClassInstanceCreation creation) {
AnonymousClassDeclaration anonymous= creation.getAnonymousClassDeclaration();
return anonymous != null;
}
}