SwitchIntToEnumHelper.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
*
* Contributors:
* Carsten Hammer
*******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.helper;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.IBinding;
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.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.SwitchCase;
import org.eclipse.jdt.core.dom.SwitchStatement;
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.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperationWithSourceRange;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.internal.common.AstProcessorBuilder;
import org.sandbox.jdt.internal.common.ReferenceHolder;
import org.sandbox.jdt.internal.corext.fix.IntToEnumFixCore;
import org.sandbox.jdt.internal.corext.fix.helper.IntToEnumHelper.IntConstantHolder;
/**
* Helper class for converting switch statements using int constants to use enums.
*
* <p>This handles the case where a switch statement already exists with int constant
* case labels. The transformation:</p>
* <ol>
* <li>Detects static final int constant declarations with a common underscore-delimited prefix</li>
* <li>Finds switch statements that use these constants as case labels</li>
* <li>Verifies the switch selector is a method parameter (so the type can be updated)</li>
* <li>Verifies constants are not referenced outside the switch (to avoid broken references)</li>
* <li>Generates an enum type from the constant names</li>
* <li>Replaces the int constants in switch cases with enum values</li>
* <li>Removes the old int constant field declarations</li>
* <li>Updates the method parameter type from int to the enum type</li>
* </ol>
*/
public class SwitchIntToEnumHelper extends AbstractTool<ReferenceHolder<Integer, IntConstantHolder>> {
@Override
public void find(IntToEnumFixCore fixcore, CompilationUnit compilationUnit,
Set<CompilationUnitRewriteOperationWithSourceRange> operations, Set<ASTNode> nodesprocessed) {
ReferenceHolder<String, Object> findHolder = ReferenceHolder.create();
AstProcessorBuilder.with(findHolder)
.onSwitchStatement((node, h) -> {
if (nodesprocessed.contains(node)) {
return true;
}
// Only process if the switch selector is a method parameter (so we can update its type)
Expression switchExpr = node.getExpression();
if (!(switchExpr instanceof SimpleName switchName)) {
return true;
}
MethodDeclaration method = findEnclosingMethod(node);
if (method == null || !isMethodParameter(method, switchName.getIdentifier())) {
return true;
}
// Collect constants from the same enclosing type as the switch
TypeDeclaration enclosingType = findEnclosingType(node);
if (enclosingType == null) {
return true;
}
Map<String, FieldDeclaration> intConstants = collectIntConstants(enclosingType);
if (intConstants.size() < 2) {
return true;
}
// Find case labels that reference collected constants (SimpleName only)
List<String> usedConstants = new ArrayList<>();
Map<String, FieldDeclaration> usedFields = new LinkedHashMap<>();
for (Object stmt : node.statements()) {
if (stmt instanceof SwitchCase switchCase && !switchCase.isDefault()) {
for (Object expr : switchCase.expressions()) {
if (expr instanceof SimpleName sn) {
String constName = sn.getIdentifier();
if (intConstants.containsKey(constName)) {
usedConstants.add(constName);
usedFields.put(constName, intConstants.get(constName));
}
}
}
}
}
if (usedConstants.size() < 2) {
return true;
}
String prefix = findCommonPrefix(usedConstants);
if (prefix == null) {
return true;
}
// Validate all derived enum constant names are valid Java identifiers
for (String constName : usedConstants) {
String enumValueName = constName.substring(prefix.length());
if (enumValueName.isEmpty() || !Character.isJavaIdentifierStart(enumValueName.charAt(0))) {
return true;
}
}
// Verify constants are not referenced outside the switch statement
if (hasReferencesOutsideSwitch(compilationUnit, usedFields.keySet(), node)) {
return true;
}
IntConstantHolder holder = new IntConstantHolder();
holder.switchStatement = node;
holder.constantFields.putAll(usedFields);
holder.constantNames.addAll(usedConstants);
holder.comparedVariable = switchName.getIdentifier();
holder.nodesProcessed = nodesprocessed;
ReferenceHolder<Integer, IntConstantHolder> dataholder = new ReferenceHolder<>();
dataholder.put(0, holder);
operations.add(fixcore.rewrite(dataholder));
nodesprocessed.add(node);
return true;
})
.build(compilationUnit);
}
/**
* Collect static final int field declarations from the given type declaration.
*/
private static Map<String, FieldDeclaration> collectIntConstants(TypeDeclaration typeDecl) {
Map<String, FieldDeclaration> intConstants = new LinkedHashMap<>();
for (FieldDeclaration field : typeDecl.getFields()) {
int modifiers = field.getModifiers();
if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)) {
Type type = field.getType();
if (type.isPrimitiveType()
&& ((PrimitiveType) type).getPrimitiveTypeCode() == PrimitiveType.INT) {
for (Object fragment : field.fragments()) {
VariableDeclarationFragment vdf = (VariableDeclarationFragment) fragment;
intConstants.put(vdf.getName().getIdentifier(), field);
}
}
}
}
return intConstants;
}
/**
* Check if the given variable name is a parameter of the method.
*/
private static boolean isMethodParameter(MethodDeclaration method, String varName) {
for (Object param : method.parameters()) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) param;
if (svd.getName().getIdentifier().equals(varName)) {
return true;
}
}
return false;
}
/**
* Check if any of the named constants are referenced outside the given switch statement.
* Returns true if there are external references (meaning we should NOT transform).
*/
private static boolean hasReferencesOutsideSwitch(CompilationUnit cu, Set<String> constantNames,
SwitchStatement switchStatement) {
AtomicBoolean hasExternalRef = new AtomicBoolean(false);
ReferenceHolder<String, Object> refHolder = ReferenceHolder.create();
AstProcessorBuilder.with(refHolder)
.onSimpleName((node, h) -> {
if (hasExternalRef.get()) {
return false;
}
if (!constantNames.contains(node.getIdentifier())) {
return true;
}
IBinding binding = node.resolveBinding();
if (!(binding instanceof IVariableBinding vb) || !vb.isField()) {
return true;
}
// Check if this reference is inside the switch statement or in a field declaration
ASTNode parent = node.getParent();
while (parent != null) {
if (parent == switchStatement) {
return true; // inside the switch - OK
}
if (parent instanceof FieldDeclaration) {
return true; // in the field declaration itself - OK
}
parent = parent.getParent();
}
// Reference found outside the switch statement
hasExternalRef.set(true);
return false;
})
.build(cu);
return hasExternalRef.get();
}
@SuppressWarnings("unchecked")
@Override
public void rewrite(IntToEnumFixCore fixCore, ReferenceHolder<Integer, IntConstantHolder> holder,
CompilationUnitRewrite cuRewrite, TextEditGroup group) {
for (IntConstantHolder data : holder.values()) {
if (data.switchStatement == null) {
continue;
}
AST ast = cuRewrite.getRoot().getAST();
ASTRewrite rewrite = cuRewrite.getASTRewrite();
String prefix = findCommonPrefix(data.constantNames);
if (prefix == null) {
continue;
}
String enumName = prefixToEnumName(prefix);
// 1. Create enum declaration
EnumDeclaration enumDecl = ast.newEnumDeclaration();
enumDecl.setName(ast.newSimpleName(enumName));
enumDecl.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.PUBLIC_KEYWORD));
for (String constName : data.constantNames) {
EnumConstantDeclaration enumConst = ast.newEnumConstantDeclaration();
String enumValueName = constName.substring(prefix.length());
enumConst.setName(ast.newSimpleName(enumValueName));
enumDecl.enumConstants().add(enumConst);
}
// 2. Find the enclosing TypeDeclaration
TypeDeclaration typeDecl = findEnclosingType(data.switchStatement);
if (typeDecl == null) {
continue;
}
ListRewrite bodyRewrite = rewrite.getListRewrite(typeDecl, TypeDeclaration.BODY_DECLARATIONS_PROPERTY);
// 3. Insert enum before the first constant field being removed
FieldDeclaration firstField = data.constantFields.values().iterator().next();
bodyRewrite.insertBefore(enumDecl, firstField, group);
// 4. Remove old int constant field declarations
Set<FieldDeclaration> fieldsToRemove = new HashSet<>(data.constantFields.values());
for (FieldDeclaration field : fieldsToRemove) {
List<?> fragments = field.fragments();
boolean allFragmentsRemoved = true;
for (Object frag : fragments) {
VariableDeclarationFragment vdf = (VariableDeclarationFragment) frag;
if (!data.constantFields.containsKey(vdf.getName().getIdentifier())) {
allFragmentsRemoved = false;
break;
}
}
if (allFragmentsRemoved) {
bodyRewrite.remove(field, group);
} else {
ListRewrite fragRewrite = rewrite.getListRewrite(field, FieldDeclaration.FRAGMENTS_PROPERTY);
for (Object frag : fragments) {
VariableDeclarationFragment vdf = (VariableDeclarationFragment) frag;
if (data.constantFields.containsKey(vdf.getName().getIdentifier())) {
fragRewrite.remove(vdf, group);
}
}
}
}
// 5. Update switch case labels to enum values
for (Object stmt : data.switchStatement.statements()) {
if (stmt instanceof SwitchCase switchCase && !switchCase.isDefault()) {
for (Object expr : switchCase.expressions()) {
if (expr instanceof SimpleName sn) {
String constName = sn.getIdentifier();
if (data.constantFields.containsKey(constName)) {
String enumValueName = constName.substring(prefix.length());
SimpleName newName = ast.newSimpleName(enumValueName);
rewrite.replace(sn, newName, group);
}
}
}
}
}
// 6. Update method parameter type from int to enum
MethodDeclaration method = findEnclosingMethod(data.switchStatement);
if (method != null && data.comparedVariable != null) {
for (Object param : method.parameters()) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) param;
if (svd.getName().getIdentifier().equals(data.comparedVariable)) {
Type newType = ast.newSimpleType(ast.newSimpleName(enumName));
rewrite.replace(svd.getType(), newType, group);
}
}
}
}
}
/**
* Find the longest common prefix of all constant names, ending at an underscore boundary.
* Returns null if no valid underscore-delimited prefix is found.
*/
static String findCommonPrefix(List<String> names) {
if (names == null || names.isEmpty()) {
return null;
}
String first = names.get(0);
int prefixEnd = first.length();
for (String name : names) {
prefixEnd = Math.min(prefixEnd, name.length());
for (int i = 0; i < prefixEnd; i++) {
if (first.charAt(i) != name.charAt(i)) {
prefixEnd = i;
break;
}
}
}
String prefix = first.substring(0, prefixEnd);
// Trim to last underscore for a clean prefix boundary.
// If there is no underscore (or only at position 0), we do not
// consider this a valid prefix according to the architecture
// contract (which requires underscore-delimited prefixes).
int lastUnderscore = prefix.lastIndexOf('_');
if (lastUnderscore > 0) {
return prefix.substring(0, lastUnderscore + 1);
}
return null;
}
/**
* Convert a constant prefix like "STATUS_" to an enum name like "Status".
*/
static String prefixToEnumName(String prefix) {
if (prefix.endsWith("_")) { //$NON-NLS-1$
prefix = prefix.substring(0, prefix.length() - 1);
}
StringBuilder sb = new StringBuilder();
boolean capitalizeNext = true;
for (char c : prefix.toLowerCase().toCharArray()) {
if (c == '_') {
capitalizeNext = true;
} else {
if (capitalizeNext) {
sb.append(Character.toUpperCase(c));
capitalizeNext = false;
} else {
sb.append(c);
}
}
}
return sb.toString();
}
private static TypeDeclaration findEnclosingType(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null) {
if (parent instanceof TypeDeclaration typeDecl) {
return typeDecl;
}
parent = parent.getParent();
}
return null;
}
private static MethodDeclaration findEnclosingMethod(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null) {
if (parent instanceof MethodDeclaration methodDecl) {
return methodDecl;
}
parent = parent.getParent();
}
return null;
}
@Override
public String getPreview(boolean afterRefactoring) {
if (!afterRefactoring) {
return """
// Before:
public static final int STATUS_PENDING = 0;
public static final int STATUS_APPROVED = 1;
switch (status) {
case STATUS_PENDING:
// handle pending
break;
case STATUS_APPROVED:
// handle approved
break;
}
"""; //$NON-NLS-1$
}
return """
// After:
public enum Status {
PENDING, APPROVED
}
switch (status) {
case PENDING:
// handle pending
break;
case APPROVED:
// handle approved
break;
}
"""; //$NON-NLS-1$
}
}