diff --git a/docs/pages/pmd/languages/java.md b/docs/pages/pmd/languages/java.md index 2a55d9f747..ef795f2d4b 100644 --- a/docs/pages/pmd/languages/java.md +++ b/docs/pages/pmd/languages/java.md @@ -63,7 +63,7 @@ The semantic analysis roughly works like so: 3. The last pass resolves the types of expressions, which performs overload resolution on method calls, and type inference. TODO describe -* why we need auxclasspath +* why we need auxclasspath, and how to put the java classes onto the auxclasspath (jre/lib/rt.jar or lib/jrt-fs.jar). * how disambiguation can fail ## Type and symbol APIs diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md index 11da4a32ba..0f2c592ae6 100644 --- a/docs/pages/release_notes.md +++ b/docs/pages/release_notes.md @@ -123,6 +123,7 @@ in the Migration Guide. * groovy * [#4726](https://github.com/pmd/pmd/pull/4726): \[groovy] Support Groovy to 3 and 4 and CPD suppressions * java + * [#4628](https://github.com/pmd/pmd/pull/4628): \[java] Support loading classes from java runtime images * [#4753](https://github.com/pmd/pmd/issues/4753): \[java] PMD crashes while using generics and wildcards * java-codestyle * [#2847](https://github.com/pmd/pmd/issues/2847): \[java] New Rule: Use Explicit Types @@ -645,6 +646,7 @@ Language specific fixes: * [#4401](https://github.com/pmd/pmd/issues/4401): \[java] PMD 7 fails to build under Java 19 * [#4405](https://github.com/pmd/pmd/issues/4405): \[java] Processing error with ArrayIndexOutOfBoundsException * [#4583](https://github.com/pmd/pmd/issues/4583): \[java] Support JDK 21 (LTS) + * [#4628](https://github.com/pmd/pmd/pull/4628): \[java] Support loading classes from java runtime images * [#4753](https://github.com/pmd/pmd/issues/4753): \[java] PMD crashes while using generics and wildcards * java-bestpractices * [#342](https://github.com/pmd/pmd/issues/342): \[java] AccessorMethodGeneration: Name clash with another public field not properly handled diff --git a/pmd-cli/src/main/java/net/sourceforge/pmd/cli/PmdCli.java b/pmd-cli/src/main/java/net/sourceforge/pmd/cli/PmdCli.java index 4fb3877359..3e630bd9dc 100644 --- a/pmd-cli/src/main/java/net/sourceforge/pmd/cli/PmdCli.java +++ b/pmd-cli/src/main/java/net/sourceforge/pmd/cli/PmdCli.java @@ -13,6 +13,11 @@ public final class PmdCli { private PmdCli() { } public static void main(String[] args) { + // See https://github.com/remkop/picocli/blob/main/RELEASE-NOTES.md#-picocli-470 + // and https://picocli.info/#_closures_in_annotations + // we don't use this feature. Disabling it avoids leaving the groovy jar open + // caused by Class.forName("groovy.lang.Closure") + System.setProperty("picocli.disable.closures", "true"); final CommandLine cli = new CommandLine(new PmdRootCommand()) .setCaseInsensitiveEnumValuesAllowed(true); diff --git a/pmd-cli/src/main/java/net/sourceforge/pmd/cli/commands/internal/PmdCommand.java b/pmd-cli/src/main/java/net/sourceforge/pmd/cli/commands/internal/PmdCommand.java index a4c7e016dd..29758b74be 100644 --- a/pmd-cli/src/main/java/net/sourceforge/pmd/cli/commands/internal/PmdCommand.java +++ b/pmd-cli/src/main/java/net/sourceforge/pmd/cli/commands/internal/PmdCommand.java @@ -323,7 +323,8 @@ public class PmdCommand extends AbstractAnalysisPmdSubcommand return CliExitCode.ERROR; } - LOG.debug("Current classpath:\n{}", System.getProperty("java.class.path")); + LOG.debug("Runtime classpath:\n{}", System.getProperty("java.class.path")); + LOG.debug("Aux classpath: {}", configuration.getClassLoader()); if (showProgressBar) { if (reportFile == null) { diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/ClasspathClassLoader.java b/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/ClasspathClassLoader.java index 024542c01f..2d5da86589 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/ClasspathClassLoader.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/internal/util/ClasspathClassLoader.java @@ -7,14 +7,27 @@ package net.sourceforge.pmd.internal.util; import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; +import java.io.UncheckedIOException; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.StringTokenizer; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -33,29 +46,38 @@ public class ClasspathClassLoader extends URLClassLoader { private static final Logger LOG = LoggerFactory.getLogger(ClasspathClassLoader.class); + String javaHome; + + private FileSystem fileSystem; + private Map> packagesDirsToModules; + static { registerAsParallelCapable(); } public ClasspathClassLoader(List files, ClassLoader parent) throws IOException { - super(fileToURL(files), parent); + super(new URL[0], parent); + for (URL url : fileToURL(files)) { + addURL(url); + } } public ClasspathClassLoader(String classpath, ClassLoader parent) throws IOException { - super(initURLs(classpath), parent); - } - - private static URL[] fileToURL(List files) throws IOException { - - List urlList = new ArrayList<>(); - - for (File f : files) { - urlList.add(f.toURI().toURL()); + super(new URL[0], parent); + for (URL url : initURLs(classpath)) { + addURL(url); } - return urlList.toArray(new URL[0]); } - private static URL[] initURLs(String classpath) { + private List fileToURL(List files) throws IOException { + List urlList = new ArrayList<>(); + for (File f : files) { + urlList.add(createURLFromPath(f.getAbsolutePath())); + } + return urlList; + } + + private List initURLs(String classpath) { AssertionUtil.requireParamNotNull("classpath", classpath); final List urls = new ArrayList<>(); try { @@ -69,10 +91,10 @@ public class ClasspathClassLoader extends URLClassLoader { } catch (IOException e) { throw new IllegalArgumentException("Cannot prepend classpath " + classpath + "\n" + e.getMessage(), e); } - return urls.toArray(new URL[0]); + return urls; } - private static void addClasspathURLs(final List urls, final String classpath) throws MalformedURLException { + private void addClasspathURLs(final List urls, final String classpath) throws MalformedURLException { StringTokenizer toker = new StringTokenizer(classpath, File.pathSeparator); while (toker.hasMoreTokens()) { String token = toker.nextToken(); @@ -81,7 +103,7 @@ public class ClasspathClassLoader extends URLClassLoader { } } - private static void addFileURLs(List urls, URL fileURL) throws IOException { + private void addFileURLs(List urls, URL fileURL) throws IOException { try (BufferedReader in = new BufferedReader(new InputStreamReader(fileURL.openStream()))) { String line; while ((line = in.readLine()) != null) { @@ -95,9 +117,67 @@ public class ClasspathClassLoader extends URLClassLoader { } } - private static URL createURLFromPath(String path) throws MalformedURLException { - File file = new File(path); - return file.getAbsoluteFile().toURI().normalize().toURL(); + private URL createURLFromPath(String path) throws MalformedURLException { + Path filePath = Paths.get(path).toAbsolutePath(); + if (filePath.endsWith(Paths.get("lib", "jrt-fs.jar"))) { + initializeJrtFilesystem(filePath); + // don't add jrt-fs.jar to the normal aux classpath + return null; + } + + return filePath.toUri().normalize().toURL(); + } + + /** + * Initializes a Java Runtime Filesystem that will be used to load class files. + * This allows end users to provide in the aux classpath another Java Runtime version + * than the one used for executing PMD. + * + * @param filePath path to the file "lib/jrt-fs.jar" inside the java installation directory. + * @see JEP 220: Modular Run-Time Images + */ + private void initializeJrtFilesystem(Path filePath) { + try { + LOG.debug("Detected Java Runtime Filesystem Provider in {}", filePath); + + if (fileSystem != null) { + throw new IllegalStateException("There is already a jrt filesystem. Do you have multiple jrt-fs.jar files on the classpath?"); + } + + if (filePath.getNameCount() < 2) { + throw new IllegalArgumentException("Can't determine java home from " + filePath + " - please provide a complete path."); + } + + try (URLClassLoader loader = new URLClassLoader(new URL[] { filePath.toUri().toURL() })) { + Map env = new HashMap<>(); + // note: providing java.home here is crucial, so that the correct runtime image is loaded. + // the class loader is only used to provide an implementation of JrtFileSystemProvider, if the current + // Java runtime doesn't provide one (e.g. if running in Java 8). + javaHome = filePath.getParent().getParent().toString(); + env.put("java.home", javaHome); + LOG.debug("Creating jrt-fs with env {}", env); + fileSystem = FileSystems.newFileSystem(URI.create("jrt:/"), env, loader); + } + + packagesDirsToModules = new HashMap<>(); + Path packages = fileSystem.getPath("packages"); + try (Stream packagesStream = Files.list(packages)) { + packagesStream.forEach(p -> { + String packageName = p.getFileName().toString().replace('.', '/'); + try (Stream modulesStream = Files.list(p)) { + Set modules = modulesStream + .map(Path::getFileName) + .map(Path::toString) + .collect(Collectors.toSet()); + packagesDirsToModules.put(packageName, modules); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } } @Override @@ -105,7 +185,42 @@ public class ClasspathClassLoader extends URLClassLoader { return getClass().getSimpleName() + "[[" + StringUtils.join(getURLs(), ":") - + "] parent: " + getParent() + ']'; + + "] jrt-fs: " + javaHome + " parent: " + getParent() + ']'; + } + + @Override + public InputStream getResourceAsStream(String name) { + // always first search in jrt-fs, if available + // note: we can't override just getResource(String) and return a jrt:/-URL, because the URL itself + // won't be connected to the correct JrtFileSystem and would just load using the system classloader. + if (fileSystem != null) { + int lastSlash = name.lastIndexOf('/'); + String packageName = name.substring(0, Math.max(lastSlash, 0)); + Set moduleNames = packagesDirsToModules.get(packageName); + if (moduleNames != null) { + LOG.trace("Trying to find {} in jrt-fs with packageName={} and modules={}", + name, packageName, moduleNames); + + for (String moduleCandidate : moduleNames) { + Path candidate = fileSystem.getPath("modules", moduleCandidate, name); + if (Files.exists(candidate)) { + LOG.trace("Found {}", candidate); + try { + // Note: The input streams from JrtFileSystem are ByteArrayInputStreams and do not + // need to be closed - we don't need to track these. The filesystem itself needs to be closed at the end. + // See https://github.com/openjdk/jdk/blob/970cd202049f592946f9c1004ea92dbd58abf6fb/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystem.java#L334 + return Files.newInputStream(candidate); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + } + } + + // search in the other jars of the aux classpath. + // this will call this.getResource, which will do a child-first search, see below. + return super.getResourceAsStream(name); } @Override @@ -126,24 +241,22 @@ public class ClasspathClassLoader extends URLClassLoader { @Override protected Class loadClass(final String name, final boolean resolve) throws ClassNotFoundException { - synchronized (getClassLoadingLock(name)) { - // First, check if the class has already been loaded - Class c = findLoadedClass(name); - if (c == null) { - try { - // checking local - c = findClass(name); - } catch (final ClassNotFoundException | SecurityException e) { - // checking parent - // This call to loadClass may eventually call findClass again, in case the parent doesn't find anything. - c = super.loadClass(name, resolve); - } - } + throw new IllegalStateException("This class loader shouldn't be used to load classes"); + } - if (resolve) { - resolveClass(c); + @Override + public void close() throws IOException { + if (fileSystem != null) { + fileSystem.close(); + // jrt created an own classloader to load the JrtFileSystemProvider class out of the + // jrt-fs.jar. This needs to be closed manually. + ClassLoader classLoader = fileSystem.getClass().getClassLoader(); + if (classLoader instanceof URLClassLoader) { + ((URLClassLoader) classLoader).close(); } - return c; + packagesDirsToModules = null; + fileSystem = null; } + super.close(); } } diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/internal/util/ClasspathClassLoaderTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/internal/util/ClasspathClassLoaderTest.java new file mode 100644 index 0000000000..50103b9091 --- /dev/null +++ b/pmd-core/src/test/java/net/sourceforge/pmd/internal/util/ClasspathClassLoaderTest.java @@ -0,0 +1,125 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.internal.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ClasspathClassLoaderTest { + @TempDir + private Path tempDir; + + @Test + void loadEmptyClasspathWithParent() throws IOException { + try (ClasspathClassLoader loader = new ClasspathClassLoader("", ClasspathClassLoader.class.getClassLoader())) { + try (InputStream resource = loader.getResourceAsStream("java/lang/Object.class")) { + assertNotNull(resource); + try (DataInputStream data = new DataInputStream(resource)) { + assertClassFile(data, Integer.valueOf(System.getProperty("java.specification.version"))); + } + } + } + } + + /** + * This test case just documents the current behavior: Eventually we load + * the class files from the system class loader, even if the auxclasspath + * is essentially empty and no parent is provided. This is an unavoidable + * behavior of {@link java.lang.ClassLoader#getResource(java.lang.String)}, which will + * search the class loader built into the VM (BootLoader). + */ + @Test + void loadEmptyClasspathNoParent() throws IOException { + try (ClasspathClassLoader loader = new ClasspathClassLoader("", null)) { + try (InputStream resource = loader.getResourceAsStream("java/lang/Object.class")) { + assertNotNull(resource); + try (DataInputStream data = new DataInputStream(resource)) { + assertClassFile(data, Integer.valueOf(System.getProperty("java.specification.version"))); + } + } + } + } + + @Test + void loadFromJar() throws IOException { + final String RESOURCE_NAME = "net/sourceforge/pmd/Sample.txt"; + final String TEST_CONTENT = "Test\n"; + + Path jarPath = tempDir.resolve("custom.jar"); + try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(jarPath))) { + out.putNextEntry(new ZipEntry(RESOURCE_NAME)); + out.write(TEST_CONTENT.getBytes(StandardCharsets.UTF_8)); + } + String classpath = jarPath.toString(); + + try (ClasspathClassLoader loader = new ClasspathClassLoader(classpath, null)) { + try (InputStream in = loader.getResourceAsStream(RESOURCE_NAME)) { + assertNotNull(in); + String s = IOUtil.readToString(in, StandardCharsets.UTF_8); + assertEquals(TEST_CONTENT, s); + } + } + } + + /** + * Verifies, that we load the class files from the runtime image of the correct java home. + * This tests multiple versions, in order to avoid that the test accidentally is successful when + * testing e.g. java17 and running the build with java17. In that case, we might load java.lang.Object + * from the system classloader and not from jrt-fs.jar. + * + *

+ * This test only runs, if you have a folder ${HOME}/openjdk{javaVersion}. + *

+ */ + @ParameterizedTest + @ValueSource(ints = {11, 17, 21}) + void loadFromJava(int javaVersion) throws IOException { + Path javaHome = Paths.get(System.getProperty("user.home"), "openjdk" + javaVersion); + assumeTrue(Files.isDirectory(javaHome), "Couldn't find java" + javaVersion + " installation at " + javaHome); + + Path jrtfsPath = javaHome.resolve("lib/jrt-fs.jar"); + assertTrue(Files.isRegularFile(jrtfsPath), "java" + javaVersion + " installation is incomplete. " + jrtfsPath + " not found!"); + String classPath = jrtfsPath.toString(); + + try (ClasspathClassLoader loader = new ClasspathClassLoader(classPath, null)) { + assertEquals(javaHome.toString(), loader.javaHome); + try (InputStream stream = loader.getResourceAsStream("java/lang/Object.class")) { + assertNotNull(stream); + try (DataInputStream data = new DataInputStream(stream)) { + assertClassFile(data, javaVersion); + } + } + + // should not fail for resources without a package + assertNull(loader.getResourceAsStream("ClassInDefaultPackage.class")); + } + } + + private void assertClassFile(DataInputStream data, int javaVersion) throws IOException { + int magicNumber = data.readInt(); + assertEquals(0xcafebabe, magicNumber); + data.readUnsignedShort(); // minorVersion + int majorVersion = data.readUnsignedShort(); + assertEquals(44 + javaVersion, majorVersion); + } +} diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/internal/JavaLanguageProcessor.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/internal/JavaLanguageProcessor.java index c3729bae3c..020fe79ed0 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/internal/JavaLanguageProcessor.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/internal/JavaLanguageProcessor.java @@ -8,6 +8,8 @@ import java.util.List; import java.util.Objects; import org.checkerframework.checker.nullness.qual.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import net.sourceforge.pmd.ViolationSuppressor; import net.sourceforge.pmd.lang.LanguageVersionHandler; @@ -36,6 +38,8 @@ import net.sourceforge.pmd.util.designerbindings.DesignerBindings; public class JavaLanguageProcessor extends BatchLanguageProcessor implements LanguageVersionHandler { + private static final Logger LOG = LoggerFactory.getLogger(JavaLanguageProcessor.class); + private final LanguageMetricsProvider myMetricsProvider = new JavaMetricsProvider(); private final JavaParser parser; private final JavaParser parserWithoutProcessing; @@ -52,6 +56,7 @@ public class JavaLanguageProcessor extends BatchLanguageProcessor { - @Nullable URL url = getUrlOfInternalName(iname); - if (url == null) { + @Nullable InputStream inputStream = getStreamOfInternalName(iname); + if (inputStream == null) { return failed; } - return new ClassStub(this, iname, new UrlLoader(url), ClassStub.UNKNOWN_ARITY); + return new ClassStub(this, iname, new StreamLoader(binaryName, inputStream), ClassStub.UNKNOWN_ARITY); }); if (!found.hasCanonicalName()) { @@ -84,7 +87,7 @@ public class AsmSymbolResolver implements SymbolResolver { } @Nullable - URL getUrlOfInternalName(String internalName) { + InputStream getStreamOfInternalName(String internalName) { return classLoader.findResource(internalName + ".class"); } @@ -105,9 +108,38 @@ public class AsmSymbolResolver implements SymbolResolver { if (prev != failed && prev != null) { return prev; } - @Nullable URL url = getUrlOfInternalName(iname); - Loader loader = url == null ? FailedLoader.INSTANCE : new UrlLoader(url); + @Nullable InputStream inputStream = getStreamOfInternalName(iname); + Loader loader = inputStream == null ? FailedLoader.INSTANCE : new StreamLoader(internalName, inputStream); return new ClassStub(this, iname, loader, observedArity); }); } + + @Override + public void logStats() { + int numParsed = 0; + int numFailed = 0; + int numFailedQueries = 0; + int numNotParsed = 0; + + for (ClassStub stub : knownStubs.values()) { + if (stub == failed) { // NOPMD CompareObjectsWithEquals + // Note that failed queries may occur under normal circumstances. + // Eg package names may be queried just to figure + // out whether they're packages or classes. + numFailedQueries++; + } else if (stub.isNotParsed()) { + numNotParsed++; + } else if (!stub.isFailed()) { + numParsed++; + } else { + numFailed++; + } + } + + LOG.trace("Of {} distinct queries to the classloader, {} queries failed, " + + "{} classes were found and parsed successfully, " + + "{} were found but failed parsing (!), " + + "{} were found but never parsed.", + knownStubs.size(), numFailedQueries, numParsed, numFailed, numNotParsed); + } } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/asm/ClassStub.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/asm/ClassStub.java index fe31977e30..b56331551f 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/asm/ClassStub.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/asm/ClassStub.java @@ -540,6 +540,14 @@ final class ClassStub implements JClassSymbol, AsmStub, AnnotationOwner { return getSimpleName().isEmpty(); } + boolean isFailed() { + return this.parseLock.isFailed(); + } + + boolean isNotParsed() { + return this.parseLock.isNotParsed(); + } + // diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/asm/Classpath.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/asm/Classpath.java index 0bdfa4e57b..b879b053fd 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/asm/Classpath.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/asm/Classpath.java @@ -4,7 +4,7 @@ package net.sourceforge.pmd.lang.java.symbols.internal.asm; -import java.net.URL; +import java.io.InputStream; import java.util.Set; import org.checkerframework.checker.nullness.qual.Nullable; @@ -23,9 +23,9 @@ public interface Classpath { * * @param resourcePath Resource path, as described in {@link ClassLoader#getResource(String)} * - * @return A URL if the resource exists, otherwise null + * @return A InputStream if the resource exists, otherwise null */ - @Nullable URL findResource(String resourcePath); + @Nullable InputStream findResource(String resourcePath); // @@ -42,7 +42,7 @@ public interface Classpath { default Classpath delegateTo(Classpath c) { return path -> { - URL p = findResource(path); + InputStream p = findResource(path); if (p != null) { return p; } @@ -56,11 +56,11 @@ public interface Classpath { /** - * Returns a classpath instance that uses {@link ClassLoader#getResource(String)} + * Returns a classpath instance that uses {@link ClassLoader#getResourceAsStream(String)} * to find resources. */ static Classpath forClassLoader(ClassLoader classLoader) { - return classLoader::getResource; + return classLoader::getResourceAsStream; } static Classpath contextClasspath() { diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/asm/Loader.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/asm/Loader.java index a44cc6bc03..8289601568 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/asm/Loader.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/asm/Loader.java @@ -7,7 +7,6 @@ package net.sourceforge.pmd.lang.java.symbols.internal.asm; import java.io.IOException; import java.io.InputStream; -import java.net.URL; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -35,27 +34,23 @@ abstract class Loader { } - static class UrlLoader extends Loader { + static class StreamLoader extends Loader { + private final @NonNull String name; + private final @NonNull InputStream stream; - private final @NonNull URL url; - - UrlLoader(@NonNull URL url) { - assert url != null : "Null url"; - this.url = url; + StreamLoader(@NonNull String name, @NonNull InputStream stream) { + this.name = name; + this.stream = stream; } - @Override - @Nullable - InputStream getInputStream() throws IOException { - return url.openStream(); + @NonNull InputStream getInputStream() { + return stream; } @Override public String toString() { - return "(URL loader)"; + return "(StreamLoader for " + name + ")"; } } - - } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/asm/ParseLock.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/asm/ParseLock.java index ed8b234168..7ed4f4d37f 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/asm/ParseLock.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/asm/ParseLock.java @@ -58,6 +58,10 @@ abstract class ParseLock { return getFinalStatus() == ParseStatus.FAILED; } + public boolean isNotParsed() { + return status == ParseStatus.NOT_PARSED; + } + // will be called in the critical section after parse is done protected void finishParse(boolean failed) { // by default do nothing diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/ast/MapSymResolver.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/ast/MapSymResolver.java index 3012ba9e17..8d25309f3b 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/ast/MapSymResolver.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/ast/MapSymResolver.java @@ -8,6 +8,8 @@ import java.util.Map; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import net.sourceforge.pmd.lang.java.symbols.JClassSymbol; import net.sourceforge.pmd.lang.java.symbols.SymbolResolver; @@ -16,6 +18,7 @@ import net.sourceforge.pmd.lang.java.symbols.SymbolResolver; * A symbol resolver that knows about a few hand-picked symbols. */ final class MapSymResolver implements SymbolResolver { + private static final Logger LOG = LoggerFactory.getLogger(MapSymResolver.class); private final Map byCanonicalName; private final Map byBinaryName; @@ -35,4 +38,10 @@ final class MapSymResolver implements SymbolResolver { public @Nullable JClassSymbol resolveClassFromCanonicalName(@NonNull String canonicalName) { return byCanonicalName.get(canonicalName); } + + @Override + public void logStats() { + LOG.trace("Used {} classes by canonical name and {} classes by binary name", + byCanonicalName.size(), byBinaryName.size()); + } } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeSystem.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeSystem.java index 788dc9ec94..c256bfc690 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeSystem.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeSystem.java @@ -737,6 +737,13 @@ public final class TypeSystem { return new TypeVarImpl.RegularTypeVar(this, symbol, HashTreePSet.empty()); } + /** + * Called at the end of the analysis to log statistics about the loaded types. + */ + public void logStats() { + resolver.logStats(); + } + private static final class NullType implements JTypeMirror { private final TypeSystem ts; diff --git a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/symbols/internal/asm/AsmLoaderTest.kt b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/symbols/internal/asm/AsmLoaderTest.kt index f0e6c7f76a..a89096feb6 100644 --- a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/symbols/internal/asm/AsmLoaderTest.kt +++ b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/symbols/internal/asm/AsmLoaderTest.kt @@ -38,7 +38,7 @@ class AsmLoaderTest : IntelliMarker, FunSpec({ // access flags // method reference with static ctdecl & zero formal parameters (asInstanceMethod) - val contextClasspath = Classpath { Thread.currentThread().contextClassLoader.getResource(it) } + val contextClasspath = Classpath { Thread.currentThread().contextClassLoader.getResourceAsStream(it) } test("First ever ASM test") {