Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[java] Support Java 23 #5112

Merged
merged 21 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
568314b
[java] Add new language version 23 and 23-preview
adangel Jul 11, 2024
2a04d98
[java] Bump asm from 9.6 to 9.7
adangel Jul 11, 2024
f13e886
[java] Allow to build PMD with Java 23
adangel Jul 26, 2024
10681cd
[java] Remove version 21-preview
adangel Jul 11, 2024
a7eab29
[java] Make UNNAMED_VARIABLES_AND_PATTERNS a regular language feature
adangel Jul 11, 2024
20750f9
[java] Update implementation for "Implicitly Declared Classes...
adangel Jul 11, 2024
bb40b75
[java] Update impl for "Flexible Constructor Bodies"
adangel Jul 12, 2024
7a6662f
[java] Support "Markdown documentation comments" (JEP 467)
adangel Jul 12, 2024
176e522
[java] UnnecessaryImportRule: Support Markdown comments
adangel Jul 20, 2024
47470b5
[java] Support module import declarations (JEP 476)
adangel Jul 20, 2024
0823c88
[java] Support primitive types in instanceof (Java 23 Preview)
adangel Jul 26, 2024
55d57ac
[java] Remove String Template Preview feature for Java 23
adangel Jul 26, 2024
cbc475e
[java] Add ImplicitClassDeclaration
adangel Jul 29, 2024
d171bcb
[java] Default Imports for simple compilation unit
adangel Jul 31, 2024
c53462b
Improve RuleTst performance
adangel Aug 1, 2024
17a4a48
[java] CommentRequired - add test for markdown comments
adangel Aug 1, 2024
21d499d
[doc] Release Notes for Java 23 Support (#5062)
adangel Aug 1, 2024
13b8556
Fixup from review (#5112)
adangel Aug 13, 2024
b51be09
[core] Cache moduleName to URLs in ClasspathClassLoader
adangel Aug 13, 2024
d7d8c9c
[java] Rename ASTImportDeclaration#isModuleImport
adangel Aug 27, 2024
93bfe7d
Merge branch 'master' into issue-5062-support-java-23
adangel Aug 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
33 changes: 33 additions & 0 deletions docs/pages/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,39 @@ This is a {{ site.pmd.release_type }} release.

### 🚀 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.

### 🐛 Fixed Issues
* apex-performance
* [#5139](https://github.com/pmd/pmd/issues/5139): \[apex] OperationWithHighCostInLoop not firing in triggers
* java
* [#5062](https://github.com/pmd/pmd/issues/5062): \[java] Support Java 23
* plsql-bestpractices
* [#5132](https://github.com/pmd/pmd/issues/5132): \[plsql] TomKytesDespair - exception 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 @@ -31,6 +57,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#isModule() %}
* {%jdoc !ac!java::lang.java.ast.JavaVisitorBase#visit(java::lang.java.ast.ASTImplicitClassDeclaration,P) %}

### ✨ External Contributions

{% endtocmaker %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -30,6 +31,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 +204,32 @@ 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;

@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 +240,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 +251,77 @@ 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 ModuleFinder extends ClassVisitor {
private String moduleName;

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

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

public String getModuleName() {
return moduleName;
}
}

private URL findModule(Enumeration<URL> moduleInfoUrls, String moduleName) throws IOException {
while (moduleInfoUrls.hasMoreElements()) {
URL url = moduleInfoUrls.nextElement();

ModuleFinder finder = new ModuleFinder();
try (InputStream inputStream = url.openStream()) {
ClassReader classReader = new ClassReader(inputStream);
classReader.accept(finder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
}
if (moduleName.equals(finder.getModuleName())) {
return url;
}
}

return null;
}

@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) {
try {
Enumeration<URL> moduleInfoUrls = findResources(MODULE_INFO_SUFFIX);
URL moduleUrl = findModule(moduleInfoUrls, moduleName);

// no match in this classloader, search in parents
if (moduleUrl == null) {
moduleInfoUrls = getParent().getResources(MODULE_INFO_SUFFIX);
moduleUrl = findModule(moduleInfoUrls, moduleName);
}

return moduleUrl;
adangel marked this conversation as resolved.
Show resolved Hide resolved
} catch (IOException e) {
throw new RuntimeException(e);
}
}

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
31 changes: 24 additions & 7 deletions pmd-java/etc/grammar/Java.jjt
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
/**
* Support "JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview)" (Java 23)
* Changes in InstanceOfExpression
* Support "JEP 476: Module Import Declarations (Preview)" (Java 23)
* Changes in ImportDeclaration
* Support "JEP 477: Implicitly Declared Classes and Instance Main Methods (Third Preview)" (Java 23)
* Changes in CompilationUnit, added new node ImplicitClassDeclaration
* Andreas Dangel 07/2024
*====================================================================
* Support "JEP 447: Statements before super(...) (Preview)" (Java 22)
* Changes in ConstructorBlock
* Support "JEP 456: Unnamed Variables & Patterns" (Java 22)
Expand Down Expand Up @@ -1106,9 +1114,14 @@ ASTCompilationUnit CompilationUnit() :

// SimpleCompilationUnit:
[
( LOOKAHEAD(3) ClassMemberDeclarationNoMethod() )*
ModifierList() MethodDeclaration()
( ClassOrInterfaceBodyDeclaration() )*
(
{ pushEmptyModifierList(); }
(
( LOOKAHEAD(3) ClassMemberDeclarationNoMethod() )*
ModifierList() MethodDeclaration()
( ClassOrInterfaceBodyDeclaration() )*
) #ClassBody
) #ImplicitClassDeclaration
]

<EOF>
Expand Down Expand Up @@ -1148,9 +1161,13 @@ void PackageDeclaration() :
void ImportDeclaration() :
{String image;}
{
"import" [ "static" {jjtThis.setStatic();} ]
image=VoidName() { jjtThis.setImage(image); }
[ "." "*" {jjtThis.setImportOnDemand();} ] ";"
"import"
(
"static" {jjtThis.setStatic();}
| LOOKAHEAD({isKeyword("module")}) <IDENTIFIER> {jjtThis.setModuleImport();}
)?
image=VoidName() { jjtThis.setImage(image); }
[ "." "*" {jjtThis.setImportOnDemand();} ] ";"
}

/*
Expand Down Expand Up @@ -1904,7 +1921,7 @@ void InstanceOfExpression() #void:
("instanceof"
(
LOOKAHEAD(ReferenceType() "(") RecordPattern()
| AnnotatedRefType() [
| AnnotatedType() [
VariableDeclaratorId() #TypePattern(2)
| RecordStructurePattern() #RecordPattern(2)
]
Expand Down
Loading