Skip to content

Commit bc2ca5b

Browse files
nosansnicoll
authored andcommitted
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 <[email protected]>
1 parent a043867 commit bc2ca5b

File tree

16 files changed

+157
-41
lines changed

16 files changed

+157
-41
lines changed

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.gradle.api.provider.ListProperty;
4545
import org.gradle.api.provider.Property;
4646
import org.gradle.api.provider.Provider;
47+
import org.gradle.api.provider.SetProperty;
4748
import org.gradle.api.tasks.Classpath;
4849
import org.gradle.api.tasks.IgnoreEmptyDirectories;
4950
import org.gradle.api.tasks.Input;
@@ -80,8 +81,8 @@ public ArchitectureCheck() {
8081
getRules().addAll(ArchitectureRules.standard());
8182
getRules().addAll(whenMainSources(
8283
() -> Collections.singletonList(ArchitectureRules.allBeanMethodsShouldReturnNonPrivateType())));
83-
getRules().addAll(and(getNullMarked(), isMainSourceSet()).map(whenTrue(
84-
() -> Collections.singletonList(ArchitectureRules.packagesShouldBeAnnotatedWithNullMarked()))));
84+
getRules().addAll(and(getNullMarkedEnabled(), isMainSourceSet()).map(whenTrue(() -> Collections.singletonList(
85+
ArchitectureRules.packagesShouldBeAnnotatedWithNullMarked(getNullMarkedIgnoredPackages().get())))));
8586
getRuleDescriptions().set(getRules().map(this::asDescriptions));
8687
}
8788

@@ -196,6 +197,9 @@ final FileTree getInputClasses() {
196197
abstract ListProperty<String> getRuleDescriptions();
197198

198199
@Internal
199-
abstract Property<Boolean> getNullMarked();
200+
abstract Property<Boolean> getNullMarkedEnabled();
201+
202+
@Internal
203+
abstract SetProperty<String> getNullMarkedIgnoredPackages();
200204

201205
}

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

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@
1616

1717
package org.springframework.boot.build.architecture;
1818

19+
import java.util.LinkedHashSet;
20+
21+
import javax.inject.Inject;
22+
23+
import org.gradle.api.Action;
24+
import org.gradle.api.model.ObjectFactory;
1925
import org.gradle.api.provider.Property;
20-
import org.jspecify.annotations.NullMarked;
26+
import org.gradle.api.provider.SetProperty;
2127

2228
/**
2329
* Extension to configure the {@link ArchitecturePlugin}.
@@ -26,14 +32,51 @@
2632
*/
2733
public abstract class ArchitectureCheckExtension {
2834

29-
public ArchitectureCheckExtension() {
30-
getNullMarked().convention(true);
35+
private final NullMarkedExtension nullMarked;
36+
37+
@Inject
38+
public ArchitectureCheckExtension(ObjectFactory objects) {
39+
this.nullMarked = objects.newInstance(NullMarkedExtension.class);
40+
}
41+
42+
/**
43+
* Get the {@code NullMarked} extension.
44+
* @return the {@code NullMarked} extension
45+
*/
46+
public NullMarkedExtension getNullMarked() {
47+
return this.nullMarked;
48+
}
49+
50+
/**
51+
* Configure the {@code NullMarked} extension.
52+
* @param action the action to configure the {@code NullMarked} extension with
53+
*/
54+
public void nullMarked(Action<? super NullMarkedExtension> action) {
55+
action.execute(this.nullMarked);
3156
}
3257

3358
/**
34-
* Whether this project uses JSpecify's {@link NullMarked} annotations.
35-
* @return whether this project uses JSpecify's @NullMarked annotations
59+
* Extension to configure the {@code NullMarked} extension.
3660
*/
37-
public abstract Property<Boolean> getNullMarked();
61+
public abstract static class NullMarkedExtension {
62+
63+
public NullMarkedExtension() {
64+
getEnabled().convention(true);
65+
getIgnoredPackages().convention(new LinkedHashSet<>());
66+
}
67+
68+
/**
69+
* Whether this project uses JSpecify's {@code NullMarked} annotations.
70+
* @return whether this project uses JSpecify's @NullMarked annotations
71+
*/
72+
public abstract Property<Boolean> getEnabled();
73+
74+
/**
75+
* Packages that should be ignored by the {@code NullMarked} checker.
76+
* @return the ignored packages
77+
*/
78+
public abstract SetProperty<String> getIgnoredPackages();
79+
80+
}
3881

3982
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ private void registerTasks(Project project, ArchitectureCheckExtension extension
5959
task.setDescription("Checks the architecture of the classes of the " + sourceSet.getName()
6060
+ " source set.");
6161
task.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
62-
task.getNullMarked().set(extension.getNullMarked());
62+
task.getNullMarkedEnabled().set(extension.getNullMarked().getEnabled());
63+
task.getNullMarkedIgnoredPackages().set(extension.getNullMarked().getIgnoredPackages());
6364
});
6465
packageTangleChecks.add(checkPackageTangles);
6566
}

buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,6 @@ final class ArchitectureRules {
8282

8383
private static final String TEST_AUTOCONFIGURATION_ANNOTATION = "org.springframework.boot.test.autoconfigure.TestAutoConfiguration";
8484

85-
private static final Predicate<JavaPackage> NULL_MARKED_PACKAGE_FILTER = (candidate) -> !List
86-
.of("org.springframework.boot.cli.json", "org.springframework.boot.configurationmetadata.json",
87-
"org.springframework.boot.configurationprocessor.json")
88-
.contains(candidate.getName());
89-
9085
private ArchitectureRules() {
9186
}
9287

@@ -262,8 +257,8 @@ private static ArchRule conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsT
262257
.allowEmptyShould(true);
263258
}
264259

265-
static ArchRule packagesShouldBeAnnotatedWithNullMarked() {
266-
return ArchRuleDefinition.all(packages(NULL_MARKED_PACKAGE_FILTER))
260+
static ArchRule packagesShouldBeAnnotatedWithNullMarked(Set<String> ignoredPackages) {
261+
return ArchRuleDefinition.all(packages((javaPackage) -> !ignoredPackages.contains(javaPackage.getName())))
267262
.should(beAnnotatedWithNullMarked())
268263
.allowEmptyShould(true);
269264
}

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

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import java.util.LinkedHashSet;
2727
import java.util.Map;
2828
import java.util.Set;
29+
import java.util.function.UnaryOperator;
30+
import java.util.stream.Collectors;
2931

3032
import org.gradle.api.tasks.SourceSet;
3133
import org.gradle.testkit.runner.BuildResult;
@@ -40,6 +42,7 @@
4042
import org.junit.jupiter.params.provider.EnumSource;
4143

4244
import org.springframework.util.ClassUtils;
45+
import org.springframework.util.CollectionUtils;
4346
import org.springframework.util.FileSystemUtils;
4447
import org.springframework.util.StringUtils;
4548

@@ -65,7 +68,7 @@ class ArchitectureCheckTests {
6568

6669
@BeforeEach
6770
void setup(@TempDir Path projectDir) {
68-
this.gradleBuild = new GradleBuild(projectDir).withNullMarked(false);
71+
this.gradleBuild = new GradleBuild(projectDir).withNullMarkedEnabled(false);
6972
}
7073

7174
@ParameterizedTest(name = "{0}")
@@ -275,14 +278,23 @@ void whenBeanMethodExposesNonPrivateTypeShouldSucceedAndWriteEmptyReport(Task ta
275278
@Test
276279
void whenPackageIsNotAnnotatedWithNullMarkedWithMainSourcesShouldFailAndWriteEmptyReport() throws IOException {
277280
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "nullmarked/notannotated");
278-
buildAndFail(this.gradleBuild.withNullMarked(true), Task.CHECK_ARCHITECTURE_MAIN,
281+
buildAndFail(this.gradleBuild.withNullMarkedEnabled(true), Task.CHECK_ARCHITECTURE_MAIN,
279282
"Package org.springframework.boot.build.architecture.nullmarked.notannotated is not annotated with @NullMarked");
280283
}
281284

285+
@Test
286+
void whenPackageIsIgnoredAndNotAnnotatedWithNullMarkedWithMainSourcesShouldSucceedAndWriteEmptyReport()
287+
throws IOException {
288+
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "nullmarked/notannotated");
289+
build(this.gradleBuild.withNullMarkedEnabled(true)
290+
.withNullMarkedIgnoredPackages("org.springframework.boot.build.architecture.nullmarked.notannotated"),
291+
Task.CHECK_ARCHITECTURE_MAIN);
292+
}
293+
282294
@Test
283295
void whenPackageIsNotAnnotatedWithNullMarkedWithTestSourcesShouldSucceedAndWriteEmptyReport() throws IOException {
284296
prepareTask(Task.CHECK_ARCHITECTURE_TEST, "nullmarked/notannotated");
285-
build(this.gradleBuild.withNullMarked(true), Task.CHECK_ARCHITECTURE_TEST);
297+
build(this.gradleBuild.withNullMarkedEnabled(true), Task.CHECK_ARCHITECTURE_TEST);
286298
}
287299

288300
@Test
@@ -386,7 +398,7 @@ private static final class GradleBuild {
386398

387399
private final Map<Task, Boolean> prohibitObjectsRequireNonNull = new LinkedHashMap<>();
388400

389-
private Boolean nullMarked;
401+
private NullMarkedExtension nullMarkedExtension;
390402

391403
private GradleBuild(Path projectDir) {
392404
this.projectDir = projectDir;
@@ -396,16 +408,29 @@ Path getProjectDir() {
396408
return this.projectDir;
397409
}
398410

399-
GradleBuild withNullMarked(Boolean nullMarked) {
400-
this.nullMarked = nullMarked;
411+
GradleBuild withProhibitObjectsRequireNonNull(Task task, boolean prohibitObjectsRequireNonNull) {
412+
this.prohibitObjectsRequireNonNull.put(task, prohibitObjectsRequireNonNull);
413+
return this;
414+
}
415+
416+
GradleBuild withNullMarkedEnabled(Boolean enabled) {
417+
configureNullMarkedExtension((nullMarked) -> nullMarked.withEnabled(enabled));
401418
return this;
402419
}
403420

404-
GradleBuild withProhibitObjectsRequireNonNull(Task task, boolean prohibitObjectsRequireNonNull) {
405-
this.prohibitObjectsRequireNonNull.put(task, prohibitObjectsRequireNonNull);
421+
GradleBuild withNullMarkedIgnoredPackages(String... ignorePackages) {
422+
configureNullMarkedExtension((nullMarked) -> nullMarked.withIgnoredPackages(ignorePackages));
406423
return this;
407424
}
408425

426+
private void configureNullMarkedExtension(UnaryOperator<NullMarkedExtension> configurer) {
427+
NullMarkedExtension nullMarkedExtension = this.nullMarkedExtension;
428+
if (nullMarkedExtension == null) {
429+
nullMarkedExtension = new NullMarkedExtension(null, null);
430+
}
431+
this.nullMarkedExtension = configurer.apply(nullMarkedExtension);
432+
}
433+
409434
GradleBuild withDependencies(String... dependencies) {
410435
this.dependencies.addAll(Arrays.asList(dependencies));
411436
return this;
@@ -444,11 +469,22 @@ private GradleRunner prepareRunner(String... arguments) throws IOException {
444469
.append(" prohibitObjectsRequireNonNull = ")
445470
.append(prohibitObjectsRequireNonNull)
446471
.append("\n}\n\n"));
447-
if (this.nullMarked != null) {
448-
buildFile.append("architectureCheck {\n")
449-
.append(" nullMarked = ")
450-
.append(this.nullMarked)
451-
.append("\n}\n");
472+
NullMarkedExtension nullMarkedExtension = this.nullMarkedExtension;
473+
if (nullMarkedExtension != null) {
474+
buildFile.append("architectureCheck {");
475+
buildFile.append("\n nullMarked {");
476+
if (nullMarkedExtension.enabled() != null) {
477+
buildFile.append("\n enabled = ").append(nullMarkedExtension.enabled());
478+
}
479+
if (!CollectionUtils.isEmpty(nullMarkedExtension.ignoredPackages())) {
480+
buildFile.append("\n ignoredPackages = ")
481+
.append(nullMarkedExtension.ignoredPackages()
482+
.stream()
483+
.map(StringUtils::quote)
484+
.collect(Collectors.joining(",", "[", "]")));
485+
}
486+
buildFile.append("\n }");
487+
buildFile.append("\n}\n\n");
452488
}
453489
Files.writeString(this.projectDir.resolve("build.gradle"), buildFile, StandardCharsets.UTF_8);
454490
return GradleRunner.create()
@@ -457,6 +493,18 @@ private GradleRunner prepareRunner(String... arguments) throws IOException {
457493
.withPluginClasspath();
458494
}
459495

496+
private record NullMarkedExtension(Boolean enabled, Set<String> ignoredPackages) {
497+
498+
private NullMarkedExtension withEnabled(Boolean enabled) {
499+
return new NullMarkedExtension(enabled, this.ignoredPackages);
500+
}
501+
502+
private NullMarkedExtension withIgnoredPackages(String... ignoredPackages) {
503+
return new NullMarkedExtension(this.enabled, new LinkedHashSet<>(Arrays.asList(ignoredPackages)));
504+
}
505+
506+
}
507+
460508
}
461509

462510
}

cli/spring-boot-cli/build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ dependencies {
6262
}
6363

6464
architectureCheck {
65-
nullMarked = false
65+
nullMarked {
66+
enabled = false
67+
ignoredPackages = ['org.springframework.boot.cli.json']
68+
}
6669
}
6770

6871
tasks.register("fullJar", Jar) {

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ dependencies {
3939
}
4040

4141
architectureCheck {
42-
nullMarked = false
42+
nullMarked {
43+
enabled = false
44+
}
4345
}
4446

4547
def dependenciesOf(String version) {

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,8 @@ dependencies {
3636
}
3737

3838
architectureCheck {
39-
nullMarked = false
39+
nullMarked {
40+
enabled = false
41+
ignoredPackages = ["org.springframework.boot.configurationmetadata.json"]
42+
}
4043
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ sourceSets {
3131
}
3232

3333
architectureCheck {
34-
nullMarked = false
34+
nullMarked {
35+
enabled = false
36+
ignoredPackages = ["org.springframework.boot.configurationprocessor.json"]
37+
}
3538
}
3639

3740
dependencies {

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,7 @@ dependencies {
2828
}
2929

3030
architectureCheck {
31-
nullMarked = false
31+
nullMarked {
32+
enabled = false
33+
}
3234
}

0 commit comments

Comments
 (0)