JavaStructureVisitor.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.eclipse.jgit.storage.hibernate.search;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.CatchClause;
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
/**
* An {@link ASTVisitor} that collects structural metadata from Java source
* files.
* <p>
* Extracts declared types, methods, fields, supertypes, and interfaces from
* the AST. Fully qualified names are constructed using the package name and
* an import map for best-effort resolution of simple type references.
* </p>
*/
public class JavaStructureVisitor extends ASTVisitor {
private static final int MAX_DOC_LENGTH = 2000;
private static final int MIN_STRING_LITERAL_LENGTH = 3;
private final Map<String, String> importMap;
private final String packageName;
private final List<String> types = new ArrayList<>();
private final List<String> fqns = new ArrayList<>();
private final List<String> methods = new ArrayList<>();
private final List<String> fields = new ArrayList<>();
private final List<String> superTypes = new ArrayList<>();
private final List<String> interfaces = new ArrayList<>();
private final List<String> annotationNames = new ArrayList<>();
private final List<String> methodSignatures = new ArrayList<>();
private final List<String> referencedTypes = new ArrayList<>();
private final List<String> stringLiterals = new ArrayList<>();
private boolean hasMainMethod;
private String typeDocumentation = ""; //$NON-NLS-1$
private String typeKind = ""; //$NON-NLS-1$
private String visibility = ""; //$NON-NLS-1$
/**
* Create a new visitor.
*
* @param importMap
* mapping from simple names to fully qualified names
* @param packageName
* the package name for FQN construction
*/
public JavaStructureVisitor(Map<String, String> importMap,
String packageName) {
this.importMap = importMap;
this.packageName = packageName != null ? packageName : ""; //$NON-NLS-1$
}
@Override
public boolean visit(TypeDeclaration node) {
String name = node.getName().getIdentifier();
types.add(name);
fqns.add(buildFQN(name));
if (node.getSuperclassType() != null) {
superTypes.add(resolveTypeName(node.getSuperclassType()));
}
for (Object iface : node.superInterfaceTypes()) {
if (iface instanceof Type) {
interfaces.add(resolveTypeName((Type) iface));
}
}
if (typeKind.isEmpty()) {
typeKind = node.isInterface() ? "interface" : "class"; //$NON-NLS-1$ //$NON-NLS-2$
visibility = extractVisibility(node.getModifiers());
}
// Capture Javadoc of the primary type
if (typeDocumentation.isEmpty() && node.getJavadoc() != null) {
typeDocumentation = truncateDoc(
node.getJavadoc().toString(), MAX_DOC_LENGTH);
}
return true;
}
@Override
public boolean visit(EnumDeclaration node) {
String name = node.getName().getIdentifier();
types.add(name);
fqns.add(buildFQN(name));
for (Object constant : node.enumConstants()) {
if (constant instanceof EnumConstantDeclaration ecd) {
fields.add(ecd.getName().getIdentifier());
}
}
for (Object iface : node.superInterfaceTypes()) {
if (iface instanceof Type) {
interfaces.add(resolveTypeName((Type) iface));
}
}
if (typeKind.isEmpty()) {
typeKind = "enum"; //$NON-NLS-1$
visibility = extractVisibility(node.getModifiers());
}
return true;
}
@Override
public boolean visit(AnnotationTypeDeclaration node) {
String name = node.getName().getIdentifier();
types.add(name);
fqns.add(buildFQN(name));
if (typeKind.isEmpty()) {
typeKind = "annotation"; //$NON-NLS-1$
visibility = extractVisibility(node.getModifiers());
}
return true;
}
@Override
public boolean visit(MarkerAnnotation node) {
annotationNames.add(resolveAnnotationName(
node.getTypeName().getFullyQualifiedName()));
return true;
}
@Override
public boolean visit(NormalAnnotation node) {
annotationNames.add(resolveAnnotationName(
node.getTypeName().getFullyQualifiedName()));
return true;
}
@Override
public boolean visit(SingleMemberAnnotation node) {
annotationNames.add(resolveAnnotationName(
node.getTypeName().getFullyQualifiedName()));
return true;
}
@Override
public boolean visit(MethodDeclaration node) {
String methodName = node.getName().getIdentifier();
methods.add(methodName);
// Build method signature: methodName(ParamType1,ParamType2)
StringBuilder sig = new StringBuilder();
sig.append(methodName);
sig.append('(');
boolean first = true;
for (Object param : node.parameters()) {
if (param instanceof SingleVariableDeclaration svd) {
if (!first) {
sig.append(',');
}
String typeName = resolveTypeName(svd.getType());
sig.append(typeName);
referencedTypes.add(typeName);
first = false;
}
}
sig.append(')');
methodSignatures.add(sig.toString());
// Return type
Type returnType = node.getReturnType2();
if (returnType != null) {
referencedTypes.add(resolveTypeName(returnType));
}
// Detect main method
if ("main".equals(methodName) //$NON-NLS-1$
&& Modifier.isPublic(node.getModifiers())
&& Modifier.isStatic(node.getModifiers())
&& node.parameters().size() == 1) {
hasMainMethod = true;
}
return true;
}
@Override
public boolean visit(FieldDeclaration node) {
for (Object fragment : node.fragments()) {
if (fragment instanceof VariableDeclarationFragment vdf) {
fields.add(vdf.getName().getIdentifier());
}
}
// Collect field type as referenced type
if (node.getType() != null) {
referencedTypes.add(resolveTypeName(node.getType()));
}
return true;
}
@Override
public boolean visit(CatchClause node) {
if (node.getException() != null
&& node.getException().getType() != null) {
referencedTypes.add(
resolveTypeName(node.getException().getType()));
}
return true;
}
@Override
public boolean visit(StringLiteral node) {
String value = node.getLiteralValue();
if (value != null && value.length() > MIN_STRING_LITERAL_LENGTH) {
stringLiterals.add(value);
}
return true;
}
/**
* Get the newline-separated list of declared type names.
*
* @return declared types
*/
public String getTypes() {
return String.join("\n", types); //$NON-NLS-1$
}
/**
* Get the newline-separated list of fully qualified names.
*
* @return fully qualified names
*/
public String getFQNs() {
return String.join("\n", fqns); //$NON-NLS-1$
}
/**
* Get the newline-separated list of declared methods.
*
* @return declared methods
*/
public String getMethods() {
return String.join("\n", methods); //$NON-NLS-1$
}
/**
* Get the newline-separated list of declared fields.
*
* @return declared fields
*/
public String getFields() {
return String.join("\n", fields); //$NON-NLS-1$
}
/**
* Get the newline-separated list of supertypes.
*
* @return supertypes
*/
public String getSuperTypes() {
return String.join("\n", superTypes); //$NON-NLS-1$
}
/**
* Get the newline-separated list of implemented interfaces.
*
* @return implemented interfaces
*/
public String getInterfaces() {
return String.join("\n", interfaces); //$NON-NLS-1$
}
/**
* Get the newline-separated list of annotation names.
*
* @return annotations
*/
public String getAnnotations() {
return String.join("\n", annotationNames); //$NON-NLS-1$
}
/**
* Get the type kind (class, interface, enum, annotation).
*
* @return the type kind
*/
public String getTypeKind() {
return typeKind;
}
/**
* Get the visibility modifier string.
*
* @return the visibility
*/
public String getVisibility() {
return visibility;
}
private String buildFQN(String simpleName) {
if (packageName.isEmpty()) {
return simpleName;
}
return packageName + "." + simpleName; //$NON-NLS-1$
}
private String resolveTypeName(Type type) {
String simpleName;
if (type instanceof SimpleType simpleType) {
simpleName = simpleType.getName().getFullyQualifiedName();
} else if (type instanceof ParameterizedType paramType) {
// For List<String>, extract just "List"
return resolveTypeName(paramType.getType());
} else {
simpleName = type.toString();
}
if (importMap.containsKey(simpleName)) {
return importMap.get(simpleName);
}
// Best effort: assume same package
return buildFQN(simpleName);
}
private String resolveAnnotationName(String simpleName) {
if (importMap.containsKey(simpleName)) {
return importMap.get(simpleName);
}
return simpleName;
}
private static String extractVisibility(int modifiers) {
StringBuilder sb = new StringBuilder();
if (Modifier.isPublic(modifiers)) {
sb.append("public"); //$NON-NLS-1$
} else if (Modifier.isProtected(modifiers)) {
sb.append("protected"); //$NON-NLS-1$
} else if (Modifier.isPrivate(modifiers)) {
sb.append("private"); //$NON-NLS-1$
} else {
sb.append("package"); //$NON-NLS-1$
}
if (Modifier.isAbstract(modifiers)) {
sb.append(" abstract"); //$NON-NLS-1$
}
if (Modifier.isFinal(modifiers)) {
sb.append(" final"); //$NON-NLS-1$
}
return sb.toString().trim();
}
private static String truncateDoc(String doc, int maxLen) {
if (doc == null) {
return ""; //$NON-NLS-1$
}
// Strip leading/trailing whitespace and comment markers
String clean = doc.replaceAll("/\\*\\*|\\*/|\\*", "").trim(); //$NON-NLS-1$ //$NON-NLS-2$
if (clean.length() > maxLen) {
return clean.substring(0, maxLen);
}
return clean;
}
/**
* Get the newline-separated list of method signatures.
*
* @return method signatures
*/
public String getMethodSignatures() {
return String.join("\n", methodSignatures); //$NON-NLS-1$
}
/**
* Get the newline-separated list of referenced types.
*
* @return referenced types
*/
public String getReferencedTypes() {
return String.join("\n", referencedTypes); //$NON-NLS-1$
}
/**
* Get the newline-separated list of string literals.
*
* @return string literals
*/
public String getStringLiterals() {
return String.join("\n", stringLiterals); //$NON-NLS-1$
}
/**
* Check if a main method was detected.
*
* @return true if a main method was found
*/
public boolean hasMainMethod() {
return hasMainMethod;
}
/**
* Get the type documentation (Javadoc).
*
* @return the type documentation
*/
public String getTypeDocumentation() {
return typeDocumentation;
}
}