Browse Source

Extend ArchitectureCheck with NullMarkedExtension

Introduce NullMarkedExtension for ArchitectureCheck, which provides
functionality to configure packages to ignore in nullability checks and
to enable or disable the extension.

See gh-47596

Signed-off-by: Dmytro Nosan <dimanosan@gmail.com>
pull/47637/head
Dmytro Nosan 2 months ago committed by Stéphane Nicoll
parent
commit
bc2ca5b9fc
  1. 10
      buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java
  2. 55
      buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheckExtension.java
  3. 3
      buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitecturePlugin.java
  4. 9
      buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java
  5. 74
      buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java
  6. 5
      cli/spring-boot-cli/build.gradle
  7. 4
      configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle
  8. 5
      configuration-metadata/spring-boot-configuration-metadata/build.gradle
  9. 5
      configuration-metadata/spring-boot-configuration-processor/build.gradle
  10. 4
      core/spring-boot-autoconfigure-processor/build.gradle
  11. 4
      documentation/spring-boot-docs/build.gradle
  12. 4
      loader/spring-boot-loader/build.gradle
  13. 4
      smoke-test/spring-boot-smoke-test-webflux-coroutines/build.gradle
  14. 4
      test-support/spring-boot-docker-test-support/build.gradle
  15. 4
      test-support/spring-boot-gradle-test-support/build.gradle
  16. 4
      test-support/spring-boot-test-support/build.gradle

10
buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java

@ -44,6 +44,7 @@ import org.gradle.api.file.FileTree;
import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property; import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider; import org.gradle.api.provider.Provider;
import org.gradle.api.provider.SetProperty;
import org.gradle.api.tasks.Classpath; import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.IgnoreEmptyDirectories; import org.gradle.api.tasks.IgnoreEmptyDirectories;
import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Input;
@ -80,8 +81,8 @@ public abstract class ArchitectureCheck extends DefaultTask {
getRules().addAll(ArchitectureRules.standard()); getRules().addAll(ArchitectureRules.standard());
getRules().addAll(whenMainSources( getRules().addAll(whenMainSources(
() -> Collections.singletonList(ArchitectureRules.allBeanMethodsShouldReturnNonPrivateType()))); () -> Collections.singletonList(ArchitectureRules.allBeanMethodsShouldReturnNonPrivateType())));
getRules().addAll(and(getNullMarked(), isMainSourceSet()).map(whenTrue( getRules().addAll(and(getNullMarkedEnabled(), isMainSourceSet()).map(whenTrue(() -> Collections.singletonList(
() -> Collections.singletonList(ArchitectureRules.packagesShouldBeAnnotatedWithNullMarked())))); ArchitectureRules.packagesShouldBeAnnotatedWithNullMarked(getNullMarkedIgnoredPackages().get())))));
getRuleDescriptions().set(getRules().map(this::asDescriptions)); getRuleDescriptions().set(getRules().map(this::asDescriptions));
} }
@ -196,6 +197,9 @@ public abstract class ArchitectureCheck extends DefaultTask {
abstract ListProperty<String> getRuleDescriptions(); abstract ListProperty<String> getRuleDescriptions();
@Internal @Internal
abstract Property<Boolean> getNullMarked(); abstract Property<Boolean> getNullMarkedEnabled();
@Internal
abstract SetProperty<String> getNullMarkedIgnoredPackages();
} }

55
buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheckExtension.java

@ -16,8 +16,14 @@
package org.springframework.boot.build.architecture; 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.gradle.api.provider.Property;
import org.jspecify.annotations.NullMarked; import org.gradle.api.provider.SetProperty;
/** /**
* Extension to configure the {@link ArchitecturePlugin}. * Extension to configure the {@link ArchitecturePlugin}.
@ -26,14 +32,51 @@ import org.jspecify.annotations.NullMarked;
*/ */
public abstract class ArchitectureCheckExtension { public abstract class ArchitectureCheckExtension {
public ArchitectureCheckExtension() { private final NullMarkedExtension nullMarked;
getNullMarked().convention(true);
@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<? super NullMarkedExtension> action) {
action.execute(this.nullMarked);
} }
/** /**
* Whether this project uses JSpecify's {@link NullMarked} annotations. * Extension to configure the {@code NullMarked} extension.
* @return whether this project uses JSpecify's @NullMarked annotations
*/ */
public abstract Property<Boolean> 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<Boolean> getEnabled();
/**
* Packages that should be ignored by the {@code NullMarked} checker.
* @return the ignored packages
*/
public abstract SetProperty<String> getIgnoredPackages();
}
} }

3
buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitecturePlugin.java

@ -59,7 +59,8 @@ public class ArchitecturePlugin implements Plugin<Project> {
task.setDescription("Checks the architecture of the classes of the " + sourceSet.getName() task.setDescription("Checks the architecture of the classes of the " + sourceSet.getName()
+ " source set."); + " source set.");
task.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP); 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); packageTangleChecks.add(checkPackageTangles);
} }

9
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 String TEST_AUTOCONFIGURATION_ANNOTATION = "org.springframework.boot.test.autoconfigure.TestAutoConfiguration";
private static final Predicate<JavaPackage> 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() { private ArchitectureRules() {
} }
@ -262,8 +257,8 @@ final class ArchitectureRules {
.allowEmptyShould(true); .allowEmptyShould(true);
} }
static ArchRule packagesShouldBeAnnotatedWithNullMarked() { static ArchRule packagesShouldBeAnnotatedWithNullMarked(Set<String> ignoredPackages) {
return ArchRuleDefinition.all(packages(NULL_MARKED_PACKAGE_FILTER)) return ArchRuleDefinition.all(packages((javaPackage) -> !ignoredPackages.contains(javaPackage.getName())))
.should(beAnnotatedWithNullMarked()) .should(beAnnotatedWithNullMarked())
.allowEmptyShould(true); .allowEmptyShould(true);
} }

74
buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java

@ -26,6 +26,8 @@ import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSet;
import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.BuildResult;
@ -40,6 +42,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.EnumSource;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.FileSystemUtils; import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -65,7 +68,7 @@ class ArchitectureCheckTests {
@BeforeEach @BeforeEach
void setup(@TempDir Path projectDir) { void setup(@TempDir Path projectDir) {
this.gradleBuild = new GradleBuild(projectDir).withNullMarked(false); this.gradleBuild = new GradleBuild(projectDir).withNullMarkedEnabled(false);
} }
@ParameterizedTest(name = "{0}") @ParameterizedTest(name = "{0}")
@ -275,14 +278,23 @@ class ArchitectureCheckTests {
@Test @Test
void whenPackageIsNotAnnotatedWithNullMarkedWithMainSourcesShouldFailAndWriteEmptyReport() throws IOException { void whenPackageIsNotAnnotatedWithNullMarkedWithMainSourcesShouldFailAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "nullmarked/notannotated"); 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"); "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 @Test
void whenPackageIsNotAnnotatedWithNullMarkedWithTestSourcesShouldSucceedAndWriteEmptyReport() throws IOException { void whenPackageIsNotAnnotatedWithNullMarkedWithTestSourcesShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_TEST, "nullmarked/notannotated"); 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 @Test
@ -386,7 +398,7 @@ class ArchitectureCheckTests {
private final Map<Task, Boolean> prohibitObjectsRequireNonNull = new LinkedHashMap<>(); private final Map<Task, Boolean> prohibitObjectsRequireNonNull = new LinkedHashMap<>();
private Boolean nullMarked; private NullMarkedExtension nullMarkedExtension;
private GradleBuild(Path projectDir) { private GradleBuild(Path projectDir) {
this.projectDir = projectDir; this.projectDir = projectDir;
@ -396,16 +408,29 @@ class ArchitectureCheckTests {
return this.projectDir; return this.projectDir;
} }
GradleBuild withNullMarked(Boolean nullMarked) { GradleBuild withProhibitObjectsRequireNonNull(Task task, boolean prohibitObjectsRequireNonNull) {
this.nullMarked = nullMarked; this.prohibitObjectsRequireNonNull.put(task, prohibitObjectsRequireNonNull);
return this;
}
GradleBuild withNullMarkedEnabled(Boolean enabled) {
configureNullMarkedExtension((nullMarked) -> nullMarked.withEnabled(enabled));
return this; return this;
} }
GradleBuild withProhibitObjectsRequireNonNull(Task task, boolean prohibitObjectsRequireNonNull) { GradleBuild withNullMarkedIgnoredPackages(String... ignorePackages) {
this.prohibitObjectsRequireNonNull.put(task, prohibitObjectsRequireNonNull); configureNullMarkedExtension((nullMarked) -> nullMarked.withIgnoredPackages(ignorePackages));
return this; return this;
} }
private void configureNullMarkedExtension(UnaryOperator<NullMarkedExtension> configurer) {
NullMarkedExtension nullMarkedExtension = this.nullMarkedExtension;
if (nullMarkedExtension == null) {
nullMarkedExtension = new NullMarkedExtension(null, null);
}
this.nullMarkedExtension = configurer.apply(nullMarkedExtension);
}
GradleBuild withDependencies(String... dependencies) { GradleBuild withDependencies(String... dependencies) {
this.dependencies.addAll(Arrays.asList(dependencies)); this.dependencies.addAll(Arrays.asList(dependencies));
return this; return this;
@ -444,11 +469,22 @@ class ArchitectureCheckTests {
.append(" prohibitObjectsRequireNonNull = ") .append(" prohibitObjectsRequireNonNull = ")
.append(prohibitObjectsRequireNonNull) .append(prohibitObjectsRequireNonNull)
.append("\n}\n\n")); .append("\n}\n\n"));
if (this.nullMarked != null) { NullMarkedExtension nullMarkedExtension = this.nullMarkedExtension;
buildFile.append("architectureCheck {\n") if (nullMarkedExtension != null) {
.append(" nullMarked = ") buildFile.append("architectureCheck {");
.append(this.nullMarked) buildFile.append("\n nullMarked {");
.append("\n}\n"); 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); Files.writeString(this.projectDir.resolve("build.gradle"), buildFile, StandardCharsets.UTF_8);
return GradleRunner.create() return GradleRunner.create()
@ -457,6 +493,18 @@ class ArchitectureCheckTests {
.withPluginClasspath(); .withPluginClasspath();
} }
private record NullMarkedExtension(Boolean enabled, Set<String> 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)));
}
}
} }
} }

5
cli/spring-boot-cli/build.gradle

@ -62,7 +62,10 @@ dependencies {
} }
architectureCheck { architectureCheck {
nullMarked = false nullMarked {
enabled = false
ignoredPackages = ['org.springframework.boot.cli.json']
}
} }
tasks.register("fullJar", Jar) { tasks.register("fullJar", Jar) {

4
configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle

@ -39,7 +39,9 @@ dependencies {
} }
architectureCheck { architectureCheck {
nullMarked = false nullMarked {
enabled = false
}
} }
def dependenciesOf(String version) { def dependenciesOf(String version) {

5
configuration-metadata/spring-boot-configuration-metadata/build.gradle

@ -36,5 +36,8 @@ dependencies {
} }
architectureCheck { architectureCheck {
nullMarked = false nullMarked {
enabled = false
ignoredPackages = ["org.springframework.boot.configurationmetadata.json"]
}
} }

5
configuration-metadata/spring-boot-configuration-processor/build.gradle

@ -31,7 +31,10 @@ sourceSets {
} }
architectureCheck { architectureCheck {
nullMarked = false nullMarked {
enabled = false
ignoredPackages = ["org.springframework.boot.configurationprocessor.json"]
}
} }
dependencies { dependencies {

4
core/spring-boot-autoconfigure-processor/build.gradle

@ -28,5 +28,7 @@ dependencies {
} }
architectureCheck { architectureCheck {
nullMarked = false nullMarked {
enabled = false
}
} }

4
documentation/spring-boot-docs/build.gradle

@ -71,7 +71,9 @@ tasks.named('compileKotlin', KotlinCompilationTask.class) {
} }
architectureCheck { architectureCheck {
nullMarked = false nullMarked {
enabled = false
}
} }
plugins.withType(EclipsePlugin) { plugins.withType(EclipsePlugin) {

4
loader/spring-boot-loader/build.gradle

@ -38,5 +38,7 @@ tasks.configureEach {
} }
architectureCheck { architectureCheck {
nullMarked = false nullMarked {
enabled = false
}
} }

4
smoke-test/spring-boot-smoke-test-webflux-coroutines/build.gradle

@ -34,5 +34,7 @@ dependencies {
} }
architectureCheck { architectureCheck {
nullMarked = false nullMarked {
enabled = false
}
} }

4
test-support/spring-boot-docker-test-support/build.gradle

@ -52,5 +52,7 @@ dependencies {
} }
architectureCheck { architectureCheck {
nullMarked = false nullMarked {
enabled = false
}
} }

4
test-support/spring-boot-gradle-test-support/build.gradle

@ -32,5 +32,7 @@ dependencies {
} }
architectureCheck { architectureCheck {
nullMarked = false nullMarked {
enabled = false
}
} }

4
test-support/spring-boot-test-support/build.gradle

@ -63,5 +63,7 @@ dependencies {
} }
architectureCheck { architectureCheck {
nullMarked = false nullMarked {
enabled = false
}
} }

Loading…
Cancel
Save