Check for @NullMarked on packages
Projects which don't have JSpecify nullability annotations can opt out by using architectureCheck { nullMarked = false } in their build.gradle script. See gh-46587
This commit is contained in:
parent
1a1ad23d60
commit
c211b88594
|
@ -67,6 +67,7 @@ import org.gradle.api.tasks.VerificationException;
|
||||||
* @author Ivan Malutin
|
* @author Ivan Malutin
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
* @author Dmytro Nosan
|
* @author Dmytro Nosan
|
||||||
|
* @author Moritz Halbritter
|
||||||
*/
|
*/
|
||||||
public abstract class ArchitectureCheck extends DefaultTask {
|
public abstract class ArchitectureCheck extends DefaultTask {
|
||||||
|
|
||||||
|
@ -79,13 +80,21 @@ 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(
|
||||||
|
() -> Collections.singletonList(ArchitectureRules.packagesShouldBeAnnotatedWithNullMarked()))));
|
||||||
getRuleDescriptions().set(getRules().map(this::asDescriptions));
|
getRuleDescriptions().set(getRules().map(this::asDescriptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Provider<Boolean> and(Provider<Boolean> provider1, Provider<Boolean> provider2) {
|
||||||
|
return provider1.zip(provider2, (result1, result2) -> result1 && result2);
|
||||||
|
}
|
||||||
|
|
||||||
private Provider<List<ArchRule>> whenMainSources(Supplier<List<ArchRule>> rules) {
|
private Provider<List<ArchRule>> whenMainSources(Supplier<List<ArchRule>> rules) {
|
||||||
return getSourceSet().convention(SourceSet.MAIN_SOURCE_SET_NAME)
|
return isMainSourceSet().map(whenTrue(rules));
|
||||||
.map(SourceSet.MAIN_SOURCE_SET_NAME::equals)
|
}
|
||||||
.map(whenTrue(rules));
|
|
||||||
|
private Provider<Boolean> isMainSourceSet() {
|
||||||
|
return getSourceSet().convention(SourceSet.MAIN_SOURCE_SET_NAME).map(SourceSet.MAIN_SOURCE_SET_NAME::equals);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Transformer<List<ArchRule>, Boolean> whenTrue(Supplier<List<ArchRule>> rules) {
|
private Transformer<List<ArchRule>, Boolean> whenTrue(Supplier<List<ArchRule>> rules) {
|
||||||
|
@ -186,4 +195,7 @@ public abstract class ArchitectureCheck extends DefaultTask {
|
||||||
@Input // Use descriptions as input since rules aren't serializable
|
@Input // Use descriptions as input since rules aren't serializable
|
||||||
abstract ListProperty<String> getRuleDescriptions();
|
abstract ListProperty<String> getRuleDescriptions();
|
||||||
|
|
||||||
|
@Internal
|
||||||
|
abstract Property<Boolean> getNullMarked();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-present the original author or authors.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* https://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.springframework.boot.build.architecture;
|
||||||
|
|
||||||
|
import org.gradle.api.provider.Property;
|
||||||
|
import org.jspecify.annotations.NullMarked;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension to configure the {@link ArchitecturePlugin}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
*/
|
||||||
|
public abstract class ArchitectureCheckExtension {
|
||||||
|
|
||||||
|
public ArchitectureCheckExtension() {
|
||||||
|
getNullMarked().convention(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this project uses JSpecify's {@link NullMarked} annotations.
|
||||||
|
* @return whether this project uses JSpecify's @NullMarked annotations
|
||||||
|
*/
|
||||||
|
public abstract Property<Boolean> getNullMarked();
|
||||||
|
|
||||||
|
}
|
|
@ -39,10 +39,12 @@ public class ArchitecturePlugin implements Plugin<Project> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void apply(Project project) {
|
public void apply(Project project) {
|
||||||
project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> registerTasks(project));
|
ArchitectureCheckExtension extension = project.getExtensions()
|
||||||
|
.create("architectureCheck", ArchitectureCheckExtension.class);
|
||||||
|
project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> registerTasks(project, extension));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerTasks(Project project) {
|
private void registerTasks(Project project, ArchitectureCheckExtension extension) {
|
||||||
JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class);
|
JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class);
|
||||||
List<TaskProvider<ArchitectureCheck>> packageTangleChecks = new ArrayList<>();
|
List<TaskProvider<ArchitectureCheck>> packageTangleChecks = new ArrayList<>();
|
||||||
for (SourceSet sourceSet : javaPluginExtension.getSourceSets()) {
|
for (SourceSet sourceSet : javaPluginExtension.getSourceSets()) {
|
||||||
|
@ -57,6 +59,7 @@ 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());
|
||||||
});
|
});
|
||||||
packageTangleChecks.add(checkPackageTangles);
|
packageTangleChecks.add(checkPackageTangles);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,11 +35,13 @@ import com.tngtech.archunit.core.domain.JavaAnnotation;
|
||||||
import com.tngtech.archunit.core.domain.JavaCall;
|
import com.tngtech.archunit.core.domain.JavaCall;
|
||||||
import com.tngtech.archunit.core.domain.JavaClass;
|
import com.tngtech.archunit.core.domain.JavaClass;
|
||||||
import com.tngtech.archunit.core.domain.JavaClass.Predicates;
|
import com.tngtech.archunit.core.domain.JavaClass.Predicates;
|
||||||
|
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||||
import com.tngtech.archunit.core.domain.JavaConstructor;
|
import com.tngtech.archunit.core.domain.JavaConstructor;
|
||||||
import com.tngtech.archunit.core.domain.JavaField;
|
import com.tngtech.archunit.core.domain.JavaField;
|
||||||
import com.tngtech.archunit.core.domain.JavaMember;
|
import com.tngtech.archunit.core.domain.JavaMember;
|
||||||
import com.tngtech.archunit.core.domain.JavaMethod;
|
import com.tngtech.archunit.core.domain.JavaMethod;
|
||||||
import com.tngtech.archunit.core.domain.JavaModifier;
|
import com.tngtech.archunit.core.domain.JavaModifier;
|
||||||
|
import com.tngtech.archunit.core.domain.JavaPackage;
|
||||||
import com.tngtech.archunit.core.domain.JavaParameter;
|
import com.tngtech.archunit.core.domain.JavaParameter;
|
||||||
import com.tngtech.archunit.core.domain.JavaType;
|
import com.tngtech.archunit.core.domain.JavaType;
|
||||||
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
|
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
|
||||||
|
@ -48,8 +50,10 @@ import com.tngtech.archunit.core.domain.properties.HasName;
|
||||||
import com.tngtech.archunit.core.domain.properties.HasOwner;
|
import com.tngtech.archunit.core.domain.properties.HasOwner;
|
||||||
import com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With;
|
import com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With;
|
||||||
import com.tngtech.archunit.core.domain.properties.HasParameterTypes;
|
import com.tngtech.archunit.core.domain.properties.HasParameterTypes;
|
||||||
|
import com.tngtech.archunit.lang.AbstractClassesTransformer;
|
||||||
import com.tngtech.archunit.lang.ArchCondition;
|
import com.tngtech.archunit.lang.ArchCondition;
|
||||||
import com.tngtech.archunit.lang.ArchRule;
|
import com.tngtech.archunit.lang.ArchRule;
|
||||||
|
import com.tngtech.archunit.lang.ClassesTransformer;
|
||||||
import com.tngtech.archunit.lang.ConditionEvents;
|
import com.tngtech.archunit.lang.ConditionEvents;
|
||||||
import com.tngtech.archunit.lang.SimpleConditionEvent;
|
import com.tngtech.archunit.lang.SimpleConditionEvent;
|
||||||
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
|
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
|
||||||
|
@ -70,6 +74,7 @@ import org.springframework.util.ResourceUtils;
|
||||||
* @author Ivan Malutin
|
* @author Ivan Malutin
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
* @author Ngoc Nhan
|
* @author Ngoc Nhan
|
||||||
|
* @author Moritz Halbritter
|
||||||
*/
|
*/
|
||||||
final class ArchitectureRules {
|
final class ArchitectureRules {
|
||||||
|
|
||||||
|
@ -244,6 +249,10 @@ final class ArchitectureRules {
|
||||||
.allowEmptyShould(true);
|
.allowEmptyShould(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ArchRule packagesShouldBeAnnotatedWithNullMarked() {
|
||||||
|
return ArchRuleDefinition.all(packages()).should(beAnnotatedWithNullMarked()).allowEmptyShould(true);
|
||||||
|
}
|
||||||
|
|
||||||
private static ArchCondition<? super JavaMethod> notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType() {
|
private static ArchCondition<? super JavaMethod> notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType() {
|
||||||
return check("not specify only a type that is the same as the method's return type", (item, events) -> {
|
return check("not specify only a type that is the same as the method's return type", (item, events) -> {
|
||||||
JavaAnnotation<JavaMethod> conditionalAnnotation = item
|
JavaAnnotation<JavaMethod> conditionalAnnotation = item
|
||||||
|
@ -471,6 +480,27 @@ final class ArchitectureRules {
|
||||||
return string + " should be used instead";
|
return string + " should be used instead";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ClassesTransformer<JavaPackage> packages() {
|
||||||
|
return new AbstractClassesTransformer<>("packages") {
|
||||||
|
@Override
|
||||||
|
public Iterable<JavaPackage> doTransform(JavaClasses collection) {
|
||||||
|
return collection.stream().map(JavaClass::getPackage).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ArchCondition<JavaPackage> beAnnotatedWithNullMarked() {
|
||||||
|
return new ArchCondition<>("be annotated with @NullMarked") {
|
||||||
|
@Override
|
||||||
|
public void check(JavaPackage item, ConditionEvents events) {
|
||||||
|
if (!item.isAnnotatedWith("org.jspecify.annotations.NullMarked")) {
|
||||||
|
String message = String.format("Package %s is not annotated with @NullMarked", item.getName());
|
||||||
|
events.add(SimpleConditionEvent.violated(item, message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static class OverridesPublicMethod<T extends JavaMember> extends DescribedPredicate<T> {
|
private static class OverridesPublicMethod<T extends JavaMember> extends DescribedPredicate<T> {
|
||||||
|
|
||||||
OverridesPublicMethod() {
|
OverridesPublicMethod() {
|
||||||
|
|
|
@ -40,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
* @author Scott Frederick
|
* @author Scott Frederick
|
||||||
* @author Ivan Malutin
|
* @author Ivan Malutin
|
||||||
* @author Dmytro Nosan
|
* @author Dmytro Nosan
|
||||||
|
* @author Moritz Halbritter
|
||||||
*/
|
*/
|
||||||
class ArchitectureCheckTests {
|
class ArchitectureCheckTests {
|
||||||
|
|
||||||
|
@ -193,6 +194,9 @@ class ArchitectureCheckTests {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.springframework.integration:spring-integration-jmx:6.3.9")
|
implementation("org.springframework.integration:spring-integration-jmx:6.3.9")
|
||||||
}
|
}
|
||||||
|
architectureCheck {
|
||||||
|
nullMarked = false
|
||||||
|
}
|
||||||
""");
|
""");
|
||||||
Path testClass = this.projectDir.resolve("src/main/java/boot/architecture/bpp/external/TestClass.java");
|
Path testClass = this.projectDir.resolve("src/main/java/boot/architecture/bpp/external/TestClass.java");
|
||||||
Files.createDirectories(testClass.getParent());
|
Files.createDirectories(testClass.getParent());
|
||||||
|
@ -211,6 +215,31 @@ class ArchitectureCheckTests {
|
||||||
+ "type assignable to org.springframework.beans.factory.config.BeanPostProcessor "));
|
+ "type assignable to org.springframework.beans.factory.config.BeanPostProcessor "));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailIfPackageIsNotAnnotatedWithNullMarked() throws IOException {
|
||||||
|
Files.writeString(this.buildFile, """
|
||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
id 'org.springframework.boot.architecture'
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
java {
|
||||||
|
sourceCompatibility = 17
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Path testClass = this.projectDir.resolve("src/main/java/boot/architecture/nullmarked/external/TestClass.java");
|
||||||
|
Files.createDirectories(testClass.getParent());
|
||||||
|
Files.writeString(testClass, """
|
||||||
|
package org.springframework.boot.build.architecture.nullmarked.external;
|
||||||
|
public class TestClass {
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
runGradle(shouldHaveFailureReportWithMessages(
|
||||||
|
"Package org.springframework.boot.build.architecture.nullmarked.external is not annotated with @NullMarked"));
|
||||||
|
}
|
||||||
|
|
||||||
private Consumer<GradleRunner> shouldHaveEmptyFailureReport() {
|
private Consumer<GradleRunner> shouldHaveEmptyFailureReport() {
|
||||||
return (gradleRunner) -> {
|
return (gradleRunner) -> {
|
||||||
try {
|
try {
|
||||||
|
@ -246,6 +275,9 @@ class ArchitectureCheckTests {
|
||||||
output.classesDirs.setFrom(file("classes"))
|
output.classesDirs.setFrom(file("classes"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
architectureCheck {
|
||||||
|
nullMarked = false
|
||||||
|
}
|
||||||
""");
|
""");
|
||||||
runGradle(callback);
|
runGradle(callback);
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,10 @@ dependencies {
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
architectureCheck {
|
||||||
|
nullMarked = false
|
||||||
|
}
|
||||||
|
|
||||||
def dependenciesOf(String version) {
|
def dependenciesOf(String version) {
|
||||||
if (version.startsWith("4.")) {
|
if (version.startsWith("4.")) {
|
||||||
return [
|
return [
|
||||||
|
|
|
@ -28,3 +28,7 @@ dependencies {
|
||||||
testImplementation("org.assertj:assertj-core")
|
testImplementation("org.assertj:assertj-core")
|
||||||
testImplementation("org.springframework:spring-core")
|
testImplementation("org.springframework:spring-core")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
architectureCheck {
|
||||||
|
nullMarked = false
|
||||||
|
}
|
||||||
|
|
|
@ -30,6 +30,10 @@ sourceSets {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
architectureCheck {
|
||||||
|
nullMarked = false
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testCompileOnly("com.google.code.findbugs:jsr305:3.0.2")
|
testCompileOnly("com.google.code.findbugs:jsr305:3.0.2")
|
||||||
|
|
||||||
|
|
|
@ -26,3 +26,7 @@ dependencies {
|
||||||
testImplementation(enforcedPlatform(project(":platform:spring-boot-dependencies")))
|
testImplementation(enforcedPlatform(project(":platform:spring-boot-dependencies")))
|
||||||
testImplementation(project(":test-support:spring-boot-test-support"))
|
testImplementation(project(":test-support:spring-boot-test-support"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
architectureCheck {
|
||||||
|
nullMarked = false
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
|
||||||
|
|
||||||
import org.springframework.boot.build.docs.ConfigureJavadocLinks
|
import org.springframework.boot.build.docs.ConfigureJavadocLinks
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
|
@ -67,6 +69,10 @@ tasks.named('compileKotlin', KotlinCompilationTask.class) {
|
||||||
javaSources.from = []
|
javaSources.from = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
architectureCheck {
|
||||||
|
nullMarked = false
|
||||||
|
}
|
||||||
|
|
||||||
plugins.withType(EclipsePlugin) {
|
plugins.withType(EclipsePlugin) {
|
||||||
eclipse.classpath { classpath ->
|
eclipse.classpath { classpath ->
|
||||||
classpath.plusConfigurations.add(configurations.getByName(sourceSets.main.runtimeClasspathConfigurationName))
|
classpath.plusConfigurations.add(configurations.getByName(sourceSets.main.runtimeClasspathConfigurationName))
|
||||||
|
|
|
@ -36,3 +36,7 @@ tasks.configureEach {
|
||||||
prohibitObjectsRequireNonNull = false
|
prohibitObjectsRequireNonNull = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
architectureCheck {
|
||||||
|
nullMarked = false
|
||||||
|
}
|
||||||
|
|
|
@ -36,3 +36,7 @@ tasks.configureEach {
|
||||||
prohibitObjectsRequireNonNull = false
|
prohibitObjectsRequireNonNull = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
architectureCheck {
|
||||||
|
nullMarked = false
|
||||||
|
}
|
||||||
|
|
|
@ -31,3 +31,7 @@ dependencies {
|
||||||
testImplementation(project(":starter:spring-boot-starter-test"))
|
testImplementation(project(":starter:spring-boot-starter-test"))
|
||||||
testImplementation("io.projectreactor:reactor-test")
|
testImplementation("io.projectreactor:reactor-test")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
architectureCheck {
|
||||||
|
nullMarked = false
|
||||||
|
}
|
||||||
|
|
|
@ -48,3 +48,7 @@ dependencies {
|
||||||
optional("org.testcontainers:redpanda")
|
optional("org.testcontainers:redpanda")
|
||||||
optional("com.redis:testcontainers-redis")
|
optional("com.redis:testcontainers-redis")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
architectureCheck {
|
||||||
|
nullMarked = false
|
||||||
|
}
|
||||||
|
|
|
@ -30,3 +30,7 @@ dependencies {
|
||||||
implementation("org.assertj:assertj-core")
|
implementation("org.assertj:assertj-core")
|
||||||
implementation("org.springframework:spring-core")
|
implementation("org.springframework:spring-core")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
architectureCheck {
|
||||||
|
nullMarked = false
|
||||||
|
}
|
||||||
|
|
|
@ -61,3 +61,7 @@ dependencies {
|
||||||
|
|
||||||
testRuntimeOnly("org.hibernate.validator:hibernate-validator")
|
testRuntimeOnly("org.hibernate.validator:hibernate-validator")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
architectureCheck {
|
||||||
|
nullMarked = false
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue