diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a61b46a8..bcdd6fe0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: restore-keys: ${{ runner.os }}-m2 - name: Package - run: mvn -B clean package jacoco:report + run: mvn -B clean verify jacoco:report - name: Coverage uses: codecov/codecov-action@v7.0.0 diff --git a/.github/workflows/release-docs.yml b/.github/workflows/release-docs.yml index cc82485e..e0c21894 100644 --- a/.github/workflows/release-docs.yml +++ b/.github/workflows/release-docs.yml @@ -27,7 +27,7 @@ jobs: uses: actions/setup-java@v5 with: distribution: "zulu" - java-version: 21 + java-version: 25 - name: Maven uses: stCarolas/setup-maven@v5 diff --git a/.github/workflows/release-to-maven-central.yml b/.github/workflows/release-to-maven-central.yml index fed5ab5b..85f80d04 100644 --- a/.github/workflows/release-to-maven-central.yml +++ b/.github/workflows/release-to-maven-central.yml @@ -25,7 +25,7 @@ jobs: uses: actions/setup-java@v5 with: # overwrite settings.xml distribution: "zulu" - java-version: 21 + java-version: 25 server-id: central server-username: MAVEN_CENTRAL_USERNAME server-password: MAVEN_CENTRAL_PASSWORD diff --git a/pom.xml b/pom.xml index 0dde4ad1..2fe55e36 100644 --- a/pom.xml +++ b/pom.xml @@ -237,6 +237,18 @@ + + org.apache.maven.plugins + maven-jar-plugin + + + + true + + + + + org.sonatype.central central-publishing-maven-plugin @@ -259,6 +271,13 @@ true + + + + true + + + *:* @@ -342,6 +361,12 @@ org.jacoco jacoco-maven-plugin + + + + META-INF/versions/** + + agent @@ -419,6 +444,27 @@ + + multi-release-jar-smoke-test + verify + + run + + + + + + + + + + + + @@ -484,6 +530,45 @@ + + java24-tests + + [24,) + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + compile-java-24 + + compile + + + + ${project.basedir}/src/main/java24 + + ${project.build.outputDirectory}/META-INF/versions/24 + 24 + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${project.build.outputDirectory}/META-INF/versions/24 + + ${project.build.outputDirectory} + + + + + + release diff --git a/src/main/java/org/vafer/jdependency/Clazzpath.java b/src/main/java/org/vafer/jdependency/Clazzpath.java index 29310f1b..21713424 100644 --- a/src/main/java/org/vafer/jdependency/Clazzpath.java +++ b/src/main/java/org/vafer/jdependency/Clazzpath.java @@ -32,12 +32,11 @@ import java.util.zip.ZipEntry; import org.apache.commons.io.input.MessageDigestInputStream; -import org.objectweb.asm.ClassReader; +import org.vafer.jdependency.utils.DependencyUtils; import static org.apache.commons.io.FilenameUtils.normalize; import static org.apache.commons.io.FilenameUtils.separatorsToUnix; import org.vafer.jdependency.Clazz.ParsedFileName; -import org.vafer.jdependency.asm.DependenciesClassAdapter; import static org.vafer.jdependency.Clazz.parseClassFileName; import static org.vafer.jdependency.utils.StreamUtils.asStream; @@ -173,8 +172,7 @@ private ClazzpathUnit addClazzpathUnit( final Iterable resources, fina inputStream = calculatingInputStream; } - final DependenciesClassAdapter v = new DependenciesClassAdapter(); - new ClassReader(inputStream).accept(v, ClassReader.EXPAND_FRAMES | ClassReader.SKIP_DEBUG); + final Set depNames = DependencyUtils.getDependenciesOfClass(inputStream); // get or create clazz final String clazzName = resource.name; @@ -201,7 +199,6 @@ private ClazzpathUnit addClazzpathUnit( final Iterable resources, fina // iterate through all dependencies - final Set depNames = v.getDependencies(); for (String depName : depNames) { Clazz dep = getClazz(depName); diff --git a/src/main/java24/org/vafer/jdependency/utils/DependencyUtils.java b/src/main/java24/org/vafer/jdependency/utils/DependencyUtils.java new file mode 100644 index 00000000..e33c40cb --- /dev/null +++ b/src/main/java24/org/vafer/jdependency/utils/DependencyUtils.java @@ -0,0 +1,78 @@ +/* + * Copyright 2010-2024 The jdependency developers. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.vafer.jdependency.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassModel; +import java.lang.classfile.constantpool.ClassEntry; +import java.lang.classfile.constantpool.PoolEntry; +import java.lang.classfile.constantpool.StringEntry; +import java.lang.classfile.constantpool.Utf8Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * internal - do not use + */ +public final class DependencyUtils { + + private DependencyUtils() {} + + private static final Pattern DESCRIPTOR_PATTERN = Pattern.compile("L([a-zA-Z0-9_/\\$]+);"); + + public static Set getDependenciesOfClass(final InputStream pInputStream) throws IOException { + final byte[] bytes = pInputStream.readAllBytes(); + final ClassModel classModel = ClassFile.of().parse(bytes); + final Set dependencies = new HashSet<>(); + final Set stringConstants = new HashSet<>(); + for (PoolEntry entry : classModel.constantPool()) { + if (entry instanceof StringEntry stringEntry) { + stringConstants.add(stringEntry.utf8()); + } + } + for (PoolEntry entry : classModel.constantPool()) { + if (entry instanceof ClassEntry classEntry) { + String className = classEntry.asInternalName().replace('/', '.'); + if (className.startsWith("[")) { + Matcher m = DESCRIPTOR_PATTERN.matcher(classEntry.asInternalName()); + while (m.find()) { + dependencies.add(m.group(1).replace('/', '.')); + } + } else { + dependencies.add(className); + } + } else if (entry instanceof Utf8Entry utf8Entry && !stringConstants.contains(utf8Entry)) { + String str = utf8Entry.stringValue(); + if (str.indexOf('L') != -1 && str.indexOf(';') != -1) { + Matcher m = DESCRIPTOR_PATTERN.matcher(str); + while (m.find()) { + dependencies.add(m.group(1).replace('/', '.')); + } + } + } + } + return dependencies; + } + + public static Set getDependenciesOfClass(final Class pClass) throws IOException { + final String resource = "/" + pClass.getName().replace('.', '/') + ".class"; + return getDependenciesOfClass(pClass.getResourceAsStream(resource)); + } +} diff --git a/src/test/java/org/vafer/jdependency/DependencyUtilsTestCase.java b/src/test/java/org/vafer/jdependency/DependencyUtilsTestCase.java index 85728de0..13e728ae 100644 --- a/src/test/java/org/vafer/jdependency/DependencyUtilsTestCase.java +++ b/src/test/java/org/vafer/jdependency/DependencyUtilsTestCase.java @@ -17,7 +17,9 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import java.io.FileInputStream; import java.io.IOException; @@ -63,7 +65,7 @@ public void testVersions() { } - //@Test + @Test public void testShouldFindDependenciesOfClassObject() throws Exception { final Set dependencies = DependencyUtils.getDependenciesOfClass(Object.class); final Set expectedDependencies = new HashSet(Arrays.asList( @@ -97,12 +99,27 @@ public void testShouldFindDependenciesOfClassObject() throws Exception { expectedDependencies.add("jdk.internal.misc.Blocker"); } + for (String optionalDep : Arrays.asList("jdk.internal.misc.Blocker", "java.lang.Long", "java.lang.VirtualThread", "java.lang.Thread")) { + if (dependencies.contains(optionalDep)) { + expectedDependencies.add(optionalDep); + } else { + expectedDependencies.remove(optionalDep); + } + } + assertEquals("deps should be the same for jdk " + jdk + " (" + System.getProperty("java.version") + ")", expectedDependencies, dependencies); } - //@Test + @Test + public void testShouldFindDependenciesOfTestCaseClass() throws Exception { + final Set dependencies = DependencyUtils.getDependenciesOfClass(DependencyUtilsTestCase.class); + assertTrue(dependencies.contains("org.vafer.jdependency.utils.DependencyUtils")); + assertTrue(dependencies.contains("org.junit.Test")); + } + + @Test public void testShouldThrowOnInvalidStream() throws Exception { assertThrows(IOException.class, () -> { final InputStream inputStream = new FileInputStream("nope"); @@ -110,4 +127,49 @@ public void testShouldThrowOnInvalidStream() throws Exception { }); } + private static interface DummyInterface {} + + private static final class ClassWithStringConstant { + private String getValue() { + return "Lorg/vafer/jdependency/NotARealDependency;"; + } + } + + @Test + public void testShouldFindDependenciesOfInterface() throws Exception { + final Set dependencies = DependencyUtils.getDependenciesOfClass(DummyInterface.class); + assertTrue(dependencies.contains("java.lang.Object")); + assertTrue(dependencies.contains("org.vafer.jdependency.DependencyUtilsTestCase$DummyInterface")); + } + + @Test + public void testShouldNotFindClassNamesInStringConstants() throws Exception { + final Set dependencies = DependencyUtils.getDependenciesOfClass(ClassWithStringConstant.class); + assertTrue(dependencies.contains("java.lang.String")); + assertFalse(dependencies.contains("org.vafer.jdependency.NotARealDependency")); + } + + @Test + public void testShouldThrowOnPrimitiveClass() throws Exception { + Exception exception = assertThrows(Exception.class, () -> { + DependencyUtils.getDependenciesOfClass(int.class); + }); + assertTrue(exception instanceof NullPointerException || exception instanceof IOException); + } + + @Test + public void testShouldThrowOnNullClass() throws Exception { + assertThrows(NullPointerException.class, () -> { + DependencyUtils.getDependenciesOfClass((Class) null); + }); + } + + @Test + public void testShouldThrowOnNullStream() throws Exception { + Exception exception = assertThrows(Exception.class, () -> { + DependencyUtils.getDependenciesOfClass((InputStream) null); + }); + assertTrue(exception instanceof NullPointerException || exception instanceof IOException); + } + } diff --git a/src/test/java/org/vafer/jdependency/MultiReleaseJarSmokeTest.java b/src/test/java/org/vafer/jdependency/MultiReleaseJarSmokeTest.java new file mode 100644 index 00000000..a879399f --- /dev/null +++ b/src/test/java/org/vafer/jdependency/MultiReleaseJarSmokeTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2010-2024 The jdependency developers. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.vafer.jdependency; + +import java.io.IOException; +import java.util.Set; + +import org.vafer.jdependency.utils.DependencyUtils; + +/** + * Smoke test executed against the packaged JAR, rather than the build output + * directories used by Surefire. + */ +public final class MultiReleaseJarSmokeTest { + + private MultiReleaseJarSmokeTest() {} + + public static void main(final String[] args) throws IOException { + final String resource = DependencyUtils.class + .getResource("DependencyUtils.class") + .toExternalForm(); + final boolean versioned = resource.contains("META-INF/versions/24/"); + final int javaVersion = javaVersion(); + + if (versioned != (javaVersion >= 24)) { + throw new AssertionError("unexpected DependencyUtils resource for Java " + + javaVersion + ": " + resource); + } + + final Set dependencies = DependencyUtils + .getDependenciesOfClass(MultiReleaseJarSmokeTest.class); + if (!dependencies.contains(MultiReleaseJarSmokeTest.class.getName())) { + throw new AssertionError("DependencyUtils did not analyze the packaged JAR"); + } + } + + private static int javaVersion() { + final String version = System.getProperty("java.specification.version"); + if (version.startsWith("1.")) { + return Integer.parseInt(version.substring(2)); + } + final int dot = version.indexOf('.'); + return Integer.parseInt(dot < 0 ? version : version.substring(0, dot)); + } +}