IntEnumMultiFilePlanner.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
*******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.multifile;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.SourceVersion;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
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.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
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.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
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.internal.corext.dom.IASTSharedValues;
import org.eclipse.jdt.internal.corext.refactoring.util.RefactoringASTParser;
import org.sandbox.jdt.cleanup.multifile.GeneratedNameAllocator;
import org.sandbox.jdt.cleanup.multifile.GeneratedNameAllocator.Allocation;
import org.sandbox.jdt.cleanup.multifile.GeneratedNameAllocator.NestedTypeRequest;
import org.sandbox.jdt.cleanup.multifile.JavaProjectCompilationUnits;
import org.sandbox.jdt.cleanup.multifile.MultiFileCandidateDiagnostic;
import org.sandbox.jdt.cleanup.multifile.MultiFileCleanUpDiagnostics;
import org.sandbox.jdt.cleanup.multifile.MultiFileCleanUpPlanResult;
import org.sandbox.jdt.cleanup.multifile.MultiFilePlanningBudget;
import org.sandbox.jdt.cleanup.multifile.MultiFilePlanningLimits;
import org.sandbox.jdt.cleanup.multifile.MultiFilePlanningMetrics;
import org.sandbox.jdt.cleanup.multifile.MultiFileScopeDiagnostic;
import org.sandbox.jdt.cleanup.multifile.SelectedCompilationUnitPlan;
/** Builds conservative source-wide integer-state migration plans. */
public final class IntEnumMultiFilePlanner {
private static final String CLEANUP_ID= "int-to-enum"; //$NON-NLS-1$
private record ConstantDecl(String bindingKey, String name, int value) {
}
private record FreezeResult(List<IntEnumCandidate> candidates,
List<MultiFileCandidateDiagnostic> diagnostics) {
FreezeResult {
candidates= List.copyOf(candidates);
diagnostics= List.copyOf(diagnostics);
}
}
private static final class CandidateBuilder {
String ownerUnitHandle;
String ownerTypeKey;
String ownerTypeQualifiedName;
String methodKey;
String methodName;
int parameterIndex;
String prefix;
String enumName;
IVariableBinding stateBinding;
final List<ConstantDecl> constants= new ArrayList<>();
final Map<Expression, String> recognisedReferences= new IdentityHashMap<>();
final Set<Expression> stateReferences= java.util.Collections.newSetFromMap(new IdentityHashMap<>());
final Map<String, Integer> callCountsByUnit= new LinkedHashMap<>();
boolean valid= true;
boolean hasExternalCaller;
String rejectionCode= ""; //$NON-NLS-1$
String rejectionMessage= ""; //$NON-NLS-1$
void invalidate(String reasonCode, String message) {
if (valid) {
valid= false;
rejectionCode= reasonCode;
rejectionMessage= message;
}
}
String candidateId() {
return ownerTypeQualifiedName + '#' + methodName + ':' + parameterIndex;
}
String generatedNameRequestId() {
return candidateId() + "->" + enumName; //$NON-NLS-1$
}
List<String> relatedUnitHandles() {
Set<String> handles= new LinkedHashSet<>();
handles.add(ownerUnitHandle);
handles.addAll(callCountsByUnit.keySet());
return List.copyOf(handles);
}
}
private IntEnumMultiFilePlanner() {
}
/** Creates a plan for an explicitly complete project selection. */
public static MultiFileCleanUpPlanResult<IntEnumMigrationPlan> create(IJavaProject project,
ICompilationUnit[] selectedUnits, IProgressMonitor monitor) throws CoreException {
if (monitor != null && monitor.isCanceled()) {
throw new OperationCanceledException();
}
SelectedCompilationUnitPlan scope= SelectedCompilationUnitPlan.of(project, selectedUnits);
Set<String> allHandles= new LinkedHashSet<>();
for (ICompilationUnit unit : JavaProjectCompilationUnits.collect(project)) {
allHandles.add(unit.getHandleIdentifier());
}
return create(project, selectedUnits, scope.compilationUnitHandles().equals(allHandles), monitor);
}
/**
* Creates a plan after the caller has proved that the selected source units
* form a closed migration scope.
*/
public static MultiFileCleanUpPlanResult<IntEnumMigrationPlan> create(IJavaProject project,
ICompilationUnit[] selectedUnits, boolean closedScope, IProgressMonitor monitor) throws CoreException {
SelectedCompilationUnitPlan scope= SelectedCompilationUnitPlan.of(project, selectedUnits);
MultiFilePlanningBudget.checkCanceled(monitor);
if (!closedScope) {
return MultiFileCleanUpPlanResult.success(new IntEnumMigrationPlan(scope, List.of()),
new org.eclipse.ltk.core.refactoring.RefactoringStatus(), MultiFilePlanningMetrics.empty(),
diagnostics(selectedUnits, false, List.of()));
}
long planningStarted= System.nanoTime();
MultiFilePlanningBudget.Assessment budget= MultiFilePlanningBudget.assess(selectedUnits,
MultiFilePlanningLimits.fromSystemProperties(), monitor);
if (!budget.mayProceed()) {
return new MultiFileCleanUpPlanResult<>(null, budget.status(), budget.metrics(),
diagnostics(selectedUnits, true, List.of()));
}
long parseStarted= System.nanoTime();
Map<String, CompilationUnit> roots= parse(project, selectedUnits, monitor);
long parseNanos= System.nanoTime() - parseStarted;
MultiFilePlanningBudget.checkCanceled(monitor);
List<CandidateBuilder> builders= discoverCandidates(roots, monitor);
MultiFilePlanningBudget.checkCanceled(monitor);
validateReferences(roots, builders, monitor);
MultiFilePlanningBudget.checkCanceled(monitor);
validateGeneratedNames(roots.values(), builders, monitor);
MultiFilePlanningBudget.checkCanceled(monitor);
FreezeResult frozen= freeze(builders, monitor);
MultiFilePlanningMetrics metrics= budget.metrics()
.withDurations(parseNanos, System.nanoTime() - planningStarted)
.withRetainedPlanEntries(frozen.candidates().size());
return MultiFileCleanUpPlanResult.success(new IntEnumMigrationPlan(scope, frozen.candidates()),
budget.status(), metrics, diagnostics(selectedUnits, true, frozen.diagnostics()));
}
private static MultiFileCleanUpDiagnostics diagnostics(ICompilationUnit[] selectedUnits, boolean complete,
List<MultiFileCandidateDiagnostic> candidates) {
List<String> selectedHandles= java.util.Arrays.stream(selectedUnits)
.map(ICompilationUnit::getPrimary)
.map(ICompilationUnit::getHandleIdentifier)
.toList();
MultiFileScopeDiagnostic scope= complete
? new MultiFileScopeDiagnostic(selectedHandles, List.of(), "CLOSED_SOURCE_SCOPE", //$NON-NLS-1$
"The selected compilation units form a closed integer-state migration scope.", true) //$NON-NLS-1$
: new MultiFileScopeDiagnostic(selectedHandles, List.of(), "INCOMPLETE_SOURCE_SCOPE", //$NON-NLS-1$
"The selected compilation units do not contain every required declaration and caller.", false); //$NON-NLS-1$
return new MultiFileCleanUpDiagnostics(CLEANUP_ID, scope, candidates);
}
private static Map<String, CompilationUnit> parse(IJavaProject project, ICompilationUnit[] units,
IProgressMonitor monitor) {
Map<String, CompilationUnit> roots= new LinkedHashMap<>();
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.setCompilerOptions(RefactoringASTParser.getCompilerOptions(project));
parser.createASTs(units, new String[0], new ASTRequestor() {
@Override
public void acceptAST(ICompilationUnit source, CompilationUnit ast) {
roots.put(source.getPrimary().getHandleIdentifier(), ast);
}
}, monitor);
return roots;
}
private static List<CandidateBuilder> discoverCandidates(Map<String, CompilationUnit> roots,
IProgressMonitor monitor) {
List<CandidateBuilder> result= new ArrayList<>();
Set<String> claimedConstants= new HashSet<>();
for (Map.Entry<String, CompilationUnit> entry : roots.entrySet()) {
MultiFilePlanningBudget.checkCanceled(monitor);
String unitHandle= entry.getKey();
entry.getValue().accept(new ASTVisitor() {
@Override
public boolean visit(TypeDeclaration type) {
MultiFilePlanningBudget.checkCanceled(monitor);
if (!(type.getParent() instanceof CompilationUnit) || type.isInterface()
|| type.getSuperclassType() != null || !type.superInterfaceTypes().isEmpty()) {
return false;
}
ITypeBinding typeBinding= type.resolveBinding();
String typeKey= typeKey(typeBinding);
if (typeKey == null) {
return false;
}
Map<String, ConstantDecl> constants= collectPackageConstants(type);
if (constants.size() < 2) {
return false;
}
for (MethodDeclaration method : type.getMethods()) {
if (method.isConstructor() || Modifier.isPrivate(method.getModifiers()) || method.getBody() == null) {
continue;
}
for (int parameterIndex= 0; parameterIndex < method.parameters().size(); parameterIndex++) {
SingleVariableDeclaration parameter= (SingleVariableDeclaration) method.parameters().get(parameterIndex);
if (!isPlainInt(parameter)) {
continue;
}
CandidateBuilder candidate= discoverMethodCandidate(unitHandle, type, typeBinding, typeKey, method,
parameter, parameterIndex, constants);
if (candidate == null) {
continue;
}
if (!isPackagePrivate(method.getModifiers())) {
candidate.invalidate("UNSUPPORTED_API_VISIBILITY", //$NON-NLS-1$
"Method " + candidate.ownerTypeQualifiedName + '#' + candidate.methodName //$NON-NLS-1$
+ " is public or protected; changing its parameter type requires an explicit compatibility policy."); //$NON-NLS-1$
}
if (candidate.valid && candidate.constants.stream()
.anyMatch(constant -> claimedConstants.contains(constant.bindingKey()))) {
candidate.invalidate("SHARED_CONSTANT_GROUP", //$NON-NLS-1$
"The integer constant group is already claimed by another migration candidate."); //$NON-NLS-1$
}
if (candidate.valid) {
candidate.constants.forEach(constant -> claimedConstants.add(constant.bindingKey()));
}
result.add(candidate);
}
}
return false;
}
});
}
return result;
}
private static CandidateBuilder discoverMethodCandidate(String unitHandle, TypeDeclaration type,
ITypeBinding typeBinding, String typeKey, MethodDeclaration method, SingleVariableDeclaration parameter,
int parameterIndex, Map<String, ConstantDecl> constants) {
IMethodBinding methodBinding= method.resolveBinding();
IVariableBinding parameterBinding= parameter.resolveBinding();
if (methodBinding == null || parameterBinding == null) {
return null;
}
List<IfStatement> roots= new ArrayList<>();
method.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(IfStatement node) {
if (!(node.getParent() instanceof IfStatement parent && parent.getElseStatement() == node)) {
roots.add(node);
}
return true;
}
});
for (IfStatement root : roots) {
CandidateBuilder candidate= parseChain(root, parameterBinding, constants);
if (candidate == null) {
continue;
}
candidate.ownerUnitHandle= unitHandle;
candidate.ownerTypeKey= typeKey;
candidate.ownerTypeQualifiedName= typeBinding.getQualifiedName();
candidate.methodKey= methodBinding.getMethodDeclaration().getKey();
candidate.methodName= method.getName().getIdentifier();
candidate.parameterIndex= parameterIndex;
if (candidate.methodKey == null || candidate.ownerTypeQualifiedName.isEmpty()) {
return null;
}
return candidate;
}
return null;
}
private static CandidateBuilder parseChain(IfStatement root, IVariableBinding parameter,
Map<String, ConstantDecl> constants) {
CandidateBuilder candidate= new CandidateBuilder();
candidate.stateBinding= parameter.getVariableDeclaration();
Set<String> usedKeys= new LinkedHashSet<>();
IfStatement current= root;
while (current != null) {
Comparison comparison= parseComparison(current.getExpression(), candidate.stateBinding, constants);
if (comparison == null || !usedKeys.add(comparison.constant().bindingKey())) {
return null;
}
candidate.recognisedReferences.put(comparison.constantExpression(), comparison.constant().bindingKey());
candidate.stateReferences.add(comparison.stateExpression());
current= current.getElseStatement() instanceof IfStatement next ? next : null;
}
if (usedKeys.size() < 2) {
return null;
}
List<ConstantDecl> used= constants.values().stream().filter(constant -> usedKeys.contains(constant.bindingKey())).toList();
String prefix= commonPrefix(used.stream().map(ConstantDecl::name).toList());
if (prefix == null) {
return null;
}
List<ConstantDecl> group= constants.values().stream().filter(constant -> constant.name().startsWith(prefix)).toList();
if (group.size() != used.size() || !distinctValues(group)) {
return null;
}
String enumName= enumTypeName(prefix);
if (!validIdentifier(enumName)) {
return null;
}
for (ConstantDecl constant : group) {
if (!validIdentifier(constant.name().substring(prefix.length()))) {
return null;
}
}
candidate.prefix= prefix;
candidate.enumName= enumName;
candidate.constants.addAll(group);
return candidate;
}
private record Comparison(Expression stateExpression, ConstantDecl constant, Expression constantExpression) {
}
private static Comparison parseComparison(Expression expression, IVariableBinding parameter,
Map<String, ConstantDecl> constants) {
Expression unwrapped= unwrap(expression);
if (!(unwrapped instanceof InfixExpression infix)
|| infix.getOperator() != InfixExpression.Operator.EQUALS || !infix.extendedOperands().isEmpty()) {
return null;
}
Expression left= unwrap(infix.getLeftOperand());
Expression right= unwrap(infix.getRightOperand());
IVariableBinding leftBinding= resolveVariable(left);
IVariableBinding rightBinding= resolveVariable(right);
ConstantDecl leftConstant= leftBinding == null ? null : constants.get(leftBinding.getKey());
ConstantDecl rightConstant= rightBinding == null ? null : constants.get(rightBinding.getKey());
if (leftConstant != null && sameVariable(parameter, rightBinding) && rightConstant == null) {
return new Comparison(right, leftConstant, left);
}
if (rightConstant != null && sameVariable(parameter, leftBinding) && leftConstant == null) {
return new Comparison(left, rightConstant, right);
}
return null;
}
private static void validateReferences(Map<String, CompilationUnit> roots,
List<CandidateBuilder> candidates, IProgressMonitor monitor) {
Map<String, CandidateBuilder> byMethod= new HashMap<>();
Map<String, CandidateBuilder> byConstant= new HashMap<>();
for (CandidateBuilder candidate : candidates) {
MultiFilePlanningBudget.checkCanceled(monitor);
byMethod.put(candidate.methodKey, candidate);
for (ConstantDecl constant : candidate.constants) {
byConstant.put(constant.bindingKey(), candidate);
}
}
for (Map.Entry<String, CompilationUnit> entry : roots.entrySet()) {
MultiFilePlanningBudget.checkCanceled(monitor);
String unitHandle= entry.getKey();
CompilationUnit root= entry.getValue();
root.accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation node) {
MultiFilePlanningBudget.checkCanceled(monitor);
IMethodBinding methodBinding= node.resolveMethodBinding();
String methodKey= methodBinding == null ? null : methodBinding.getMethodDeclaration().getKey();
CandidateBuilder candidate= byMethod.get(methodKey);
if (candidate == null) {
return true;
}
candidate.callCountsByUnit.putIfAbsent(unitHandle, Integer.valueOf(0));
if (!unitHandle.equals(candidate.ownerUnitHandle)) {
candidate.hasExternalCaller= true;
}
if (candidate.parameterIndex >= node.arguments().size()) {
candidate.invalidate("UNRESOLVED_INVOCATION", //$NON-NLS-1$
"A call does not expose the planned integer-state parameter."); //$NON-NLS-1$
return false;
}
Expression argument= unwrap((Expression) node.arguments().get(candidate.parameterIndex));
IVariableBinding argumentBinding= resolveVariable(argument);
IntEnumConstant constant= frozenConstant(candidate, argumentBinding);
if (constant == null) {
candidate.invalidate("ARBITRARY_INTEGER_ARGUMENT", //$NON-NLS-1$
"A call passes an integer expression that is not one of the modeled constants."); //$NON-NLS-1$
return false;
}
candidate.recognisedReferences.put(argument, constant.bindingKey());
candidate.callCountsByUnit.merge(unitHandle, Integer.valueOf(1), Integer::sum);
return true;
}
@Override
public boolean visit(SimpleName node) {
MultiFilePlanningBudget.checkCanceled(monitor);
IBinding binding= node.resolveBinding();
if (binding instanceof IVariableBinding variable) {
String key= variable.getVariableDeclaration().getKey();
CandidateBuilder candidate= byConstant.get(key);
if (candidate != null && !isVariableDeclarationName(node)
&& !candidate.recognisedReferences.containsKey(containingExpression(node))) {
candidate.invalidate("UNSUPPORTED_CONSTANT_REFERENCE", //$NON-NLS-1$
"A modeled integer constant is used outside the supported comparisons and call arguments."); //$NON-NLS-1$
}
for (CandidateBuilder stateCandidate : candidates) {
if (sameVariable(stateCandidate.stateBinding, variable) && !isParameterDeclarationName(node)
&& !stateCandidate.stateReferences.contains(containingExpression(node))) {
stateCandidate.invalidate("UNSUPPORTED_STATE_REFERENCE", //$NON-NLS-1$
"The integer-state parameter is used outside the modeled equality chain."); //$NON-NLS-1$
}
}
} else if (binding instanceof IMethodBinding methodBinding) {
CandidateBuilder candidate= byMethod.get(methodBinding.getMethodDeclaration().getKey());
if (candidate != null && !isMethodDeclarationName(node)
&& !(node.getParent() instanceof MethodInvocation invocation && invocation.getName() == node)) {
candidate.invalidate("UNSUPPORTED_METHOD_REFERENCE", //$NON-NLS-1$
"The candidate method is referenced in a form other than a direct invocation."); //$NON-NLS-1$
}
}
return true;
}
});
}
}
private static void validateGeneratedNames(Collection<CompilationUnit> roots,
List<CandidateBuilder> candidates, IProgressMonitor monitor) {
List<NestedTypeRequest> requests= new ArrayList<>();
Map<String, CandidateBuilder> candidatesByRequestId= new LinkedHashMap<>();
for (CandidateBuilder candidate : candidates) {
MultiFilePlanningBudget.checkCanceled(monitor);
if (!candidate.valid) {
continue;
}
String requestId= candidate.generatedNameRequestId();
requests.add(new NestedTypeRequest(requestId, candidate.ownerUnitHandle, candidate.ownerTypeKey,
candidate.ownerTypeQualifiedName, candidate.enumName));
candidatesByRequestId.put(requestId, candidate);
}
if (requests.isEmpty()) {
return;
}
Map<String, Allocation> allocations= GeneratedNameAllocator.allocateNestedTypes(roots, requests);
for (Map.Entry<String, Allocation> entry : allocations.entrySet()) {
MultiFilePlanningBudget.checkCanceled(monitor);
Allocation allocation= entry.getValue();
if (allocation.available()) {
continue;
}
CandidateBuilder candidate= candidatesByRequestId.get(entry.getKey());
if (candidate != null) {
candidate.invalidate("GENERATED_NAME_COLLISION", //$NON-NLS-1$
"Nested enum " + candidate.enumName + " cannot be generated in " //$NON-NLS-1$ //$NON-NLS-2$
+ candidate.ownerTypeQualifiedName + ": " + allocation.diagnosticMessage() + '.'); //$NON-NLS-1$
}
}
}
private static FreezeResult freeze(List<CandidateBuilder> builders, IProgressMonitor monitor) {
List<IntEnumCandidate> result= new ArrayList<>();
List<MultiFileCandidateDiagnostic> diagnostics= new ArrayList<>();
for (CandidateBuilder builder : builders) {
MultiFilePlanningBudget.checkCanceled(monitor);
if (!builder.valid) {
diagnostics.add(MultiFileCandidateDiagnostic.rejected(builder.candidateId(), builder.ownerUnitHandle,
builder.rejectionCode, builder.rejectionMessage, builder.relatedUnitHandles()));
continue;
}
if (!builder.hasExternalCaller) {
continue;
}
Map<String, Map<String, Integer>> referenceCounts= new LinkedHashMap<>();
for (Map.Entry<Expression, String> reference : builder.recognisedReferences.entrySet()) {
MultiFilePlanningBudget.checkCanceled(monitor);
CompilationUnit root= root(reference.getKey());
if (root == null || !(root.getJavaElement() instanceof ICompilationUnit unit)) {
builder.invalidate("UNRESOLVED_SOURCE_REFERENCE", //$NON-NLS-1$
"A modeled reference can no longer be mapped to a source compilation unit."); //$NON-NLS-1$
break;
}
referenceCounts.computeIfAbsent(unit.getPrimary().getHandleIdentifier(), key -> new LinkedHashMap<>())
.merge(reference.getValue(), Integer.valueOf(1), Integer::sum);
}
if (!builder.valid) {
diagnostics.add(MultiFileCandidateDiagnostic.rejected(builder.candidateId(), builder.ownerUnitHandle,
builder.rejectionCode, builder.rejectionMessage, builder.relatedUnitHandles()));
continue;
}
List<IntEnumConstant> constants= builder.constants.stream()
.map(constant -> new IntEnumConstant(constant.bindingKey(), constant.name(),
constant.name().substring(builder.prefix.length()), constant.value()))
.toList();
result.add(new IntEnumCandidate(builder.ownerUnitHandle, builder.ownerTypeKey,
builder.ownerTypeQualifiedName, builder.methodKey, builder.parameterIndex, builder.prefix,
builder.enumName, constants, referenceCounts, builder.callCountsByUnit));
diagnostics.add(MultiFileCandidateDiagnostic.transformed(builder.candidateId(), builder.ownerUnitHandle,
"Migrates the closed integer-state flow to nested enum " + builder.enumName + '.', //$NON-NLS-1$
builder.relatedUnitHandles()));
}
return new FreezeResult(result, diagnostics);
}
private static Map<String, ConstantDecl> collectPackageConstants(TypeDeclaration type) {
Map<String, ConstantDecl> result= new LinkedHashMap<>();
for (FieldDeclaration field : type.getFields()) {
if (!isPackagePrivate(field.getModifiers()) || !Modifier.isStatic(field.getModifiers())
|| !Modifier.isFinal(field.getModifiers()) || !isInt(field.getType())) {
continue;
}
for (Object fragmentObject : field.fragments()) {
VariableDeclarationFragment fragment= (VariableDeclarationFragment) fragmentObject;
IVariableBinding binding= fragment.resolveBinding();
Object value= binding == null ? null : binding.getConstantValue();
String key= binding == null ? null : binding.getVariableDeclaration().getKey();
if (key != null && value instanceof Integer integer) {
result.put(key, new ConstantDecl(key, fragment.getName().getIdentifier(), integer.intValue()));
}
}
}
return result;
}
private static IntEnumConstant frozenConstant(CandidateBuilder candidate, IVariableBinding binding) {
if (binding == null) {
return null;
}
String key= binding.getVariableDeclaration().getKey();
for (ConstantDecl constant : candidate.constants) {
if (constant.bindingKey().equals(key)) {
return new IntEnumConstant(key, constant.name(), constant.name().substring(candidate.prefix.length()),
constant.value());
}
}
return null;
}
private static IVariableBinding resolveVariable(Expression expression) {
Expression unwrapped= unwrap(expression);
IBinding binding= null;
if (unwrapped instanceof Name name) {
binding= name.resolveBinding();
} else if (unwrapped instanceof FieldAccess fieldAccess) {
binding= fieldAccess.resolveFieldBinding();
}
return binding instanceof IVariableBinding variable ? variable.getVariableDeclaration() : null;
}
private static Expression unwrap(Expression expression) {
Expression current= expression;
while (current instanceof ParenthesizedExpression parenthesized) {
current= parenthesized.getExpression();
}
return current;
}
private static Expression containingExpression(SimpleName name) {
ASTNode parent= name.getParent();
if (parent instanceof QualifiedName qualified && qualified.getName() == name) {
return qualified;
}
if (parent instanceof FieldAccess access && access.getName() == name) {
return access;
}
return name;
}
private static CompilationUnit root(ASTNode node) {
ASTNode current= node;
while (current != null && !(current instanceof CompilationUnit)) {
current= current.getParent();
}
return (CompilationUnit) current;
}
private static boolean isVariableDeclarationName(SimpleName name) {
return name.getParent() instanceof VariableDeclarationFragment fragment && fragment.getName() == name;
}
private static boolean isParameterDeclarationName(SimpleName name) {
return name.getParent() instanceof SingleVariableDeclaration parameter && parameter.getName() == name;
}
private static boolean isMethodDeclarationName(SimpleName name) {
return name.getParent() instanceof MethodDeclaration method && method.getName() == name;
}
private static boolean isPackagePrivate(int modifiers) {
return !Modifier.isPublic(modifiers) && !Modifier.isProtected(modifiers) && !Modifier.isPrivate(modifiers);
}
private static boolean isPlainInt(SingleVariableDeclaration parameter) {
return !parameter.isVarargs() && parameter.extraDimensions().isEmpty() && isInt(parameter.getType());
}
private static boolean isInt(Type type) {
return type.isPrimitiveType()
&& ((PrimitiveType) type).getPrimitiveTypeCode() == PrimitiveType.INT;
}
private static boolean sameVariable(IVariableBinding first, IVariableBinding second) {
return first != null && second != null
&& first.getVariableDeclaration().isEqualTo(second.getVariableDeclaration());
}
private static String typeKey(ITypeBinding binding) {
return binding == null ? null : binding.getTypeDeclaration().getKey();
}
private static boolean distinctValues(Collection<ConstantDecl> constants) {
Set<Integer> values= new HashSet<>();
return constants.stream().allMatch(constant -> values.add(Integer.valueOf(constant.value())));
}
private static String commonPrefix(List<String> names) {
if (names.isEmpty()) {
return null;
}
String first= names.get(0);
int end= first.length();
for (String name : names) {
end= Math.min(end, name.length());
int index= 0;
while (index < end && first.charAt(index) == name.charAt(index)) {
index++;
}
end= index;
}
int underscore= first.substring(0, end).lastIndexOf('_');
return underscore > 0 ? first.substring(0, underscore + 1) : null;
}
private static String enumTypeName(String prefix) {
String raw= prefix.endsWith("_") ? prefix.substring(0, prefix.length() - 1) : prefix; //$NON-NLS-1$ //$NON-NLS-2$
StringBuilder result= new StringBuilder();
boolean capitalize= true;
for (char character : raw.toLowerCase().toCharArray()) {
if (character == '_') {
capitalize= true;
} else {
result.append(capitalize ? Character.toUpperCase(character) : character);
capitalize= false;
}
}
return result.toString();
}
private static boolean validIdentifier(String name) {
return SourceVersion.isIdentifier(name) && !SourceVersion.isKeyword(name);
}
}