NamingUtils.java
/*******************************************************************************
* Copyright (c) 2025 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.util;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.QualifiedType;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
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.GeneratedNameHierarchyPolicy;
/**
* Utility class for naming and string operations.
* Provides methods for class name generation, type name extraction,
* and string transformations needed during JUnit migration.
*/
public final class NamingUtils {
/** Length in hexadecimal characters of the checksum used for generated nested class names to ensure uniqueness */
private static final int GENERATED_CLASS_NAME_CHECKSUM_LENGTH = 5;
private NamingUtils() {
// Utility class - prevent instantiation
}
/**
* Checks if a string is a valid Java identifier.
* A valid Java identifier starts with a character for which
* {@link Character#isJavaIdentifierStart(char)} returns true,
* followed by characters for which {@link Character#isJavaIdentifierPart(char)} returns true.
*
* @param name the string to check, may be null
* @return true if the string is a valid Java identifier, false otherwise
*/
public static boolean isValidJavaIdentifier(String name) {
if (name == null || name.isEmpty()) {
return false;
}
if (!Character.isJavaIdentifierStart(name.charAt(0))) {
return false;
}
for (int i = 1; i < name.length(); i++) {
if (!Character.isJavaIdentifierPart(name.charAt(i))) {
return false;
}
}
return true;
}
/**
* Capitalizes the first letter of a string.
*
* @param input the string to capitalize
* @return the string with first letter capitalized, or the original string if null or empty
*/
public static String capitalizeFirstLetter(String input) {
if (input == null || input.isEmpty()) {
return input;
}
return Character.toUpperCase(input.charAt(0)) + input.substring(1);
}
/**
* Generates and validates a deterministic nested helper class name.
*
* <p>The historical field-name/checksum convention is retained for stable
* output, but the proposed name is now checked by the shared generated-name
* services against type, member, local, import and inherited member-type
* namespaces before any rewrite is committed.</p>
*
* @param anonymousClass the anonymous class declaration
* @param baseName the base name from the field
* @return the available class name
* @throws IllegalStateException if owner context is unavailable or the name collides
*/
public static String generateUniqueNestedClassName(AnonymousClassDeclaration anonymousClass, String baseName) {
String anonymousCode = anonymousClass.toString();
String checksum = generateChecksum(anonymousCode);
String requestedName = capitalizeFirstLetter(baseName) + "_" + checksum; //$NON-NLS-1$
if (!(anonymousClass.getRoot() instanceof CompilationUnit root)) {
throw new IllegalStateException("Cannot validate generated helper name without a compilation unit."); //$NON-NLS-1$
}
TypeDeclaration owner= ASTNavigationUtils.findEnclosingTypeDeclaration(anonymousClass);
if (owner == null) {
throw new IllegalStateException("Cannot validate generated helper name without an enclosing type."); //$NON-NLS-1$
}
ITypeBinding ownerBinding= owner.resolveBinding();
ITypeBinding ownerDeclaration= ownerBinding == null ? null : ownerBinding.getTypeDeclaration();
String ownerKey= ownerDeclaration == null || ownerDeclaration.getKey() == null
? "" : ownerDeclaration.getKey(); //$NON-NLS-1$
String ownerQualifiedName= ownerDeclaration == null || ownerDeclaration.getQualifiedName().isEmpty()
? qualifiedName(root, owner) : ownerDeclaration.getQualifiedName();
String unitHandle= root.getJavaElement() instanceof ICompilationUnit unit
? unit.getPrimary().getHandleIdentifier() : ""; //$NON-NLS-1$
String requestId= (ownerKey.isEmpty() ? ownerQualifiedName : ownerKey) + "#junit-helper:" + requestedName; //$NON-NLS-1$
NestedTypeRequest request= new NestedTypeRequest(requestId, unitHandle, ownerKey, ownerQualifiedName,
requestedName);
Allocation allocation= GeneratedNameAllocator.allocateNestedTypes(List.of(root), List.of(request))
.get(requestId);
if (allocation == null || !allocation.available()) {
String detail= allocation == null ? "allocation result is missing" : allocation.diagnosticMessage(); //$NON-NLS-1$
throw new IllegalStateException("Generated nested helper name " + requestedName //$NON-NLS-1$
+ " is unavailable: " + detail); //$NON-NLS-1$
}
List<String> inheritedCollisions= GeneratedNameHierarchyPolicy
.inheritedMemberTypeCollisions(ownerDeclaration, requestedName);
if (!inheritedCollisions.isEmpty()) {
throw new IllegalStateException("Generated nested helper name " + requestedName //$NON-NLS-1$
+ " is unavailable: inherited member type (" //$NON-NLS-1$
+ String.join(", ", inheritedCollisions) + ")"); //$NON-NLS-1$ //$NON-NLS-2$
}
return requestedName;
}
/**
* Generates a short SHA-256 checksum for the given input.
*
* @param input the string to hash
* @return a 5-character hexadecimal checksum
* @throws RuntimeException if SHA-256 algorithm is not available (should never happen in standard JVM environments)
*/
public static String generateChecksum(String input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256"); //$NON-NLS-1$
byte[] hashBytes = md.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hashBytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString().substring(0, GENERATED_CLASS_NAME_CHECKSUM_LENGTH);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 algorithm not found", e); //$NON-NLS-1$
}
}
/**
* Extracts the class name from a field declaration's initializer.
*
* @param field the field declaration to extract from
* @return the class name, or null if not found
*/
public static String extractClassNameFromField(FieldDeclaration field) {
for (Object fragmentObj : field.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment
&& fragment.getInitializer() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation creation) {
return extractTypeName(creation.getType());
}
}
return null;
}
/**
* Extracts the field name from a field declaration.
*
* @param fieldDeclaration the field declaration
* @return the field name, or "UnnamedField" if not found
*/
public static String extractFieldName(FieldDeclaration fieldDeclaration) {
return (String) fieldDeclaration.fragments().stream()
.filter(VariableDeclarationFragment.class::isInstance)
.map(fragment -> ((VariableDeclarationFragment) fragment).getName().getIdentifier())
.findFirst()
.orElse("UnnamedField"); //$NON-NLS-1$
}
/**
* Extracts the fully qualified type name from a QualifiedType AST node.
*
* @param qualifiedType the qualified type to extract from
* @return the fully qualified class name
*/
public static String extractQualifiedTypeName(QualifiedType qualifiedType) {
StringBuilder fullClassName = new StringBuilder();
Type currentType = qualifiedType;
while (currentType instanceof QualifiedType currentQualified) {
if (fullClassName.length() > 0) {
fullClassName.insert(0, "."); //$NON-NLS-1$
}
fullClassName.insert(0, currentQualified.getName().getFullyQualifiedName());
currentType = currentQualified.getQualifier();
}
return fullClassName.toString();
}
/**
* General method to extract a type's fully qualified name.
*
* @param type the type to extract from
* @return the type name, or null if not a recognized type
*/
public static String extractTypeName(Type type) {
if (type instanceof QualifiedType qualifiedType) {
return extractQualifiedTypeName(qualifiedType);
} else if (type instanceof SimpleType simpleType) {
return simpleType.getName().getFullyQualifiedName();
}
return null;
}
private static String qualifiedName(CompilationUnit root, TypeDeclaration owner) {
List<String> typeNames= new ArrayList<>();
ASTNode current= owner;
while (current != null) {
if (current instanceof TypeDeclaration type) {
typeNames.add(0, type.getName().getIdentifier());
}
current= current.getParent();
}
String packageName= root.getPackage() == null
? "" : root.getPackage().getName().getFullyQualifiedName(); //$NON-NLS-1$
String localName= String.join(".", typeNames); //$NON-NLS-1$
return packageName.isEmpty() ? localName : packageName + '.' + localName;
}
}