Merge pull request #4628 from adangel:support-jrt-fs
[java] Support loading classes from java runtime images #4628
This commit is contained in:
commit
070cca8743
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
|
@ -323,7 +323,8 @@ public class PmdCommand extends AbstractAnalysisPmdSubcommand<PMDConfiguration>
|
||||
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) {
|
||||
|
@ -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<String, Set<String>> packagesDirsToModules;
|
||||
|
||||
static {
|
||||
registerAsParallelCapable();
|
||||
}
|
||||
|
||||
public ClasspathClassLoader(List<File> 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<File> files) throws IOException {
|
||||
|
||||
List<URL> 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<URL> fileToURL(List<File> files) throws IOException {
|
||||
List<URL> urlList = new ArrayList<>();
|
||||
for (File f : files) {
|
||||
urlList.add(createURLFromPath(f.getAbsolutePath()));
|
||||
}
|
||||
return urlList;
|
||||
}
|
||||
|
||||
private List<URL> initURLs(String classpath) {
|
||||
AssertionUtil.requireParamNotNull("classpath", classpath);
|
||||
final List<URL> 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<URL> urls, final String classpath) throws MalformedURLException {
|
||||
private void addClasspathURLs(final List<URL> 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<URL> urls, URL fileURL) throws IOException {
|
||||
private void addFileURLs(List<URL> 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 <a href="https://openjdk.org/jeps/220">JEP 220: Modular Run-Time Images</a>
|
||||
*/
|
||||
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<String, String> 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<Path> packagesStream = Files.list(packages)) {
|
||||
packagesStream.forEach(p -> {
|
||||
String packageName = p.getFileName().toString().replace('.', '/');
|
||||
try (Stream<Path> modulesStream = Files.list(p)) {
|
||||
Set<String> 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<String> 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();
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* This test only runs, if you have a folder ${HOME}/openjdk{javaVersion}.
|
||||
* </p>
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
}
|
@ -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<JavaLanguageProperties>
|
||||
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<JavaLanguagePr
|
||||
|
||||
public JavaLanguageProcessor(JavaLanguageProperties properties) {
|
||||
this(properties, TypeSystem.usingClassLoaderClasspath(properties.getAnalysisClassLoader()));
|
||||
LOG.debug("Using analysis classloader: {}", properties.getAnalysisClassLoader());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -124,4 +129,10 @@ public class JavaLanguageProcessor extends BatchLanguageProcessor<JavaLanguagePr
|
||||
public void setTypeSystem(TypeSystem ts) {
|
||||
this.typeSystem = Objects.requireNonNull(ts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
this.typeSystem.logStats();
|
||||
super.close();
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +81,16 @@ public interface SymbolResolver {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logStats() {
|
||||
stack.forEach(SymbolResolver::logStats);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Called at the end of the analysis in order to log out statistics of the resolved symbols.
|
||||
*/
|
||||
void logStats();
|
||||
}
|
||||
|
@ -5,18 +5,20 @@
|
||||
package net.sourceforge.pmd.lang.java.symbols.internal.asm;
|
||||
|
||||
|
||||
import java.net.URL;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol;
|
||||
import net.sourceforge.pmd.lang.java.symbols.SymbolResolver;
|
||||
import net.sourceforge.pmd.lang.java.symbols.internal.asm.Loader.FailedLoader;
|
||||
import net.sourceforge.pmd.lang.java.symbols.internal.asm.Loader.UrlLoader;
|
||||
import net.sourceforge.pmd.lang.java.symbols.internal.asm.Loader.StreamLoader;
|
||||
import net.sourceforge.pmd.lang.java.types.TypeSystem;
|
||||
import net.sourceforge.pmd.util.AssertionUtil;
|
||||
|
||||
@ -24,6 +26,7 @@ import net.sourceforge.pmd.util.AssertionUtil;
|
||||
* A {@link SymbolResolver} that reads class files to produce symbols.
|
||||
*/
|
||||
public class AsmSymbolResolver implements SymbolResolver {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AsmSymbolResolver.class);
|
||||
|
||||
static final int ASM_API_V = Opcodes.ASM9;
|
||||
|
||||
@ -53,12 +56,12 @@ public class AsmSymbolResolver implements SymbolResolver {
|
||||
String internalName = getInternalName(binaryName);
|
||||
|
||||
ClassStub found = knownStubs.computeIfAbsent(internalName, iname -> {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
// </editor-fold>
|
||||
|
||||
|
@ -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);
|
||||
|
||||
// <editor-fold defaultstate="collapsed" desc="Transformation methods (defaults)">
|
||||
|
||||
@ -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() {
|
||||
|
@ -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 + ")";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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<String, JClassSymbol> byCanonicalName;
|
||||
private final Map<String, JClassSymbol> 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());
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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") {
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user