diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java index bd0cca192657..9be2873ce719 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java @@ -44,6 +44,7 @@ import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; import org.gradle.api.provider.Provider; +import org.gradle.api.provider.SetProperty; import org.gradle.api.tasks.Classpath; import org.gradle.api.tasks.IgnoreEmptyDirectories; import org.gradle.api.tasks.Input; @@ -80,8 +81,8 @@ public ArchitectureCheck() { getRules().addAll(ArchitectureRules.standard()); getRules().addAll(whenMainSources( () -> Collections.singletonList(ArchitectureRules.allBeanMethodsShouldReturnNonPrivateType()))); - getRules().addAll(and(getNullMarked(), isMainSourceSet()).map(whenTrue( - () -> Collections.singletonList(ArchitectureRules.packagesShouldBeAnnotatedWithNullMarked())))); + getRules().addAll(and(getNullMarkedEnabled(), isMainSourceSet()).map(whenTrue(() -> Collections.singletonList( + ArchitectureRules.packagesShouldBeAnnotatedWithNullMarked(getNullMarkedIgnoredPackages().get()))))); getRuleDescriptions().set(getRules().map(this::asDescriptions)); } @@ -196,6 +197,9 @@ final FileTree getInputClasses() { abstract ListProperty getRuleDescriptions(); @Internal - abstract Property getNullMarked(); + abstract Property getNullMarkedEnabled(); + + @Internal + abstract SetProperty getNullMarkedIgnoredPackages(); } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheckExtension.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheckExtension.java index bc927f609e07..1f9ba7f1c0cf 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheckExtension.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheckExtension.java @@ -16,8 +16,14 @@ package org.springframework.boot.build.architecture; +import java.util.LinkedHashSet; + +import javax.inject.Inject; + +import org.gradle.api.Action; +import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.Property; -import org.jspecify.annotations.NullMarked; +import org.gradle.api.provider.SetProperty; /** * Extension to configure the {@link ArchitecturePlugin}. @@ -26,14 +32,51 @@ */ public abstract class ArchitectureCheckExtension { - public ArchitectureCheckExtension() { - getNullMarked().convention(true); + private final NullMarkedExtension nullMarked; + + @Inject + public ArchitectureCheckExtension(ObjectFactory objects) { + this.nullMarked = objects.newInstance(NullMarkedExtension.class); + } + + /** + * Get the {@code NullMarked} extension. + * @return the {@code NullMarked} extension + */ + public NullMarkedExtension getNullMarked() { + return this.nullMarked; + } + + /** + * Configure the {@code NullMarked} extension. + * @param action the action to configure the {@code NullMarked} extension with + */ + public void nullMarked(Action action) { + action.execute(this.nullMarked); } /** - * Whether this project uses JSpecify's {@link NullMarked} annotations. - * @return whether this project uses JSpecify's @NullMarked annotations + * Extension to configure the {@code NullMarked} extension. */ - public abstract Property getNullMarked(); + public abstract static class NullMarkedExtension { + + public NullMarkedExtension() { + getEnabled().convention(true); + getIgnoredPackages().convention(new LinkedHashSet<>()); + } + + /** + * Whether this project uses JSpecify's {@code NullMarked} annotations. + * @return whether this project uses JSpecify's @NullMarked annotations + */ + public abstract Property getEnabled(); + + /** + * Packages that should be ignored by the {@code NullMarked} checker. + * @return the ignored packages + */ + public abstract SetProperty getIgnoredPackages(); + + } } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitecturePlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitecturePlugin.java index 390bc19108e0..2433fab9800a 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitecturePlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitecturePlugin.java @@ -59,7 +59,8 @@ private void registerTasks(Project project, ArchitectureCheckExtension extension task.setDescription("Checks the architecture of the classes of the " + sourceSet.getName() + " source set."); task.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP); - task.getNullMarked().set(extension.getNullMarked()); + task.getNullMarkedEnabled().set(extension.getNullMarked().getEnabled()); + task.getNullMarkedIgnoredPackages().set(extension.getNullMarked().getIgnoredPackages()); }); packageTangleChecks.add(checkPackageTangles); } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java index 6183e07b6e31..b89d79a0b870 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java @@ -82,11 +82,6 @@ final class ArchitectureRules { private static final String TEST_AUTOCONFIGURATION_ANNOTATION = "org.springframework.boot.test.autoconfigure.TestAutoConfiguration"; - private static final Predicate NULL_MARKED_PACKAGE_FILTER = (candidate) -> !List - .of("org.springframework.boot.cli.json", "org.springframework.boot.configurationmetadata.json", - "org.springframework.boot.configurationprocessor.json") - .contains(candidate.getName()); - private ArchitectureRules() { } @@ -262,8 +257,8 @@ private static ArchRule conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsT .allowEmptyShould(true); } - static ArchRule packagesShouldBeAnnotatedWithNullMarked() { - return ArchRuleDefinition.all(packages(NULL_MARKED_PACKAGE_FILTER)) + static ArchRule packagesShouldBeAnnotatedWithNullMarked(Set ignoredPackages) { + return ArchRuleDefinition.all(packages((javaPackage) -> !ignoredPackages.contains(javaPackage.getName()))) .should(beAnnotatedWithNullMarked()) .allowEmptyShould(true); } diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java index dc7b2f9f09e4..96c4bfa9d2ec 100644 --- a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java @@ -26,6 +26,8 @@ import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; import org.gradle.api.tasks.SourceSet; import org.gradle.testkit.runner.BuildResult; @@ -40,6 +42,7 @@ import org.junit.jupiter.params.provider.EnumSource; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.FileSystemUtils; import org.springframework.util.StringUtils; @@ -65,7 +68,7 @@ class ArchitectureCheckTests { @BeforeEach void setup(@TempDir Path projectDir) { - this.gradleBuild = new GradleBuild(projectDir).withNullMarked(false); + this.gradleBuild = new GradleBuild(projectDir).withNullMarkedEnabled(false); } @ParameterizedTest(name = "{0}") @@ -275,14 +278,23 @@ void whenBeanMethodExposesNonPrivateTypeShouldSucceedAndWriteEmptyReport(Task ta @Test void whenPackageIsNotAnnotatedWithNullMarkedWithMainSourcesShouldFailAndWriteEmptyReport() throws IOException { prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "nullmarked/notannotated"); - buildAndFail(this.gradleBuild.withNullMarked(true), Task.CHECK_ARCHITECTURE_MAIN, + buildAndFail(this.gradleBuild.withNullMarkedEnabled(true), Task.CHECK_ARCHITECTURE_MAIN, "Package org.springframework.boot.build.architecture.nullmarked.notannotated is not annotated with @NullMarked"); } + @Test + void whenPackageIsIgnoredAndNotAnnotatedWithNullMarkedWithMainSourcesShouldSucceedAndWriteEmptyReport() + throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "nullmarked/notannotated"); + build(this.gradleBuild.withNullMarkedEnabled(true) + .withNullMarkedIgnoredPackages("org.springframework.boot.build.architecture.nullmarked.notannotated"), + Task.CHECK_ARCHITECTURE_MAIN); + } + @Test void whenPackageIsNotAnnotatedWithNullMarkedWithTestSourcesShouldSucceedAndWriteEmptyReport() throws IOException { prepareTask(Task.CHECK_ARCHITECTURE_TEST, "nullmarked/notannotated"); - build(this.gradleBuild.withNullMarked(true), Task.CHECK_ARCHITECTURE_TEST); + build(this.gradleBuild.withNullMarkedEnabled(true), Task.CHECK_ARCHITECTURE_TEST); } @Test @@ -386,7 +398,7 @@ private static final class GradleBuild { private final Map prohibitObjectsRequireNonNull = new LinkedHashMap<>(); - private Boolean nullMarked; + private NullMarkedExtension nullMarkedExtension; private GradleBuild(Path projectDir) { this.projectDir = projectDir; @@ -396,16 +408,29 @@ Path getProjectDir() { return this.projectDir; } - GradleBuild withNullMarked(Boolean nullMarked) { - this.nullMarked = nullMarked; + GradleBuild withProhibitObjectsRequireNonNull(Task task, boolean prohibitObjectsRequireNonNull) { + this.prohibitObjectsRequireNonNull.put(task, prohibitObjectsRequireNonNull); + return this; + } + + GradleBuild withNullMarkedEnabled(Boolean enabled) { + configureNullMarkedExtension((nullMarked) -> nullMarked.withEnabled(enabled)); return this; } - GradleBuild withProhibitObjectsRequireNonNull(Task task, boolean prohibitObjectsRequireNonNull) { - this.prohibitObjectsRequireNonNull.put(task, prohibitObjectsRequireNonNull); + GradleBuild withNullMarkedIgnoredPackages(String... ignorePackages) { + configureNullMarkedExtension((nullMarked) -> nullMarked.withIgnoredPackages(ignorePackages)); return this; } + private void configureNullMarkedExtension(UnaryOperator configurer) { + NullMarkedExtension nullMarkedExtension = this.nullMarkedExtension; + if (nullMarkedExtension == null) { + nullMarkedExtension = new NullMarkedExtension(null, null); + } + this.nullMarkedExtension = configurer.apply(nullMarkedExtension); + } + GradleBuild withDependencies(String... dependencies) { this.dependencies.addAll(Arrays.asList(dependencies)); return this; @@ -444,11 +469,22 @@ private GradleRunner prepareRunner(String... arguments) throws IOException { .append(" prohibitObjectsRequireNonNull = ") .append(prohibitObjectsRequireNonNull) .append("\n}\n\n")); - if (this.nullMarked != null) { - buildFile.append("architectureCheck {\n") - .append(" nullMarked = ") - .append(this.nullMarked) - .append("\n}\n"); + NullMarkedExtension nullMarkedExtension = this.nullMarkedExtension; + if (nullMarkedExtension != null) { + buildFile.append("architectureCheck {"); + buildFile.append("\n nullMarked {"); + if (nullMarkedExtension.enabled() != null) { + buildFile.append("\n enabled = ").append(nullMarkedExtension.enabled()); + } + if (!CollectionUtils.isEmpty(nullMarkedExtension.ignoredPackages())) { + buildFile.append("\n ignoredPackages = ") + .append(nullMarkedExtension.ignoredPackages() + .stream() + .map(StringUtils::quote) + .collect(Collectors.joining(",", "[", "]"))); + } + buildFile.append("\n }"); + buildFile.append("\n}\n\n"); } Files.writeString(this.projectDir.resolve("build.gradle"), buildFile, StandardCharsets.UTF_8); return GradleRunner.create() @@ -457,6 +493,18 @@ private GradleRunner prepareRunner(String... arguments) throws IOException { .withPluginClasspath(); } + private record NullMarkedExtension(Boolean enabled, Set ignoredPackages) { + + private NullMarkedExtension withEnabled(Boolean enabled) { + return new NullMarkedExtension(enabled, this.ignoredPackages); + } + + private NullMarkedExtension withIgnoredPackages(String... ignoredPackages) { + return new NullMarkedExtension(this.enabled, new LinkedHashSet<>(Arrays.asList(ignoredPackages))); + } + + } + } } diff --git a/cli/spring-boot-cli/build.gradle b/cli/spring-boot-cli/build.gradle index 1667fbfeef57..b0ea8b54de2c 100644 --- a/cli/spring-boot-cli/build.gradle +++ b/cli/spring-boot-cli/build.gradle @@ -62,7 +62,10 @@ dependencies { } architectureCheck { - nullMarked = false + nullMarked { + enabled = false + ignoredPackages = ['org.springframework.boot.cli.json'] + } } tasks.register("fullJar", Jar) { diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle index 409d933ec547..2a00c14df969 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle @@ -39,7 +39,9 @@ dependencies { } architectureCheck { - nullMarked = false + nullMarked { + enabled = false + } } def dependenciesOf(String version) { diff --git a/configuration-metadata/spring-boot-configuration-metadata/build.gradle b/configuration-metadata/spring-boot-configuration-metadata/build.gradle index ecc899004f06..780ddec1e7ba 100644 --- a/configuration-metadata/spring-boot-configuration-metadata/build.gradle +++ b/configuration-metadata/spring-boot-configuration-metadata/build.gradle @@ -36,5 +36,8 @@ dependencies { } architectureCheck { - nullMarked = false + nullMarked { + enabled = false + ignoredPackages = ["org.springframework.boot.configurationmetadata.json"] + } } diff --git a/configuration-metadata/spring-boot-configuration-processor/build.gradle b/configuration-metadata/spring-boot-configuration-processor/build.gradle index 0e3a12a78e0f..67d993a56c3b 100644 --- a/configuration-metadata/spring-boot-configuration-processor/build.gradle +++ b/configuration-metadata/spring-boot-configuration-processor/build.gradle @@ -31,7 +31,10 @@ sourceSets { } architectureCheck { - nullMarked = false + nullMarked { + enabled = false + ignoredPackages = ["org.springframework.boot.configurationprocessor.json"] + } } dependencies { diff --git a/core/spring-boot-autoconfigure-processor/build.gradle b/core/spring-boot-autoconfigure-processor/build.gradle index 3d6792d1f033..65c09aaec2bc 100644 --- a/core/spring-boot-autoconfigure-processor/build.gradle +++ b/core/spring-boot-autoconfigure-processor/build.gradle @@ -28,5 +28,7 @@ dependencies { } architectureCheck { - nullMarked = false + nullMarked { + enabled = false + } } diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index 865e9bb6dc50..59a828fcba07 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -71,7 +71,9 @@ tasks.named('compileKotlin', KotlinCompilationTask.class) { } architectureCheck { - nullMarked = false + nullMarked { + enabled = false + } } plugins.withType(EclipsePlugin) { diff --git a/loader/spring-boot-loader/build.gradle b/loader/spring-boot-loader/build.gradle index e67b72d4fc03..5fbc9f19e505 100644 --- a/loader/spring-boot-loader/build.gradle +++ b/loader/spring-boot-loader/build.gradle @@ -38,5 +38,7 @@ tasks.configureEach { } architectureCheck { - nullMarked = false + nullMarked { + enabled = false + } } diff --git a/smoke-test/spring-boot-smoke-test-webflux-coroutines/build.gradle b/smoke-test/spring-boot-smoke-test-webflux-coroutines/build.gradle index 95badc7e2beb..f465b9445330 100644 --- a/smoke-test/spring-boot-smoke-test-webflux-coroutines/build.gradle +++ b/smoke-test/spring-boot-smoke-test-webflux-coroutines/build.gradle @@ -34,5 +34,7 @@ dependencies { } architectureCheck { - nullMarked = false + nullMarked { + enabled = false + } } diff --git a/test-support/spring-boot-docker-test-support/build.gradle b/test-support/spring-boot-docker-test-support/build.gradle index 1773b749c0a0..cf4cb12aa98c 100644 --- a/test-support/spring-boot-docker-test-support/build.gradle +++ b/test-support/spring-boot-docker-test-support/build.gradle @@ -52,5 +52,7 @@ dependencies { } architectureCheck { - nullMarked = false + nullMarked { + enabled = false + } } diff --git a/test-support/spring-boot-gradle-test-support/build.gradle b/test-support/spring-boot-gradle-test-support/build.gradle index 8c5831091c69..33a281ea384c 100644 --- a/test-support/spring-boot-gradle-test-support/build.gradle +++ b/test-support/spring-boot-gradle-test-support/build.gradle @@ -32,5 +32,7 @@ dependencies { } architectureCheck { - nullMarked = false + nullMarked { + enabled = false + } } diff --git a/test-support/spring-boot-test-support/build.gradle b/test-support/spring-boot-test-support/build.gradle index 5d4d60194020..40b7cd6562a2 100644 --- a/test-support/spring-boot-test-support/build.gradle +++ b/test-support/spring-boot-test-support/build.gradle @@ -63,5 +63,7 @@ dependencies { } architectureCheck { - nullMarked = false + nullMarked { + enabled = false + } }