IntEnumScopeCandidateDetector.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.multifile;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.dom.ASTRequestor;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.internal.corext.dom.IASTSharedValues;
/** Lightweight selected-scope detector for project-wide Int-to-Enum planning. */
public final class IntEnumScopeCandidateDetector {
/** Binding-derived elements whose references define the required source closure. */
public record SearchSeeds(boolean candidateFound, boolean complete, List<IJavaElement> elements) {
public SearchSeeds {
elements= List.copyOf(elements);
}
}
private IntEnumScopeCandidateDetector() {
// utility class
}
/** Returns whether the current selection contains a structural candidate. */
public static boolean containsCandidate(IJavaProject project, Collection<ICompilationUnit> currentScope,
IProgressMonitor monitor) {
return findSearchSeeds(project, currentScope, monitor).candidateFound();
}
/**
* Finds the candidate methods and constants whose references define the
* coordinated source closure. A structural candidate with a missing binding is
* reported as incomplete so the caller can use its conservative fallback.
*/
public static SearchSeeds findSearchSeeds(IJavaProject project, Collection<ICompilationUnit> currentScope,
IProgressMonitor monitor) {
if (project == null || currentScope == null || currentScope.isEmpty()) {
return new SearchSeeds(false, true, List.of());
}
checkCanceled(monitor);
Set<ICompilationUnit> units= new LinkedHashSet<>();
for (ICompilationUnit unit : currentScope) {
if (unit != null && unit.exists() && project.equals(unit.getJavaProject())) {
units.add(unit.getPrimary());
}
}
if (units.isEmpty()) {
return new SearchSeeds(false, true, List.of());
}
boolean[] candidateFound= { false };
boolean[] complete= { true };
Set<IJavaElement> elements= new LinkedHashSet<>();
ASTParser parser= ASTParser.newParser(IASTSharedValues.SHARED_AST_LEVEL);
parser.setProject(project);
parser.setResolveBindings(true);
parser.setBindingsRecovery(IASTSharedValues.SHARED_BINDING_RECOVERY);
parser.setStatementsRecovery(IASTSharedValues.SHARED_AST_STATEMENT_RECOVERY);
parser.createASTs(units.toArray(ICompilationUnit[]::new), new String[0], new ASTRequestor() {
@Override
public void acceptAST(ICompilationUnit source, CompilationUnit ast) {
ast.accept(new ASTVisitor() {
@Override
public boolean visit(TypeDeclaration type) {
if (!(type.getParent() instanceof CompilationUnit) || type.isInterface()
|| type.getSuperclassType() != null || !type.superInterfaceTypes().isEmpty()) {
return false;
}
List<VariableDeclarationFragment> constants= packagePrivateIntConstants(type);
List<MethodDeclaration> methods= packagePrivateIntStateMethods(type);
if (constants.size() < 2 || methods.isEmpty()) {
return false;
}
candidateFound[0]= true;
for (VariableDeclarationFragment constant : constants) {
IVariableBinding binding= constant.resolveBinding();
complete[0]&= addJavaElement(binding, elements);
}
for (MethodDeclaration method : methods) {
IMethodBinding binding= method.resolveBinding();
complete[0]&= addJavaElement(binding, elements);
}
return false;
}
});
}
}, monitor);
checkCanceled(monitor);
return new SearchSeeds(candidateFound[0], complete[0], new ArrayList<>(elements));
}
private static List<VariableDeclarationFragment> packagePrivateIntConstants(TypeDeclaration type) {
List<VariableDeclarationFragment> result= new ArrayList<>();
for (FieldDeclaration field : type.getFields()) {
int modifiers= field.getModifiers();
if (!isPackagePrivate(modifiers) || !Modifier.isStatic(modifiers) || !Modifier.isFinal(modifiers)
|| !isPlainInt(field.getType())) {
continue;
}
for (Object fragment : field.fragments()) {
result.add((VariableDeclarationFragment) fragment);
}
}
return result;
}
private static List<MethodDeclaration> packagePrivateIntStateMethods(TypeDeclaration type) {
List<MethodDeclaration> result= new ArrayList<>();
for (MethodDeclaration method : type.getMethods()) {
if (method.isConstructor() || method.getBody() == null || !isPackagePrivate(method.getModifiers())) {
continue;
}
for (Object parameterObject : method.parameters()) {
SingleVariableDeclaration parameter= (SingleVariableDeclaration) parameterObject;
if (parameter.getExtraDimensions() == 0 && !parameter.isVarargs() && isPlainInt(parameter.getType())) {
result.add(method);
break;
}
}
}
return result;
}
private static boolean addJavaElement(IBinding binding, Set<IJavaElement> elements) {
IJavaElement element= binding == null ? null : binding.getJavaElement();
if (element == null || !element.exists()) {
return false;
}
elements.add(element);
return true;
}
private static boolean isPlainInt(Type type) {
return type instanceof PrimitiveType primitive
&& primitive.getPrimitiveTypeCode() == PrimitiveType.INT;
}
private static boolean isPackagePrivate(int modifiers) {
return !Modifier.isPublic(modifiers) && !Modifier.isProtected(modifiers) && !Modifier.isPrivate(modifiers);
}
private static void checkCanceled(IProgressMonitor monitor) {
if (monitor != null && monitor.isCanceled()) {
throw new OperationCanceledException();
}
}
}