Skip to content

Commit

Permalink
[java] Support Java 23 (#5112)
Browse files Browse the repository at this point in the history
Merge pull request #5112 from issue-5062-support-java-23
  • Loading branch information
adangel committed Aug 29, 2024
2 parents 819b6bc + 93bfe7d commit 537dab9
Show file tree
Hide file tree
Showing 98 changed files with 2,906 additions and 2,192 deletions.
7 changes: 4 additions & 3 deletions docs/pages/pmd/languages/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Java support
permalink: pmd_languages_java.html
author: Clément Fournier
last_updated: December 2023 (7.0.0)
last_updated: July 2024 (7.5.0)
tags: [languages, PmdCapableLanguage, CpdCapableLanguage]
summary: "Java-specific features and guidance"
---
Expand All @@ -15,9 +15,10 @@ Usually the latest non-preview Java Version is the default version.

| Java Version | Alias | Supported by PMD since |
|--------------|-------|------------------------|
| 23-preview | | 7.5.0 |
| 23 (default) | | 7.5.0 |
| 22-preview | | 7.0.0 |
| 22 (default) | | 7.0.0 |
| 21-preview | | 7.0.0 |
| 22 | | 7.0.0 |
| 21 | | 7.0.0 |
| 20 | | 6.55.0 |
| 19 | | 6.48.0 |
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/pmd/userdocs/tools/ant.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ accordingly and this rule won't be executed.
The specific version of a language to be used is selected via the `sourceLanguage`
nested element. Example:

<sourceLanguage name="java" version="22"/>
<sourceLanguage name="java" version="23"/>

The available versions depend on the language. You can get a list of the currently supported language versions
via the CLI option `--help`.
Expand Down
31 changes: 31 additions & 0 deletions docs/pages/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,28 @@ This is a {{ site.pmd.release_type }} release.
{% tocmaker is_release_notes_processor %}

### 🚀 New and noteworthy
#### New: Java 23 Support

This release of PMD brings support for Java 23. There are no new standard language features,
but a couple of preview language features:

* [JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview)](https://openjdk.org/jeps/455)
* [JEP 476: Module Import Declarations (Preview)](https://openjdk.org/jeps/476)
* [JEP 477: Implicitly Declared Classes and Instance Main Methods (Third Preview)](https://openjdk.org/jeps/477)
* [JEP 482: Flexible Constructor Bodies (Second Preview)](https://openjdk.org/jeps/482)

Note that String Templates (introduced as preview in Java 21 and 22) are not supported anymore in Java 23,
see [JDK-8329949](https://bugs.openjdk.org/browse/JDK-8329949) for details.

In order to analyze a project with PMD that uses these preview language features,
you'll need to enable it via the environment variable `PMD_JAVA_OPTS` and select the new language
version `23-preview`:

export PMD_JAVA_OPTS=--enable-preview
pmd check --use-version java-23-preview ...

Note: Support for Java 21 preview language features have been removed. The version "21-preview"
are no longer available.

### 🌟 New and changed rules
#### New Rules
Expand All @@ -23,6 +45,7 @@ This is a {{ site.pmd.release_type }} release.
* apex-performance
* [#5139](https://github.com/pmd/pmd/issues/5139): \[apex] OperationWithHighCostInLoop: false negative for triggers
* java
* [#5062](https://github.com/pmd/pmd/issues/5062): \[java] Support Java 23
* [#5167](https://github.com/pmd/pmd/issues/5167): \[java] java.lang.IllegalArgumentException: \<?\> cannot be a wildcard bound
* java-bestpractices
* [#3602](https://github.com/pmd/pmd/issues/3602): \[java] GuardLogStatement: False positive when compile-time constant is created from external constants
Expand All @@ -42,6 +65,7 @@ This is a {{ site.pmd.release_type }} release.
* [#5132](https://github.com/pmd/pmd/issues/5132): \[plsql] TomKytesDespair: XPathException for more complex exception handler

### 🚨 API Changes
#### Deprecations
* pmd-jsp
* {%jdoc jsp::lang.jsp.ast.JspParserImpl %} is deprecated now. It should have been package-private
because this is an implementation class that should not be used directly.
Expand All @@ -56,6 +80,13 @@ This is a {{ site.pmd.release_type }} release.
* {%jdoc visualforce::lang.visualforce.ast.VfParserImpl %} is deprecated now. It should have been package-private
because this is an implementation class that should not be used directly.

#### Experimental
* pmd-java
* Renamed `isUnnamedClass()` to {%jdoc !!java::lang.java.ast.ASTCompilationUnit#isSimpleCompilationUnit() %}
* {%jdoc java::lang.java.ast.ASTImplicitClassDeclaration %}
* {%jdoc !!java::lang.java.ast.ASTImportDeclaration#isModuleImport() %}
* {%jdoc !ac!java::lang.java.ast.JavaVisitorBase#visit(java::lang.java.ast.ASTImplicitClassDeclaration,P) %}

### ✨ External Contributions
* [#5125](https://github.com/pmd/pmd/pull/5125): \[plsql] Improve merge statement (order of merge insert/update flexible, allow prefixes in column names) - [Arjen Duursma](https://github.com/duursma) (@duursma)
* [#5175](https://github.com/pmd/pmd/pull/5175): \[java] Update AvoidSynchronizedAtMethodLevel message to mention ReentrantLock, new rule AvoidSynchronizedStatement - [Chas Honton](https://github.com/chonton) (@chonton)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -30,6 +32,11 @@
import java.util.stream.Stream;

import org.apache.commons.lang3.StringUtils;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ModuleVisitor;
import org.objectweb.asm.Opcodes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -198,12 +205,34 @@ public String toString() {
+ "] jrt-fs: " + javaHome + " parent: " + getParent() + ']';
}

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

@Nullable
private static String extractModuleName(String name) {
if (!name.endsWith(MODULE_INFO_SUFFIX_SLASH)) {
return null;
}
return name.substring(0, name.length() - MODULE_INFO_SUFFIX_SLASH.length());
}

@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) {
String moduleName = extractModuleName(name);
if (moduleName != null) {
LOG.trace("Trying to load module-info.class for module {} in jrt-fs", moduleName);
Path candidate = fileSystem.getPath("modules", moduleName, MODULE_INFO_SUFFIX);
if (Files.exists(candidate)) {
return newInputStreamFromJrtFilesystem(candidate);
}
}

int lastSlash = name.lastIndexOf('/');
String packageName = name.substring(0, Math.max(lastSlash, 0));
Set<String> moduleNames = packagesDirsToModules.get(packageName);
Expand All @@ -214,15 +243,7 @@ public InputStream getResourceAsStream(String name) {
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);
}
return newInputStreamFromJrtFilesystem(candidate);
}
}
}
Expand All @@ -233,12 +254,84 @@ public InputStream getResourceAsStream(String name) {
return super.getResourceAsStream(name);
}

private static InputStream newInputStreamFromJrtFilesystem(Path path) {
LOG.trace("Found {}", path);
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(path);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

private static class ModuleNameExtractor extends ClassVisitor {
private String moduleName;

protected ModuleNameExtractor() {
super(Opcodes.ASM9);
}

@Override
public ModuleVisitor visitModule(String name, int access, String version) {
moduleName = name;
return null;
}

public String getModuleName() {
return moduleName;
}
}

private void collectAllModules() {
if (moduleNameToModuleInfoUrls != null) {
return;
}

Map<String, URL> allModules = new HashMap<>();
try {
Enumeration<URL> moduleInfoUrls = findResources(MODULE_INFO_SUFFIX);
collectModules(allModules, moduleInfoUrls);

// also search in parents
moduleInfoUrls = getParent().getResources(MODULE_INFO_SUFFIX);
collectModules(allModules, moduleInfoUrls);

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

moduleNameToModuleInfoUrls = Collections.unmodifiableMap(allModules);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

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

ModuleNameExtractor finder = new ModuleNameExtractor();
try (InputStream inputStream = url.openStream()) {
ClassReader classReader = new ClassReader(inputStream);
classReader.accept(finder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
}
allModules.putIfAbsent(finder.getModuleName(), url);
}
}

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

String moduleName = extractModuleName(name);
if (moduleName != null) {
collectAllModules();
assert moduleNameToModuleInfoUrls != null : "Modules should have been detected by collectAllModules()";
return moduleNameToModuleInfoUrls.get(moduleName);
}

URL url = findResource(name);
if (url == null) {
// note this will actually call back into this.findResource, but
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@

package net.sourceforge.pmd.internal.util;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.endsWith;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
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.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
Expand Down Expand Up @@ -193,22 +198,56 @@ void loadFromJava(int javaVersion) throws IOException {
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);
}
assertClassFile(stream, javaVersion);
}

// should not fail for resources without a package
assertNull(loader.getResourceAsStream("ClassInDefaultPackage.class"));

// load module java.base
try (InputStream stream = loader.getResourceAsStream("java.base/module-info.class")) {
assertClassFile(stream, javaVersion);
}
}
}

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);
private void assertClassFile(InputStream inputStream, int javaVersion) throws IOException {
assertNotNull(inputStream);
try (DataInputStream data = new DataInputStream(inputStream)) {
int magicNumber = data.readInt();
assertEquals(0xcafebabe, magicNumber);
data.readUnsignedShort(); // minorVersion
int majorVersion = data.readUnsignedShort();
assertEquals(44 + javaVersion, majorVersion);
}
}

private static byte[] readBytes(InputStream stream) throws IOException {
assertNotNull(stream);
ByteArrayOutputStream data = new ByteArrayOutputStream();
try (InputStream inputStream = stream) {
byte[] buffer = new byte[8192];
int count;
while ((count = inputStream.read(buffer)) != -1) {
data.write(buffer, 0, count);
}
}
return data.toByteArray();
}

@Test
void findModuleInfoFromJar() throws IOException {
try (ClasspathClassLoader loader = new ClasspathClassLoader("", ClasspathClassLoader.class.getClassLoader())) {
// search for module org.junit.platform.suite.api, which should be on the test-classpath in pmd-core...
// inside a jar
String junitJupiterApiModule = "org.junit.platform.suite.api/module-info.class";
URL resource = loader.getResource(junitJupiterApiModule);
assertNotNull(resource);
assertThat(resource.toString(), endsWith(".jar!/module-info.class"));

byte[] fromUrl = readBytes(resource.openStream());
byte[] fromStream = readBytes(loader.getResourceAsStream(junitJupiterApiModule));
assertArrayEquals(fromUrl, fromStream, "getResource and getResourceAsStream should return the same module");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ class BinaryDistributionIT extends AbstractBinaryDistributionTest {
"java-11", "java-12", "java-13", "java-14", "java-15",
"java-16", "java-17", "java-18", "java-19",
"java-20",
"java-21", "java-21-preview",
"java-21",
"java-22", "java-22-preview",
"java-23", "java-23-preview",
"java-5", "java-6", "java-7",
"java-8", "java-9", "jsp-2", "jsp-3", "kotlin-1.6",
"kotlin-1.7", "kotlin-1.8", "modelica-3.4", "modelica-3.5",
Expand Down
Loading

0 comments on commit 537dab9

Please sign in to comment.