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 Phillip Webb
|
||||
* @author Dmytro Nosan
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
public abstract class ArchitectureCheck extends DefaultTask {
|
||||
|
||||
|
@ -79,13 +80,21 @@ public abstract class ArchitectureCheck extends DefaultTask {
|
|||
getRules().addAll(ArchitectureRules.standard());
|
||||
getRules().addAll(whenMainSources(
|
||||
() -> Collections.singletonList(ArchitectureRules.allBeanMethodsShouldReturnNonPrivateType())));
|
||||
getRules().addAll(and(getNullMarked(), isMainSourceSet()).map(whenTrue(
|
||||
() -> Collections.singletonList(ArchitectureRules.packagesShouldBeAnnotatedWithNullMarked()))));
|
||||
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) {
|
||||
return getSourceSet().convention(SourceSet.MAIN_SOURCE_SET_NAME)
|
||||
.map(SourceSet.MAIN_SOURCE_SET_NAME::equals)
|
||||
.map(whenTrue(rules));
|
||||
return isMainSourceSet().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) {
|
||||
|
@ -186,4 +195,7 @@ public abstract class ArchitectureCheck extends DefaultTask {
|
|||
@Input // Use descriptions as input since rules aren't serializable
|
||||
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
|
||||
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);
|
||||
List<TaskProvider<ArchitectureCheck>> packageTangleChecks = new ArrayList<>();
|
||||
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()
|
||||
+ " source set.");
|
||||
task.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
|
||||
task.getNullMarked().set(extension.getNullMarked());
|
||||
});
|
||||
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.JavaClass;
|
||||
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.JavaField;
|
||||
import com.tngtech.archunit.core.domain.JavaMember;
|
||||
import com.tngtech.archunit.core.domain.JavaMethod;
|
||||
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.JavaType;
|
||||
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.Predicates.With;
|
||||
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.ArchRule;
|
||||
import com.tngtech.archunit.lang.ClassesTransformer;
|
||||
import com.tngtech.archunit.lang.ConditionEvents;
|
||||
import com.tngtech.archunit.lang.SimpleConditionEvent;
|
||||
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
|
||||
|
@ -70,6 +74,7 @@ import org.springframework.util.ResourceUtils;
|
|||
* @author Ivan Malutin
|
||||
* @author Phillip Webb
|
||||
* @author Ngoc Nhan
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
final class ArchitectureRules {
|
||||
|
||||
|
@ -244,6 +249,10 @@ final class ArchitectureRules {
|
|||
.allowEmptyShould(true);
|
||||
}
|
||||
|
||||
static ArchRule packagesShouldBeAnnotatedWithNullMarked() {
|
||||
return ArchRuleDefinition.all(packages()).should(beAnnotatedWithNullMarked()).allowEmptyShould(true);
|
||||
}
|
||||
|
||||
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) -> {
|
||||
JavaAnnotation<JavaMethod> conditionalAnnotation = item
|
||||
|
@ -471,6 +480,27 @@ final class ArchitectureRules {
|
|||
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> {
|
||||
|
||||
OverridesPublicMethod() {
|
||||
|
|
|
@ -40,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
* @author Scott Frederick
|
||||
* @author Ivan Malutin
|
||||
* @author Dmytro Nosan
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class ArchitectureCheckTests {
|
||||
|
||||
|
@ -193,6 +194,9 @@ class ArchitectureCheckTests {
|
|||
dependencies {
|
||||
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");
|
||||
Files.createDirectories(testClass.getParent());
|
||||
|
@ -211,6 +215,31 @@ class ArchitectureCheckTests {
|
|||
+ "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() {
|
||||
return (gradleRunner) -> {
|
||||
try {
|
||||
|
@ -246,6 +275,9 @@ class ArchitectureCheckTests {
|
|||
output.classesDirs.setFrom(file("classes"))
|
||||
}
|
||||
}
|
||||
architectureCheck {
|
||||
nullMarked = false
|
||||
}
|
||||
""");
|
||||
runGradle(callback);
|
||||
}
|
||||
|
|
|
@ -38,6 +38,10 @@ dependencies {
|
|||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
}
|
||||
|
||||
architectureCheck {
|
||||
nullMarked = false
|
||||
}
|
||||
|
||||
def dependenciesOf(String version) {
|
||||
if (version.startsWith("4.")) {
|
||||
return [
|
||||
|
|
|
@ -28,3 +28,7 @@ dependencies {
|
|||
testImplementation("org.assertj:assertj-core")
|
||||
testImplementation("org.springframework:spring-core")
|
||||
}
|
||||
|
||||
architectureCheck {
|
||||
nullMarked = false
|
||||
}
|
||||
|
|
|
@ -30,6 +30,10 @@ sourceSets {
|
|||
}
|
||||
}
|
||||
|
||||
architectureCheck {
|
||||
nullMarked = false
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testCompileOnly("com.google.code.findbugs:jsr305:3.0.2")
|
||||
|
||||
|
|
|
@ -26,3 +26,7 @@ dependencies {
|
|||
testImplementation(enforcedPlatform(project(":platform:spring-boot-dependencies")))
|
||||
testImplementation(project(":test-support:spring-boot-test-support"))
|
||||
}
|
||||
|
||||
architectureCheck {
|
||||
nullMarked = false
|
||||
}
|
||||
|
|
|
@ -14,7 +14,9 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
|
||||
|
||||
import org.springframework.boot.build.docs.ConfigureJavadocLinks
|
||||
|
||||
plugins {
|
||||
|
@ -67,6 +69,10 @@ tasks.named('compileKotlin', KotlinCompilationTask.class) {
|
|||
javaSources.from = []
|
||||
}
|
||||
|
||||
architectureCheck {
|
||||
nullMarked = false
|
||||
}
|
||||
|
||||
plugins.withType(EclipsePlugin) {
|
||||
eclipse.classpath { classpath ->
|
||||
classpath.plusConfigurations.add(configurations.getByName(sourceSets.main.runtimeClasspathConfigurationName))
|
||||
|
|
|
@ -36,3 +36,7 @@ tasks.configureEach {
|
|||
prohibitObjectsRequireNonNull = false
|
||||
}
|
||||
}
|
||||
|
||||
architectureCheck {
|
||||
nullMarked = false
|
||||
}
|
||||
|
|
|
@ -36,3 +36,7 @@ tasks.configureEach {
|
|||
prohibitObjectsRequireNonNull = false
|
||||
}
|
||||
}
|
||||
|
||||
architectureCheck {
|
||||
nullMarked = false
|
||||
}
|
||||
|
|
|
@ -31,3 +31,7 @@ dependencies {
|
|||
testImplementation(project(":starter:spring-boot-starter-test"))
|
||||
testImplementation("io.projectreactor:reactor-test")
|
||||
}
|
||||
|
||||
architectureCheck {
|
||||
nullMarked = false
|
||||
}
|
||||
|
|
|
@ -48,3 +48,7 @@ dependencies {
|
|||
optional("org.testcontainers:redpanda")
|
||||
optional("com.redis:testcontainers-redis")
|
||||
}
|
||||
|
||||
architectureCheck {
|
||||
nullMarked = false
|
||||
}
|
||||
|
|
|
@ -30,3 +30,7 @@ dependencies {
|
|||
implementation("org.assertj:assertj-core")
|
||||
implementation("org.springframework:spring-core")
|
||||
}
|
||||
|
||||
architectureCheck {
|
||||
nullMarked = false
|
||||
}
|
||||
|
|
|
@ -61,3 +61,7 @@ dependencies {
|
|||
|
||||
testRuntimeOnly("org.hibernate.validator:hibernate-validator")
|
||||
}
|
||||
|
||||
architectureCheck {
|
||||
nullMarked = false
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue