Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
pull_request:

env:
build_java_version: 21
build_java_version: 25

jobs:
build:
Expand Down Expand Up @@ -48,6 +48,7 @@ jobs:
- 11
- 17
- 21
- 25
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
Expand Down Expand Up @@ -97,6 +98,7 @@ jobs:
- 11
- 17
- 21
- 25
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update-gradle-wrapper.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
uses: actions/setup-java@v5.2.0
with:
distribution: 'zulu'
java-version: 17
java-version: 25

- name: Update Gradle Wrapper
uses: gradle-update/update-gradle-wrapper-action@v2.1.0
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ Contributions are very welcome. The following will provide some helpful guidelin

## How to build the project

ArchUnit requires at least JDK 15 to build.
ArchUnit requires at least JDK 25 to build.
The following is just an example input/output from a Unix command line.
Windows users should use `gradlew.bat` instead.

```
$ cd /path/to/git/clone/of/ArchUnit
$ ./gradlew showJdkVersion
Configured JDK: 15
Configured JDK: 25
$ ./gradlew build
```

Expand Down
2 changes: 1 addition & 1 deletion archunit/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ compileJdk9mainJava {
dependsOn(compileJava)
ext.minimumJavaVersion = JavaVersion.VERSION_1_9

destinationDir = compileJava.destinationDir
destinationDirectory.set(compileJava.destinationDirectory.get())
}
javadoc.dependsOn(compileJdk9mainJava)
spotbugsMain.dependsOn(compileJdk9mainJava)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
import static com.tngtech.archunit.core.domain.TestUtils.targetFrom;
import static com.tngtech.archunit.core.importer.ClassFileImporterTestUtils.findAnyByName;
import static com.tngtech.archunit.core.importer.ClassFileImporterTestUtils.getByName;
import static com.tngtech.archunit.core.importer.ClassFileImporterTestUtils.getMethodCallsFromClassWithoutAutomaticNullCheck;
import static com.tngtech.archunit.core.importer.DomainBuilders.newMethodCallTargetBuilder;
import static com.tngtech.archunit.testutil.Assertions.assertThat;
import static com.tngtech.archunit.testutil.Assertions.assertThatAccess;
Expand Down Expand Up @@ -457,9 +458,10 @@ String covariantlyOverriddenCausingBridgeMethod() {
}

JavaMethodCall callFromBridgeMethodToOverriddenOne = getOnlyElement(
new ClassFileImporter().importClasses(Parent.class, Child.class)
.get(Child.class)
.getMethodCallsFromSelf());
getMethodCallsFromClassWithoutAutomaticNullCheck(
new ClassFileImporter().importClasses(Parent.class, Child.class)
.get(Child.class)
));
JavaCodeUnit bridgeMethod = callFromBridgeMethodToOverriddenOne.getOrigin();

assertThat(bridgeMethod.getName()).isEqualTo("covariantlyOverriddenCausingBridgeMethod");
Expand Down Expand Up @@ -1146,10 +1148,11 @@ void call(Target target) {
}
}

JavaMethodCall call = getOnlyElement(new ClassFileImporter()
.importClasses(Origin.class, Target.class)
.get(Origin.class)
.getMethodCallsFromSelf());
JavaMethodCall call = getOnlyElement(
getMethodCallsFromClassWithoutAutomaticNullCheck(
new ClassFileImporter().importClasses(Origin.class, Target.class)
.get(Origin.class)
));

assertThat(call.getTarget().getParameterTypes())
.isEqualTo(call.getTarget().getRawParameterTypes());
Expand Down Expand Up @@ -1193,10 +1196,11 @@ void call(Target target) {
}
}

JavaClass origin = new ClassFileImporter().importClasses(Origin.class, Target.class).get(Origin.class);

assertThat(origin.getMethodCallsFromSelf()).hasSize(2);
for (JavaMethodCall call : origin.getMethodCallsFromSelf()) {
JavaClass origin = new ClassFileImporter().importClasses(Origin.class, Target.class)
.get(Origin.class);
Set<JavaMethodCall> methodCallsFromSelf = getMethodCallsFromClassWithoutAutomaticNullCheck(origin);
assertThat(methodCallsFromSelf).hasSize(2);
for (JavaMethodCall call : methodCallsFromSelf) {
assertThatCall(call).isTo("callMe");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.junit.runner.RunWith;

import static com.google.common.collect.Iterables.getOnlyElement;
import static com.tngtech.archunit.core.importer.ClassFileImporterTestUtils.getMethodCallsFromClassWithoutAutomaticNullCheck;
import static com.tngtech.archunit.core.importer.DependencyResolutionProcess.MAX_ITERATIONS_FOR_ACCESSES_TO_TYPES_PROPERTY_NAME;
import static com.tngtech.archunit.core.importer.DependencyResolutionProcess.MAX_ITERATIONS_FOR_ANNOTATION_TYPES_PROPERTY_NAME;
import static com.tngtech.archunit.core.importer.DependencyResolutionProcess.MAX_ITERATIONS_FOR_ENCLOSING_TYPES_DEFAULT_VALUE;
Expand Down Expand Up @@ -351,7 +352,7 @@ void resolvesMethodCallTargetOwner() {

JavaClass javaClass = ImporterWithAdjustedResolutionRuns.disableAllIterationsExcept(MAX_ITERATIONS_FOR_ACCESSES_TO_TYPES_PROPERTY_NAME)
.importClass(Origin.class);
JavaMethodCall call = getOnlyElement(javaClass.getMethodCallsFromSelf());
JavaMethodCall call = getOnlyElement(getMethodCallsFromClassWithoutAutomaticNullCheck(javaClass));

assertThat(call.getTargetOwner()).isFullyImported(true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import static com.tngtech.archunit.core.domain.Formatters.formatNamesOf;
import static com.tngtech.archunit.core.domain.JavaConstructor.CONSTRUCTOR_NAME;
import static com.tngtech.archunit.core.domain.properties.HasName.Utils.namesOf;
import static com.tngtech.archunit.core.importer.ClassFileImporterTestUtils.getMethodCallsFromClassWithoutAutomaticNullCheck;
import static com.tngtech.archunit.core.importer.JavaClassDescriptorImporterTestUtils.isLambdaMethodName;
import static com.tngtech.archunit.testutil.Assertions.assertThat;
import static com.tngtech.archunit.testutil.Assertions.assertThatAccess;
Expand Down Expand Up @@ -68,7 +69,7 @@ Runnable call() {
}

JavaClasses classes = new ClassFileImporter().importClasses(Target.class, Caller.class);
JavaMethodCall call = getOnlyElement(classes.get(Caller.class).getMethodCallsFromSelf());
JavaMethodCall call = getOnlyElement(getMethodCallsFromClassWithoutAutomaticNullCheck(classes.get(Caller.class)));

assertThatCall(call).isFrom("call").isTo(Target.class, "target");
}
Expand Down Expand Up @@ -172,7 +173,7 @@ Consumer<String> call() {
}

JavaClasses classes = new ClassFileImporter().importClasses(Target.class, Caller.class);
JavaMethodCall call = getOnlyElement(classes.get(Caller.class).getMethodCallsFromSelf());
JavaMethodCall call = getOnlyElement(getMethodCallsFromClassWithoutAutomaticNullCheck(classes.get(Caller.class)));

assertThatCall(call).isFrom("call").isTo(Target.class, "target", String.class);
}
Expand All @@ -196,7 +197,7 @@ Supplier<String> call() {
}

JavaClasses classes = new ClassFileImporter().importClasses(Target.class, Caller.class);
JavaMethodCall call = getOnlyElement(classes.get(Caller.class).getMethodCallsFromSelf());
JavaMethodCall call = getOnlyElement(getMethodCallsFromClassWithoutAutomaticNullCheck(classes.get(Caller.class)));

assertThatCall(call).isFrom("call").isTo(Target.class, "target");
}
Expand Down Expand Up @@ -252,7 +253,7 @@ Runnable call() {
}

JavaClasses classes = new ClassFileImporter().importClasses(Target.class, Caller.class);
Set<JavaMethodCall> calls = classes.get(Caller.class).getMethodCallsFromSelf();
Set<JavaMethodCall> calls = getMethodCallsFromClassWithoutAutomaticNullCheck(classes.get(Caller.class));

assertThat(calls).hasSize(2);
calls.forEach(call -> assertThatCall(call).isFrom("call").isTo(Target.class, "target"));
Expand Down Expand Up @@ -285,7 +286,7 @@ Function<String, Target> call2() {
}

JavaClasses classes = new ClassFileImporter().importClasses(Target.class, Caller.class);
Set<JavaMethodCall> calls = classes.get(Caller.class).getMethodCallsFromSelf();
Set<JavaMethodCall> calls = getMethodCallsFromClassWithoutAutomaticNullCheck(classes.get(Caller.class));
assertThat(calls).hasSize(5);

assertThat(filterOriginByName(calls, "call1"))
Expand Down Expand Up @@ -361,10 +362,10 @@ Supplier<String> call() {

JavaClasses classes = new ClassFileImporter().importClasses(Target.class, Caller1.class, Caller2.class);

JavaMethodCall call1 = getOnlyElement(classes.get(Caller1.class).getMethodCallsFromSelf());
JavaMethodCall call1 = getOnlyElement(getMethodCallsFromClassWithoutAutomaticNullCheck(classes.get(Caller1.class)));
assertThatCall(call1).isFrom("call").isTo(Target.class, "target");

JavaMethodCall call2 = getOnlyElement(classes.get(Caller2.class).getMethodCallsFromSelf());
JavaMethodCall call2 = getOnlyElement(getMethodCallsFromClassWithoutAutomaticNullCheck(classes.get(Caller2.class)));
assertThatCall(call2).isFrom("call").isTo(Target.class, "target");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@
import java.net.JarURLConnection;
import java.net.URLConnection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.jar.JarFile;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaCodeUnit;
import com.tngtech.archunit.core.domain.JavaField;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.domain.JavaMethodCall;
import com.tngtech.archunit.core.domain.properties.HasName;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.getFirst;
import static com.tngtech.archunit.testutil.TestUtils.urlOf;
import static java.util.stream.Collectors.toSet;

class ClassFileImporterTestUtils {

Expand Down Expand Up @@ -64,4 +67,19 @@ static JarFile jarFileOf(Class<?> clazzInJar) throws IOException {
checkArgument(connection instanceof JarURLConnection, "Class %s is not contained in a JAR", clazzInJar.getName());
return ((JarURLConnection) connection).getJarFile();
}

/**
* @return {@link JavaClass#getMethodCallsFromSelf()} excluding {@link Objects#requireNonNull(Object)} calls
* from the constructor, which the JDK 25+ compiler automatically emits in inner classes,
* to normalize test results over multiple JDKs
* @see <a href="https://inside.java/2025/04/04/quality-heads-up/">New Null Checks in Inner Class Constructors</a>
*/
static Set<JavaMethodCall> getMethodCallsFromClassWithoutAutomaticNullCheck(JavaClass javaClass) {
return javaClass.getMethodCallsFromSelf().stream()
.filter(call -> {
return !(call.getOrigin().isConstructor() && call.getOriginOwner().isInnerClass() &&
call.getTargetOwner().isEquivalentTo(Objects.class) && call.getTarget().getName().equals("requireNonNull"));
})
.collect(toSet());
}
}
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ ext {
]

minSupportedJavaVersion = JavaVersion.VERSION_1_8
maxSupportedJavaVersion = JavaVersion.VERSION_21
maxSupportedJavaVersion = JavaVersion.VERSION_25
isTestBuild = project.hasProperty('testJavaVersion')
configuredTestJavaVersion = project.findProperty('testJavaVersion')?.toString()?.with { JavaVersion.toVersion(it) }
assert configuredTestJavaVersion <= maxSupportedJavaVersion:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ tasks.withType(AbstractPublishToMaven) {
publishing {
publications {
mavenJava(MavenPublication) {
artifactId = project.archivesBaseName
artifactId = project.name
from components.java
pom {
name = app.name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ class VersionSpec {
JavaVersion.VERSION_1_8,
JavaVersion.VERSION_11,
JavaVersion.VERSION_17,
JavaVersion.VERSION_21
JavaVersion.VERSION_21,
JavaVersion.VERSION_25,
] as SortedSet

JavaVersion desiredJdkVersion
Expand Down Expand Up @@ -43,11 +44,10 @@ afterEvaluate {
toolchain {
languageVersion = versionSpec.desiredJavaLanguageVersion
}
sourceCompatibility = versionSpec.desiredSourceCompatibility
targetCompatibility = versionSpec.desiredSourceCompatibility
}

sourceCompatibility = versionSpec.desiredSourceCompatibility
targetCompatibility = versionSpec.desiredSourceCompatibility

tasks.withType(JavaCompile) { Task task ->
VersionSpec taskVersionSpec = versionSpec.withLowerLtsVersionBoundCompatibleWith(getMinTaskJavaVersion(task))
println "Task ${task.name} configured: ${taskVersionSpec.describe()}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ dependencies {
def parseLicenseInfoFrom = { config ->
def pom = config.resolve().find { it.name.endsWith('.pom') }

def projectNode = new XmlParser().parse(pom)
def projectNode = new groovy.xml.XmlParser().parse(pom)
def licenses = projectNode.licenses.license
assert licenses.size() == 1: 'Can only handle one declared license at the moment'

Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
6 changes: 4 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you drop the checksum intentionally?
according to https://gradle.org/release-checksums/ it is now for binary version 9.4.1 2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
and for the wrapper 55243ef57851f12b070ad14f7f5bb8302daceeebc5bce5ece5fa6edb23e1145c (I verified that your new wrapper file has the correct checksum)

If we actually check it somewhere in the pipeline, I'd prefer to keep it, otherwise, deletion is fine

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just did the update with

./gradlew wrapper --gradle-version latest && ./gradlew wrapper

and will now add the checksum, thanks for the hint! (Future updates can hopefully be done automatically by https://github.com/TNG/ArchUnit/actions/workflows/update-gradle-wrapper.yml again.)

I remember from gradle/actions#12 that the setup-gradle action was said to validate the wrapper.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionSha256Sum=553c78f50dafcd54d65b9a444649057857469edf836431389695608536d6b746
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
7 changes: 2 additions & 5 deletions gradlew

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading