• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pmd / pmd / 658

10 Jul 2026 12:22PM UTC coverage: 79.202%. Remained the same
658

push

github

web-flow
[core] refactor: AnalysisCache based on Path (#6841)

* [core] refactor: AnalysisCache based on Path

And not anymore based on URLs.
Moved out dependency to (URL)ClassLoader.
Added new utility AnalysisClasspathUtil to interpret with
the classpath.

* Refactor ClasspathClassLoader to reuse AnalysisClasspathUtil

* Fix unit tests (Windows...)

* Fix unit tests (Windows...) - Part 2

* Fix unit tests (Windows...) - Part 3

* Fix unit tests (Windows...) - Part 4

* Rename AnalysisClasspathUtil -> AuxClasspathUtil

* Fix compile errors

19192 of 25165 branches covered (76.26%)

Branch coverage included in aggregate %.

73 of 91 new or added lines in 10 files covered. (80.22%)

41622 of 51618 relevant lines covered (80.63%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

45.98
/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/ClasspathClassLoader.java
1
/*
2
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3
 */
4

5
package net.sourceforge.pmd.internal.util;
6

7
import java.io.File;
8
import java.io.IOException;
9
import java.io.InputStream;
10
import java.io.UncheckedIOException;
11
import java.net.MalformedURLException;
12
import java.net.URI;
13
import java.net.URL;
14
import java.net.URLClassLoader;
15
import java.nio.file.FileSystem;
16
import java.nio.file.FileSystems;
17
import java.nio.file.Files;
18
import java.nio.file.Path;
19
import java.nio.file.Paths;
20
import java.util.ArrayList;
21
import java.util.Collections;
22
import java.util.Enumeration;
23
import java.util.HashMap;
24
import java.util.List;
25
import java.util.Map;
26
import java.util.Objects;
27
import java.util.Set;
28
import java.util.stream.Collectors;
29
import java.util.stream.Stream;
30

31
import org.apache.commons.lang3.StringUtils;
32
import org.checkerframework.checker.nullness.qual.Nullable;
33
import org.objectweb.asm.ClassReader;
34
import org.objectweb.asm.ClassVisitor;
35
import org.objectweb.asm.ModuleVisitor;
36
import org.objectweb.asm.Opcodes;
37
import org.slf4j.Logger;
38
import org.slf4j.LoggerFactory;
39

40
import net.sourceforge.pmd.util.internal.AuxClasspathUtil;
41

42
/**
43
 * Create a ClassLoader which loads classes using a CLASSPATH like String. If
44
 * the String looks like a URL to a file (e.g. starts with <code>file://</code>)
45
 * the file will be read with each line representing an path on the classpath.
46
 *
47
 * @author Edwin Chan
48
 */
49
public class ClasspathClassLoader extends URLClassLoader {
50

51
    private static final Logger LOG = LoggerFactory.getLogger(ClasspathClassLoader.class);
1✔
52

53
    String javaHome;
54

55
    private FileSystem fileSystem;
56
    private Map<String, Set<String>> packagesDirsToModules;
57

58
    static {
59
        registerAsParallelCapable();
1✔
60

61
        // Disable caching for jar files to prevent issues like #4899
62
        try {
63
            // Uses a pseudo URL to be able to call URLConnection#setDefaultUseCaches
64
            // with Java9+ there is a static method for that per protocol:
65
            // https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/net/URLConnection.html#setDefaultUseCaches(java.lang.String,boolean)
66
            URI.create("jar:file:file.jar!/").toURL().openConnection().setDefaultUseCaches(false);
1✔
67
        } catch (IOException e) {
×
68
            throw new RuntimeException(e);
×
69
        }
1✔
70
    }
1✔
71

72
    public ClasspathClassLoader(List<File> files, ClassLoader parent) throws IOException {
73
        super(new URL[0], parent);
×
NEW
74
        List<Path> paths = files.stream().map(File::toPath).collect(Collectors.toList());
×
NEW
75
        for (URL url : pathToUrl(paths)) {
×
76
            addURL(url);
×
77
        }
×
78
    }
×
79

80
    public ClasspathClassLoader(String classpath, ClassLoader parent) throws IOException {
81
        super(new URL[0], parent);
1✔
82
        List<Path> paths = AuxClasspathUtil.expandClasspath(classpath);
1✔
83
        for (URL url : pathToUrl(paths)) {
1✔
84
            addURL(url);
1✔
85
        }
1✔
86
    }
1✔
87

88
    private List<URL> pathToUrl(List<Path> files) throws IOException {
89
        List<URL> urlList = new ArrayList<>();
1✔
90
        for (Path f : files) {
1✔
91
            urlList.add(createURLFromPath(f.toAbsolutePath().toString()));
1✔
92
        }
1✔
93
        return urlList;
1✔
94
    }
95

96
    private URL createURLFromPath(String path) throws MalformedURLException {
97
        Path filePath = Paths.get(path).toAbsolutePath();
1✔
98
        if (filePath.endsWith(Paths.get("lib", "jrt-fs.jar"))) {
1!
99
            initializeJrtFilesystem(filePath);
×
100
            // don't add jrt-fs.jar to the normal aux classpath
101
            return null;
×
102
        }
103

104
        return filePath.toUri().normalize().toURL();
1✔
105
    }
106

107
    /**
108
     * Initializes a Java Runtime Filesystem that will be used to load class files.
109
     * This allows end users to provide in the aux classpath another Java Runtime version
110
     * than the one used for executing PMD.
111
     *
112
     * @param filePath path to the file "lib/jrt-fs.jar" inside the java installation directory.
113
     * @see <a href="https://openjdk.org/jeps/220">JEP 220: Modular Run-Time Images</a>
114
     */
115
    private void initializeJrtFilesystem(Path filePath) {
116
        try {
117
            LOG.debug("Detected Java Runtime Filesystem Provider in {}", filePath);
×
118

119
            if (fileSystem != null) {
×
120
                throw new IllegalStateException("There is already a jrt filesystem. Do you have multiple jrt-fs.jar files on the classpath?");
×
121
            }
122

123
            if (filePath.getNameCount() < 2) {
×
124
                throw new IllegalArgumentException("Can't determine java home from " + filePath + " - please provide a complete path.");
×
125
            }
126

127
            try (URLClassLoader loader = new URLClassLoader(new URL[] { filePath.toUri().toURL() })) {
×
128
                Map<String, String> env = new HashMap<>();
×
129
                // note: providing java.home here is crucial, so that the correct runtime image is loaded.
130
                // the class loader is only used to provide an implementation of JrtFileSystemProvider, if the current
131
                // Java runtime doesn't provide one (e.g. if running in Java 8).
132
                javaHome = filePath.getParent().getParent().toString();
×
133
                env.put("java.home", javaHome);
×
134
                LOG.debug("Creating jrt-fs with env {}", env);
×
135
                fileSystem = FileSystems.newFileSystem(URI.create("jrt:/"), env, loader);
×
136
            }
137

138
            packagesDirsToModules = new HashMap<>();
×
139
            Path packages = fileSystem.getPath("packages");
×
140
            try (Stream<Path> packagesStream = Files.list(packages)) {
×
141
                packagesStream.forEach(p -> {
×
142
                    String packageName = p.getFileName().toString().replace('.', '/');
×
143
                    try (Stream<Path> modulesStream = Files.list(p)) {
×
144
                        Set<String> modules = modulesStream
×
145
                                .map(Path::getFileName)
×
146
                                .map(Path::toString)
×
147
                                .collect(Collectors.toSet());
×
148
                        packagesDirsToModules.put(packageName, modules);
×
149
                    } catch (IOException e) {
×
150
                        throw new UncheckedIOException(e);
×
151
                    }
×
152
                });
×
153
            }
154
        } catch (IOException e) {
×
155
            throw new UncheckedIOException(e);
×
156
        }
×
157
    }
×
158

159
    @Override
160
    public String toString() {
161
        return getClass().getSimpleName()
×
162
            + "[["
163
            + StringUtils.join(getURLs(), ":")
×
164
            + "] jrt-fs: " + javaHome + " parent: " + getParent() + ']';
×
165
    }
166

167
    private static final String MODULE_INFO_SUFFIX = "module-info.class";
168
    private static final String MODULE_INFO_SUFFIX_SLASH = "/" + MODULE_INFO_SUFFIX;
169
    // this is lazily initialized on first query of a module-info.class
170
    private Map<String, URL> moduleNameToModuleInfoUrls;
171

172
    private static @Nullable String extractModuleName(String name) {
173
        if (!name.endsWith(MODULE_INFO_SUFFIX_SLASH)) {
1✔
174
            return null;
1✔
175
        }
176
        return name.substring(0, name.length() - MODULE_INFO_SUFFIX_SLASH.length());
1✔
177
    }
178

179
    @Override
180
    public InputStream getResourceAsStream(String name) {
181
        // always first search in jrt-fs, if available
182
        // note: we can't override just getResource(String) and return a jrt:/-URL, because the URL itself
183
        // won't be connected to the correct JrtFileSystem and would just load using the system classloader.
184
        if (fileSystem != null) {
1!
185
            String moduleName = extractModuleName(name);
×
186
            if (moduleName != null) {
×
187
                LOG.trace("Trying to load module-info.class for module {} in jrt-fs", moduleName);
×
188
                Path candidate = fileSystem.getPath("modules", moduleName, MODULE_INFO_SUFFIX);
×
189
                if (Files.exists(candidate)) {
×
190
                    return newInputStreamFromJrtFilesystem(candidate);
×
191
                }
192
            }
193

194
            int lastSlash = name.lastIndexOf('/');
×
195
            String packageName = name.substring(0, Math.max(lastSlash, 0));
×
196
            Set<String> moduleNames = packagesDirsToModules.get(packageName);
×
197
            if (moduleNames != null) {
×
198
                LOG.trace("Trying to find {} in jrt-fs with packageName={} and modules={}",
×
199
                        name, packageName, moduleNames);
200

201
                for (String moduleCandidate : moduleNames) {
×
202
                    Path candidate = fileSystem.getPath("modules", moduleCandidate, name);
×
203
                    if (Files.exists(candidate)) {
×
204
                        return newInputStreamFromJrtFilesystem(candidate);
×
205
                    }
206
                }
×
207
            }
208
        }
209

210
        // search in the other jars of the aux classpath.
211
        // this will call this.getResource, which will do a child-first search, see below.
212
        return super.getResourceAsStream(name);
1✔
213
    }
214

215
    private static InputStream newInputStreamFromJrtFilesystem(Path path) {
216
        LOG.trace("Found {}", path);
×
217
        try {
218
            // Note: The input streams from JrtFileSystem are ByteArrayInputStreams and do not
219
            // need to be closed - we don't need to track these. The filesystem itself needs to be closed at the end.
220
            // See https://github.com/openjdk/jdk/blob/970cd202049f592946f9c1004ea92dbd58abf6fb/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystem.java#L334
221
            return Files.newInputStream(path);
×
222
        } catch (IOException e) {
×
223
            throw new UncheckedIOException(e);
×
224
        }
225
    }
226

227
    private static class ModuleNameExtractor extends ClassVisitor {
228
        private String moduleName;
229

230
        protected ModuleNameExtractor() {
231
            super(Opcodes.ASM9);
1✔
232
        }
1✔
233

234
        @Override
235
        public ModuleVisitor visitModule(String name, int access, String version) {
236
            moduleName = name;
1✔
237
            return null;
1✔
238
        }
239

240
        public String getModuleName() {
241
            return moduleName;
1✔
242
        }
243
    }
244

245
    private void collectAllModules() {
246
        if (moduleNameToModuleInfoUrls != null) {
1✔
247
            return;
1✔
248
        }
249

250
        Map<String, URL> allModules = new HashMap<>();
1✔
251
        try {
252
            Enumeration<URL> moduleInfoUrls = findResources(MODULE_INFO_SUFFIX);
1✔
253
            collectModules(allModules, moduleInfoUrls);
1✔
254

255
            // also search in parents
256
            moduleInfoUrls = getParent().getResources(MODULE_INFO_SUFFIX);
1✔
257
            collectModules(allModules, moduleInfoUrls);
1✔
258

259
            LOG.debug("Found {} modules on auxclasspath", allModules.size());
1✔
260

261
            moduleNameToModuleInfoUrls = Collections.unmodifiableMap(allModules);
1✔
262
        } catch (IOException e) {
×
263
            throw new RuntimeException(e);
×
264
        }
1✔
265
    }
1✔
266

267
    private void collectModules(Map<String, URL> allModules, Enumeration<URL> moduleInfoUrls) throws IOException {
268
        while (moduleInfoUrls.hasMoreElements()) {
1✔
269
            URL url = moduleInfoUrls.nextElement();
1✔
270

271
            ModuleNameExtractor finder = new ModuleNameExtractor();
1✔
272
            try (InputStream inputStream = url.openStream()) {
1✔
273
                ClassReader classReader = new ClassReader(inputStream);
1✔
274
                classReader.accept(finder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
1✔
275
            }
276
            allModules.putIfAbsent(finder.getModuleName(), url);
1✔
277
        }
1✔
278
    }
1✔
279

280
    @Override
281
    public URL getResource(String name) {
282
        // Override to make it child-first. This is the method used by
283
        // pmd-java's type resolution to fetch classes, instead of loadClass.
284
        Objects.requireNonNull(name);
1✔
285

286
        String moduleName = extractModuleName(name);
1✔
287
        if (moduleName != null) {
1✔
288
            collectAllModules();
1✔
289
            assert moduleNameToModuleInfoUrls != null : "Modules should have been detected by collectAllModules()";
1!
290
            return moduleNameToModuleInfoUrls.get(moduleName);
1✔
291
        }
292

293
        URL url = findResource(name);
1✔
294
        if (url == null) {
1✔
295
            // note this will actually call back into this.findResource, but
296
            // we can't avoid this as the super implementation uses JDK internal
297
            // stuff that we can't copy down here.
298
            return super.getResource(name);
1✔
299
        }
300
        return url;
1✔
301
    }
302

303
    @Override
304
    protected Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
305
        throw new IllegalStateException("This class loader shouldn't be used to load classes");
×
306
    }
307

308
    @Override
309
    public void close() throws IOException {
310
        if (fileSystem != null) {
1!
311
            fileSystem.close();
×
312
            // jrt created an own classloader to load the JrtFileSystemProvider class out of the
313
            // jrt-fs.jar. This needs to be closed manually.
314
            ClassLoader classLoader = fileSystem.getClass().getClassLoader();
×
315
            if (classLoader instanceof URLClassLoader) {
×
316
                ((URLClassLoader) classLoader).close();
×
317
            }
318
            packagesDirsToModules = null;
×
319
            fileSystem = null;
×
320
        }
321
        super.close();
1✔
322
    }
1✔
323
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc