Repository: javaparser/javaparser
Pull request #4716: Make JarTypeSolver and ReflectionTypeSolver a bit more versatile.
Author: login: johanneskloos
Created: 2025-04-08T16:28:35Z | Merged: 2025-04-09T16:04:41Z

Two improvements to `TypeSolver`:
- `JarTypeSolver` can now also resolves types that live in `.class` files hierarchies (think the output from `javac`), without packaging them in a jar file first.
- `ReflectionTypeSolver` gets more flexible filtering; in particular, a pre-made filter is added for the entire Java Class Library.


Diff:
diff --git a/javaparser-symbol-solver-core/src/main/java/com/github/javaparser/symbolsolver/resolution/typesolvers/JarTypeSolver.java b/javaparser-symbol-solver-core/src/main/java/com/github/javaparser/symbolsolver/resolution/typesolvers/JarTypeSolver.java
index 4c1216f98d..65e0bd5d55 100644
--- a/javaparser-symbol-solver-core/src/main/java/com/github/javaparser/symbolsolver/resolution/typesolvers/JarTypeSolver.java
+++ b/javaparser-symbol-solver-core/src/main/java/com/github/javaparser/symbolsolver/resolution/typesolvers/JarTypeSolver.java
@@ -27,10 +27,13 @@
 import com.github.javaparser.resolution.model.SymbolReference;
 import com.github.javaparser.symbolsolver.javassistmodel.JavassistFactory;
 import java.io.*;
+import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.*;
 import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
+import java.util.stream.Stream;
 import javassist.ClassPool;
 import javassist.NotFoundException;
 
@@ -99,34 +102,34 @@ private static String convertEntryPathToClassPoolName(String entryPath) {
     /**
      * Create a {@link JarTypeSolver} from a {@link Path}.
      *
-     * @param pathToJar The path where the jar is located.
+     * @param pathToJarOrClassFileHierarchy The path where the jar or class file hierarchy is located.
      *
-     * @throws IOException If an I/O exception occurs while reading the Jar.
+     * @throws IOException If an I/O exception occurs while reading the Jar or class file hierarchy.
      */
-    public JarTypeSolver(Path pathToJar) throws IOException {
-        this(pathToJar.toFile());
+    public JarTypeSolver(Path pathToJarOrClassFileHierarchy) throws IOException {
+        this(pathToJarOrClassFileHierarchy.toFile());
     }
 
     /**
      * Create a {@link JarTypeSolver} from a {@link File}.
      *
-     * @param pathToJar The file pointing to the jar is located.
+     * @param pathToJarOrClassFileHierarchy The file pointing to the jar or class file hierarchy is located.
      *
-     * @throws IOException If an I/O exception occurs while reading the Jar.
+     * @throws IOException If an I/O exception occurs while reading the Jar or class file hierarchy.
      */
-    public JarTypeSolver(File pathToJar) throws IOException {
-        this(pathToJar.getAbsolutePath());
+    public JarTypeSolver(File pathToJarOrClassFileHierarchy) throws IOException {
+        this(pathToJarOrClassFileHierarchy.getAbsolutePath());
     }
 
     /**
      * Create a {@link JarTypeSolver} from a path in a {@link String} format.
      *
-     * @param pathToJar The path pointing to the jar.
+     * @param pathToJarOrClassFileHierarchy The path pointing to the jar or class file hierarchy.
      *
-     * @throws IOException If an I/O exception occurs while reading the Jar.
+     * @throws IOException If an I/O exception occurs while reading the Jar or class file hierarchy.
      */
-    public JarTypeSolver(String pathToJar) throws IOException {
-        addPathToJar(pathToJar);
+    public JarTypeSolver(String pathToJarOrClassFileHierarchy) throws IOException {
+        addPathToJar(pathToJarOrClassFileHierarchy);
     }
 
     /**
@@ -173,14 +176,14 @@ private File dumpToTempFile(InputStream inputStream) throws IOException {
     /**
      * Utility method to register a new class path.
      *
-     * @param pathToJar The path pointing to the jar file.
+     * @param pathToJarOrClassFileHierarchy The path pointing to the jar file or class file hierarchy.
      *
-     * @throws IOException If an I/O error occurs while reading the JarFile.
+     * @throws IOException If an I/O error occurs while reading the JarFile or class file hierarchy.
      */
-    private void addPathToJar(String pathToJar) throws IOException {
+    private void addPathToJar(String pathToJarOrClassFileHierarchy) throws IOException {
         try {
-            classPool.appendClassPath(pathToJar);
-            registerKnownClassesFor(pathToJar);
+            classPool.appendClassPath(pathToJarOrClassFileHierarchy);
+            registerKnownClassesFor(pathToJarOrClassFileHierarchy);
         } catch (NotFoundException e) {
             // If JavaAssist throws a NotFoundException we should notify the user
             // with a FileNotFoundException.
@@ -196,34 +199,78 @@ private void addPathToJar(String pathToJar) throws IOException {
      * When we create a new {@link JarTypeSolver} we should store the list of
      * solvable types.
      *
-     * @param pathToJar The path to the jar file.
+     * @param pathToJarOrClassFileHierarchy The path to the jar file or .class file hierarchy.
      *
-     * @throws IOException If an I/O error occurs while reading the JarFile.
+     * @throws IOException If an I/O error occurs while reading the JarFile or .class file hierarchy.
      */
-    private void registerKnownClassesFor(String pathToJar) throws IOException {
-        try (JarFile jarFile = new JarFile(pathToJar)) {
+    private void registerKnownClassesFor(String pathToJarOrClassFileHierarchy) throws IOException {
+        Path jarOrClassFileHierarchy = Paths.get(pathToJarOrClassFileHierarchy);
+        if (Files.isDirectory(jarOrClassFileHierarchy)) {
+            try (Stream<Path> classFiles = Files.walk(jarOrClassFileHierarchy)) {
+                classFiles
+                        .filter(path -> Files.isRegularFile(path)
+                                && path.getFileName().toString().endsWith(CLASS_EXTENSION))
+                        .forEach(pathToClassFile -> {
+                            // We can't just use path.toString here, since path seperators are OS-dependent.
+                            String packagePrefix = convertPathToPackagePrefix(jarOrClassFileHierarchy, pathToClassFile);
+                            String classFileName = pathToClassFile.getFileName().toString();
+                            String classNameWithDollars =
+                                    classFileName.substring(0, classFileName.length() - CLASS_EXTENSION.length());
+                            String classPoolName = packagePrefix + classNameWithDollars;
+                            String qualifiedName = packagePrefix + classNameWithDollars.replace('$', '.');
 
-            Enumeration<JarEntry> jarEntries = jarFile.entries();
-            while (jarEntries.hasMoreElements()) {
+                            // If the qualified name is the same as the class pool name we don't need to duplicate store
+                            // two
+                            // different String instances. Let's reuse the same.
+                            if (qualifiedName.equals(classPoolName)) {
+                                knownClasses.put(qualifiedName, qualifiedName);
+                            } else {
+                                knownClasses.put(qualifiedName, classPoolName);
+                            }
+                        });
+            }
+        } else if (Files.isRegularFile(jarOrClassFileHierarchy)) {
+            try (JarFile jarFile = new JarFile(pathToJarOrClassFileHierarchy)) {
+
+                Enumeration<JarEntry> jarEntries = jarFile.entries();
+                while (jarEntries.hasMoreElements()) {
 
-                JarEntry entry = jarEntries.nextElement();
-                // Check if the entry is a .class file
-                if (!entry.isDirectory() && entry.getName().endsWith(CLASS_EXTENSION)) {
-                    String qualifiedName = convertEntryPathToClassName(entry.getName());
-                    String classPoolName = convertEntryPathToClassPoolName(entry.getName());
+                    JarEntry entry = jarEntries.nextElement();
+                    // Check if the entry is a .class file
+                    if (!entry.isDirectory() && entry.getName().endsWith(CLASS_EXTENSION)) {
+                        String qualifiedName = convertEntryPathToClassName(entry.getName());
+                        String classPoolName = convertEntryPathToClassPoolName(entry.getName());
 
-                    // If the qualified name is the same as the class pool name we don't need to duplicate store two
-                    // different String instances. Let's reuse the same.
-                    if (qualifiedName.equals(classPoolName)) {
-                        knownClasses.put(qualifiedName, qualifiedName);
-                    } else {
-                        knownClasses.put(qualifiedName, classPoolName);
+                        // If the qualified name is the same as the class pool name we don't need to duplicate store two
+                        // different String instances. Let's reuse the same.
+                        if (qualifiedName.equals(classPoolName)) {
+                            knownClasses.put(qualifiedName, qualifiedName);
+                        } else {
+                            knownClasses.put(qualifiedName, classPoolName);
+                        }
                     }
                 }
             }
         }
     }
 
+    /**
+     * Given a path to a class file inside a class file hierarchy, extract the package name from the path.
+     * @param pathToClassFileHierarchy the root of the class file hierarchy
+     * @param pathToClassFile the path to the class file, inside the hierarchy
+     * @return the package name of the class file
+     */
+    private static String convertPathToPackagePrefix(Path pathToClassFileHierarchy, Path pathToClassFile) {
+        Path packagePath = pathToClassFileHierarchy.relativize(pathToClassFile).getParent();
+        StringBuilder packagePrefixBuilder = new StringBuilder();
+        if (packagePath != null) {
+            for (Path component : packagePath) {
+                packagePrefixBuilder.append(component.toString()).append('.');
+            }
+        }
+        return packagePrefixBuilder.toString();
+    }
+
     /**
      * Get the set of classes that can be resolved in the current type solver.
      *
diff --git a/javaparser-symbol-solver-core/src/main/java/com/github/javaparser/symbolsolver/resolution/typesolvers/ReflectionTypeSolver.java b/javaparser-symbol-solver-core/src/main/java/com/github/javaparser/symbolsolver/resolution/typesolvers/ReflectionTypeSolver.java
index 1108953f0a..7b899f2fa4 100644
--- a/javaparser-symbol-solver-core/src/main/java/com/github/javaparser/symbolsolver/resolution/typesolvers/ReflectionTypeSolver.java
+++ b/javaparser-symbol-solver-core/src/main/java/com/github/javaparser/symbolsolver/resolution/typesolvers/ReflectionTypeSolver.java
@@ -21,6 +21,9 @@
 
 package com.github.javaparser.symbolsolver.resolution.typesolvers;
 
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
 /**
  * Uses reflection to resolve types.
  * Classes on the classpath used to run your application will be found.
@@ -29,8 +32,52 @@
  * @author Federico Tomassetti
  */
 public class ReflectionTypeSolver extends ClassLoaderTypeSolver {
+    private final Predicate<String> classFilter;
+    /**
+     * Class name filter matching all classes.
+     */
+    public static final Predicate<String> ALL_CLASSES = name -> true;
+    /**
+     * Class name filter matching classes in the core Java standard library.
+     * This includes everything under {@code java.} and {@code javax.}.
+     */
+    public static final Predicate<String> JRE_ONLY = name -> name.startsWith("java.") || name.startsWith("javax.");
+    /**
+     * Class name filter matching classes in the Java Class Library.
+     * The Java Class Library is the Java standard library; this filter includes all
+     * packages listed in the {@code java.se} module of Java 21, as well as
+     * {@code java.corba} as of Java 9. In this way, it encompasses the JCL of all
+     * Java version from Java 8 to 21.
+     */
+    public static final Predicate<String> JCL_ONLY = name -> Stream.of(
+                    "java.",
+                    "javax.",
+                    "org.ietf.jgss.",
+                    "org.omg.COBRA.",
+                    "org.omg.COBRA_2_3.",
+                    "org.omg.CosNaming.",
+                    "org.omg.Dynamic.",
+                    "org.omg.DynamicAny.",
+                    "org.omg.IOP.",
+                    "org.omg.Messaging.",
+                    "org.omg.PortableInterceptor.",
+                    "org.omg.PortableServer.",
+                    "org.omg.stub.java.rmi.",
+                    "org.w3c.dom.",
+                    "org.xml.sax.")
+            .anyMatch(name::startsWith);
 
-    private final boolean jreOnly;
+    /**
+     * @param classFilter a name-based filter indicating which types to resolve.
+     *                    Similarily to {@link ReflectionTypeSolver(boolean)}, this allows you to specify "I need to resolve certain classes (like the Java standard library), and whatever is available on the classpath is fine.".
+     * @see #ALL_CLASSES
+     * @see #JRE_ONLY
+     * @see #JCL_ONLY
+     */
+    public ReflectionTypeSolver(Predicate<String> classFilter) {
+        super(ReflectionTypeSolver.class.getClassLoader());
+        this.classFilter = classFilter;
+    }
 
     /**
      * @param jreOnly if true, will only resolve types from the java or javax packages.
@@ -38,8 +85,7 @@ public class ReflectionTypeSolver extends ClassLoaderTypeSolver {
      * If false, will resolve any kind of type.
      */
     public ReflectionTypeSolver(boolean jreOnly) {
-        super(ReflectionTypeSolver.class.getClassLoader());
-        this.jreOnly = jreOnly;
+        this(jreOnly ? JRE_ONLY : ALL_CLASSES);
     }
 
     /**
@@ -52,6 +98,6 @@ public ReflectionTypeSolver() {
 
     @Override
     protected boolean filterName(String name) {
-        return !jreOnly || (name.startsWith("java.") || name.startsWith("javax."));
+        return classFilter.test(name);
     }
 }
diff --git a/javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/resolution/typesolvers/JarTypeSolverTest.java b/javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/resolution/typesolvers/JarTypeSolverTest.java
index a907239ee8..7be10450cc 100644
--- a/javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/resolution/typesolvers/JarTypeSolverTest.java
+++ b/javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/resolution/typesolvers/JarTypeSolverTest.java
@@ -30,9 +30,12 @@
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.List;
 import java.util.function.Supplier;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
@@ -78,6 +81,33 @@ void initial() throws IOException {
         assertEquals(false, jarTypeSolver.tryToSolveType("Foo").isSolved());
     }
 
+    @Test
+    void classFileHierarchy(@TempDir Path tempDir) throws IOException {
+        Path pathToJar = adaptPath("src/test/resources/javaparser-core-2.1.0.jar");
+
+        // Unpack the jar file into a class hierarchy
+        try (JarInputStream jarInputStream = new JarInputStream(Files.newInputStream(pathToJar))) {
+            for (JarEntry entry = jarInputStream.getNextJarEntry();
+                    entry != null;
+                    entry = jarInputStream.getNextJarEntry()) {
+                if (entry.isDirectory()) {
+                    Files.createDirectories(tempDir.resolve(entry.getName()));
+                } else {
+                    Files.copy(jarInputStream, tempDir.resolve(entry.getName()));
+                }
+            }
+        }
+
+        JarTypeSolver jarTypeSolver = new JarTypeSolver(tempDir);
+        assertEquals(
+                true,
+                jarTypeSolver
+                        .tryToSolveType("com.github.javaparser.SourcesHelper")
+                        .isSolved());
+        assertEquals(
+                false, jarTypeSolver.tryToSolveType("com.github.javaparser.Foo").isSolved());
+    }
+
     @Test
     void dependenciesBetweenJarsNotTriggeringReferences() throws IOException {
         Path pathToJar1 = adaptPath("src/test/resources/jar1.jar");
diff --git a/javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/resolution/typesolvers/ReflectionTypeSolverTest.java b/javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/resolution/typesolvers/ReflectionTypeSolverTest.java
index 1f8f2f9304..b67fbcb204 100644
--- a/javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/resolution/typesolvers/ReflectionTypeSolverTest.java
+++ b/javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/resolution/typesolvers/ReflectionTypeSolverTest.java
@@ -85,4 +85,28 @@ public void visit(ObjectCreationExpr exp, Object arg) {
                         },
                         null));
     }
+
+    @Test
+    void testFilteringAll() {
+        ReflectionTypeSolver reflectionTypeSolver = new ReflectionTypeSolver(false);
+        assertEquals(true, reflectionTypeSolver.hasType("java.lang.Object"));
+        assertEquals(true, reflectionTypeSolver.hasType("org.xml.sax.Parser"));
+        assertEquals(true, reflectionTypeSolver.hasType(this.getClass().getCanonicalName()));
+    }
+
+    @Test
+    void testFilteringJRE() {
+        ReflectionTypeSolver reflectionTypeSolver = new ReflectionTypeSolver(true);
+        assertEquals(true, reflectionTypeSolver.hasType("java.lang.Object"));
+        assertEquals(false, reflectionTypeSolver.hasType("org.xml.sax.Parser"));
+        assertEquals(false, reflectionTypeSolver.hasType(this.getClass().getCanonicalName()));
+    }
+
+    @Test
+    void testFilteringJCL() {
+        ReflectionTypeSolver reflectionTypeSolver = new ReflectionTypeSolver(ReflectionTypeSolver.JCL_ONLY);
+        assertEquals(true, reflectionTypeSolver.hasType("java.lang.Object"));
+        assertEquals(true, reflectionTypeSolver.hasType("org.xml.sax.Parser"));
+        assertEquals(false, reflectionTypeSolver.hasType(this.getClass().getCanonicalName()));
+    }
 }


Reviews:
user: login: jlerbsc
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T16:52:35Z
user: login: jlerbsc
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T16:57:34Z
user: login: johanneskloos
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T09:46:17Z
user: login: johanneskloos
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T09:49:56Z

Review comments:
diff_hunk: @@ -196,34 +199,78 @@ private void addPathToJar(String pathToJar) throws IOException {
      * When we create a new {@link JarTypeSolver} we should store the list of
      * solvable types.
      *
-     * @param pathToJar The path to the jar file.
+     * @param pathToJarOrClassFileHierarchy The path to the jar file or .class file hierarchy.
      *
-     * @throws IOException If an I/O error occurs while reading the JarFile.
+     * @throws IOException If an I/O error occurs while reading the JarFile or .class file hierarchy.
      */
-    private void registerKnownClassesFor(String pathToJar) throws IOException {
-        try (JarFile jarFile = new JarFile(pathToJar)) {
+    private void registerKnownClassesFor(String pathToJarOrClassFileHierarchy) throws IOException {
+        Path jarOrClassFileHierarchy = Paths.get(pathToJarOrClassFileHierarchy);
+        if (Files.isDirectory(jarOrClassFileHierarchy)) {
+            try (Stream<Path> classFiles = Files.walk(jarOrClassFileHierarchy)) {
+                classFiles
+                        .filter(path -> Files.isRegularFile(path)
+                                && path.getFileName().toString().endsWith(CLASS_EXTENSION))
+                        .forEach(pathToClassFile -> {
+                            // We can't just use path.toString here, since path seperators are OS-dependent.
+                            String packagePrefix = convertPathToPackagePrefix(jarOrClassFileHierarchy, pathToClassFile);
+                            String classFileName = pathToClassFile.getFileName().toString();
+                            String classNameWithDollars =
+                                    classFileName.substring(0, classFileName.length() - CLASS_EXTENSION.length());
+                            String classPoolName = packagePrefix + classNameWithDollars;
+                            String qualifiedName = packagePrefix + classNameWithDollars.replace('$', '.');
 
-            Enumeration<JarEntry> jarEntries = jarFile.entries();
-            while (jarEntries.hasMoreElements()) {
+                            // If the qualified name is the same as the class pool name we don't need to duplicate store
+                            // two
+                            // different String instances. Let's reuse the same.
+                            if (qualifiedName.equals(classPoolName)) {
+                                knownClasses.put(qualifiedName, qualifiedName);
+                            } else {
+                                knownClasses.put(qualifiedName, classPoolName);
+                            }
+                        });
+            }
+        } else if (Files.isRegularFile(jarOrClassFileHierarchy)) {
+            try (JarFile jarFile = new JarFile(pathToJarOrClassFileHierarchy)) {
+
+                Enumeration<JarEntry> jarEntries = jarFile.entries();
+                while (jarEntries.hasMoreElements()) {
 
-                JarEntry entry = jarEntries.nextElement();
-                // Check if the entry is a .class file
-                if (!entry.isDirectory() && entry.getName().endsWith(CLASS_EXTENSION)) {
-                    String qualifiedName = convertEntryPathToClassName(entry.getName());
-                    String classPoolName = convertEntryPathToClassPoolName(entry.getName());
+                    JarEntry entry = jarEntries.nextElement();
+                    // Check if the entry is a .class file
+                    if (!entry.isDirectory() && entry.getName().endsWith(CLASS_EXTENSION)) {
+                        String qualifiedName = convertEntryPathToClassName(entry.getName());
+                        String classPoolName = convertEntryPathToClassPoolName(entry.getName());
 
-                    // If the qualified name is the same as the class pool name we don't need to duplicate store two
-                    // different String instances. Let's reuse the same.
-                    if (qualifiedName.equals(classPoolName)) {
-                        knownClasses.put(qualifiedName, qualifiedName);
-                    } else {
-                        knownClasses.put(qualifiedName, classPoolName);
+                        // If the qualified name is the same as the class pool name we don't need to duplicate store two
+                        // different String instances. Let's reuse the same.
+                        if (qualifiedName.equals(classPoolName)) {
+                            knownClasses.put(qualifiedName, qualifiedName);
+                        } else {
+                            knownClasses.put(qualifiedName, classPoolName);
+                        }
                     }
                 }
             }
         }
     }
 
+    /**
+     * Given a apth to class file inside a class file hierarchy, extract the package name from the path.
path: javaparser-symbol-solver-core/src/main/java/com/github/javaparser/symbolsolver/resolution/typesolvers/JarTypeSolver.java
user: login: jlerbsc
body: Given a path...
created_at: 2025-04-08T16:52:34Z
author_association: COLLABORATOR
diff_hunk: @@ -78,6 +81,34 @@ void initial() throws IOException {
         assertEquals(false, jarTypeSolver.tryToSolveType("Foo").isSolved());
     }
 
+    @Test
+    void classFileHierarchy() throws IOException {
+        Path pathToJar = adaptPath("src/test/resources/javaparser-core-2.1.0.jar");
+
+        // Unpack the jar file into a class hierarchy
+        Path tempDir = Files.createTempDirectory("jp");
path: javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/resolution/typesolvers/JarTypeSolverTest.java
user: login: jlerbsc
body: Will this directory be deleted?
created_at: 2025-04-08T16:57:33Z
author_association: COLLABORATOR
diff_hunk: @@ -196,34 +199,78 @@ private void addPathToJar(String pathToJar) throws IOException {
      * When we create a new {@link JarTypeSolver} we should store the list of
      * solvable types.
      *
-     * @param pathToJar The path to the jar file.
+     * @param pathToJarOrClassFileHierarchy The path to the jar file or .class file hierarchy.
      *
-     * @throws IOException If an I/O error occurs while reading the JarFile.
+     * @throws IOException If an I/O error occurs while reading the JarFile or .class file hierarchy.
      */
-    private void registerKnownClassesFor(String pathToJar) throws IOException {
-        try (JarFile jarFile = new JarFile(pathToJar)) {
+    private void registerKnownClassesFor(String pathToJarOrClassFileHierarchy) throws IOException {
+        Path jarOrClassFileHierarchy = Paths.get(pathToJarOrClassFileHierarchy);
+        if (Files.isDirectory(jarOrClassFileHierarchy)) {
+            try (Stream<Path> classFiles = Files.walk(jarOrClassFileHierarchy)) {
+                classFiles
+                        .filter(path -> Files.isRegularFile(path)
+                                && path.getFileName().toString().endsWith(CLASS_EXTENSION))
+                        .forEach(pathToClassFile -> {
+                            // We can't just use path.toString here, since path seperators are OS-dependent.
+                            String packagePrefix = convertPathToPackagePrefix(jarOrClassFileHierarchy, pathToClassFile);
+                            String classFileName = pathToClassFile.getFileName().toString();
+                            String classNameWithDollars =
+                                    classFileName.substring(0, classFileName.length() - CLASS_EXTENSION.length());
+                            String classPoolName = packagePrefix + classNameWithDollars;
+                            String qualifiedName = packagePrefix + classNameWithDollars.replace('$', '.');
 
-            Enumeration<JarEntry> jarEntries = jarFile.entries();
-            while (jarEntries.hasMoreElements()) {
+                            // If the qualified name is the same as the class pool name we don't need to duplicate store
+                            // two
+                            // different String instances. Let's reuse the same.
+                            if (qualifiedName.equals(classPoolName)) {
+                                knownClasses.put(qualifiedName, qualifiedName);
+                            } else {
+                                knownClasses.put(qualifiedName, classPoolName);
+                            }
+                        });
+            }
+        } else if (Files.isRegularFile(jarOrClassFileHierarchy)) {
+            try (JarFile jarFile = new JarFile(pathToJarOrClassFileHierarchy)) {
+
+                Enumeration<JarEntry> jarEntries = jarFile.entries();
+                while (jarEntries.hasMoreElements()) {
 
-                JarEntry entry = jarEntries.nextElement();
-                // Check if the entry is a .class file
-                if (!entry.isDirectory() && entry.getName().endsWith(CLASS_EXTENSION)) {
-                    String qualifiedName = convertEntryPathToClassName(entry.getName());
-                    String classPoolName = convertEntryPathToClassPoolName(entry.getName());
+                    JarEntry entry = jarEntries.nextElement();
+                    // Check if the entry is a .class file
+                    if (!entry.isDirectory() && entry.getName().endsWith(CLASS_EXTENSION)) {
+                        String qualifiedName = convertEntryPathToClassName(entry.getName());
+                        String classPoolName = convertEntryPathToClassPoolName(entry.getName());
 
-                    // If the qualified name is the same as the class pool name we don't need to duplicate store two
-                    // different String instances. Let's reuse the same.
-                    if (qualifiedName.equals(classPoolName)) {
-                        knownClasses.put(qualifiedName, qualifiedName);
-                    } else {
-                        knownClasses.put(qualifiedName, classPoolName);
+                        // If the qualified name is the same as the class pool name we don't need to duplicate store two
+                        // different String instances. Let's reuse the same.
+                        if (qualifiedName.equals(classPoolName)) {
+                            knownClasses.put(qualifiedName, qualifiedName);
+                        } else {
+                            knownClasses.put(qualifiedName, classPoolName);
+                        }
                     }
                 }
             }
         }
     }
 
+    /**
+     * Given a apth to class file inside a class file hierarchy, extract the package name from the path.
path: javaparser-symbol-solver-core/src/main/java/com/github/javaparser/symbolsolver/resolution/typesolvers/JarTypeSolver.java
user: login: johanneskloos
body: Fixed.
created_at: 2025-04-09T09:46:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,6 +81,34 @@ void initial() throws IOException {
         assertEquals(false, jarTypeSolver.tryToSolveType("Foo").isSolved());
     }
 
+    @Test
+    void classFileHierarchy() throws IOException {
+        Path pathToJar = adaptPath("src/test/resources/javaparser-core-2.1.0.jar");
+
+        // Unpack the jar file into a class hierarchy
+        Path tempDir = Files.createTempDirectory("jp");
path: javaparser-symbol-solver-testing/src/test/java/com/github/javaparser/symbolsolver/resolution/typesolvers/JarTypeSolverTest.java
user: login: johanneskloos
body: Switched to using `@TempDir`.
created_at: 2025-04-09T09:49:56Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-08T16:52:52Z
body: ## [Codecov](https://app.codecov.io/gh/javaparser/javaparser/pull/4716?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) Report
Attention: Patch coverage is `96.07843%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 58.235%. Comparing base [(`7a24d4a`)](https://app.codecov.io/gh/javaparser/javaparser/commit/7a24d4a2c7e841769e88e59c0a7e6b86bc657f92?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) to head [(`c40cd37`)](https://app.codecov.io/gh/javaparser/javaparser/commit/c40cd3725f649f9862bb2a8f25bed435d0d11716?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser).
> Report is 5 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/javaparser/javaparser/pull/4716?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | Patch % | Lines |
|---|---|---|
| [...olsolver/resolution/typesolvers/JarTypeSolver.java](https://app.codecov.io/gh/javaparser/javaparser/pull/4716?src=pr&el=tree&filepath=javaparser-symbol-solver-core%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgithub%2Fjavaparser%2Fsymbolsolver%2Fresolution%2Ftypesolvers%2FJarTypeSolver.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser#diff-amF2YXBhcnNlci1zeW1ib2wtc29sdmVyLWNvcmUvc3JjL21haW4vamF2YS9jb20vZ2l0aHViL2phdmFwYXJzZXIvc3ltYm9sc29sdmVyL3Jlc29sdXRpb24vdHlwZXNvbHZlcnMvSmFyVHlwZVNvbHZlci5qYXZh) | 95.238% | [0 Missing and 2 partials :warning: ](https://app.codecov.io/gh/javaparser/javaparser/pull/4716?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) |

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/graphs/tree.svg?width=650&height=150&src=pr&token=L63ZT4cdpg&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser)](https://app.codecov.io/gh/javaparser/javaparser/pull/4716?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser)

```diff
@@             Coverage Diff             @@
##              master     #4716   +/-   ##
===========================================
  Coverage     58.235%   58.235%           
  Complexity      2511      2511           
===========================================
  Files            671       671           
  Lines          38767     38767           
  Branches        7034      7034           
===========================================
  Hits           22576     22576           
  Misses         13295     13295           
  Partials        2896      2896           
```

| [Flag](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | Coverage Δ | |
|---|---|---|
| [AlsoSlowTests](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | `58.235% <96.078%> (ø)` | |
| [javaparser-core](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | `58.235% <96.078%> (ø)` | |
| [javaparser-symbol-solver](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | `58.235% <96.078%> (ø)` | |
| [jdk-10](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | `57.786% <96.078%> (ø)` | |
| [jdk-11](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | `57.785% <96.000%> (ø)` | |
| [jdk-12](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | `57.785% <96.000%> (ø)` | |
| [jdk-13](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | `57.785% <96.000%> (ø)` | |
| [jdk-14](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | `58.032% <96.000%> (ø)` | |
| [jdk-15](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | `58.032% <96.000%> (ø)` | |
| [jdk-16](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | `58.007% <96.000%> (ø)` | |
| [jdk-17](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | `58.161% <96.000%> (ø)` | |
| [jdk-18](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | `58.161% <96.000%> (ø)` | |
| [jdk-8](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | `57.787% <96.078%> (ø)` | |
| [jdk-9](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | `57.783% <96.078%> (ø)` | |
| [macos-latest](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | `58.227% <96.078%> (ø)` | |
| [ubuntu-latest](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | `58.222% <96.078%> (ø)` | |
| [windows-latest](https://app.codecov.io/gh/javaparser/javaparser/pull/4716/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | `58.217% <96.078%> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files with missing lines](https://app.codecov.io/gh/javaparser/javaparser/pull/4716?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser) | Coverage Δ | |
|---|---|---|
| [...r/resolution/typesolvers/ReflectionTypeSolver.java](https://app.codecov.io/gh/javaparser/javaparser/pull/4716?src=pr&el=tree&filepath=javaparser-symbol-solver-core%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgithub%2Fjavaparser%2Fsymbolsolver%2Fresolution%2Ftypesolvers%2FReflectionTypeSolver.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser#diff-amF2YXBhcnNlci1zeW1ib2wtc29sdmVyLWNvcmUvc3JjL21haW4vamF2YS9jb20vZ2l0aHViL2phdmFwYXJzZXIvc3ltYm9sc29sdmVyL3Jlc29sdXRpb24vdHlwZXNvbHZlcnMvUmVmbGVjdGlvblR5cGVTb2x2ZXIuamF2YQ==) | `100.000% <100.000%> (ø)` | |
| [...olsolver/resolution/typesolvers/JarTypeSolver.java](https://app.codecov.io/gh/javaparser/javaparser/pull/4716?src=pr&el=tree&filepath=javaparser-symbol-solver-core%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgithub%2Fjavaparser%2Fsymbolsolver%2Fresolution%2Ftypesolvers%2FJarTypeSolver.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser#diff-amF2YXBhcnNlci1zeW1ib2wtc29sdmVyLWNvcmUvc3JjL21haW4vamF2YS9jb20vZ2l0aHViL2phdmFwYXJzZXIvc3ltYm9sc29sdmVyL3Jlc29sdXRpb24vdHlwZXNvbHZlcnMvSmFyVHlwZVNvbHZlci5qYXZh) | `90.625% <95.238%> (ø)` | |

------

[Continue to review full report in Codecov by Sentry](https://app.codecov.io/gh/javaparser/javaparser/pull/4716?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://app.codecov.io/gh/javaparser/javaparser/pull/4716?dropdown=coverage&src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser). Last update [8e0eff1...c40cd37](https://app.codecov.io/gh/javaparser/javaparser/pull/4716?dropdown=coverage&src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javaparser).

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: jlerbsc
created_at: 2025-04-08T17:14:07Z
body: Thanks for this interesting PR.
author_association: COLLABORATOR
user: login: johanneskloos
created_at: 2025-04-09T09:52:21Z
body: @jlerbsc I have addresed your review comments; would you like me to rebase?
author_association: CONTRIBUTOR