Configure Nullabillity checks with NullAway

This commit configures the Errorprone and Nullaway plugins in the build
to automatically check for null safety issues in the codebase.

Closes gh-1204
This commit is contained in:
Brian Clozel
2025-05-21 09:30:07 +02:00
parent 121add8ef7
commit 5acb64e61d
6 changed files with 93 additions and 4 deletions

View File

@@ -10,6 +10,7 @@ ext {
subprojects {
apply plugin: 'org.springframework.graphql.conventions'
apply plugin: 'org.springframework.graphql.architecture'
apply plugin: 'org.springframework.graphql.nullability'
group = 'org.springframework.graphql'
ext.javadocLinks = [

View File

@@ -23,6 +23,7 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
implementation("io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}")
implementation("net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:${errorProneVersion}")
implementation "com.tngtech.archunit:archunit:1.4.0"
}
@@ -40,6 +41,10 @@ gradlePlugin {
id = "org.springframework.graphql.architecture"
implementationClass = "org.springframework.graphql.build.architecture.ArchitecturePlugin"
}
nullability {
id = "org.springframework.graphql.nullability"
implementationClass = "org.springframework.graphql.build.nullability.NullabilityPlugin"
}
}
}

View File

@@ -1 +1,2 @@
javaFormatVersion=0.0.43
javaFormatVersion=0.0.43
errorProneVersion=4.2.0

View File

@@ -45,10 +45,12 @@ import org.gradle.api.tasks.SkipWhenEmpty;
import org.gradle.api.tasks.TaskAction;
import static org.springframework.graphql.build.architecture.ArchitectureRules.allPackagesShouldBeFreeOfTangles;
import static org.springframework.graphql.build.architecture.ArchitectureRules.classShouldNotUseSpringNullAnnotations;
import static org.springframework.graphql.build.architecture.ArchitectureRules.classesShouldNotImportForbiddenTypes;
import static org.springframework.graphql.build.architecture.ArchitectureRules.javaClassesShouldNotImportKotlinAnnotations;
import static org.springframework.graphql.build.architecture.ArchitectureRules.noClassesShouldCallStringToLowerCaseWithoutLocale;
import static org.springframework.graphql.build.architecture.ArchitectureRules.noClassesShouldCallStringToUpperCaseWithoutLocale;
import static org.springframework.graphql.build.architecture.ArchitectureRules.packageInfoShouldBeNullMarked;
/**
* {@link Task} that checks for architecture problems.
@@ -68,7 +70,9 @@ public abstract class ArchitectureCheck extends DefaultTask {
javaClassesShouldNotImportKotlinAnnotations(),
allPackagesShouldBeFreeOfTangles(),
noClassesShouldCallStringToLowerCaseWithoutLocale(),
noClassesShouldCallStringToUpperCaseWithoutLocale());
noClassesShouldCallStringToUpperCaseWithoutLocale(),
packageInfoShouldBeNullMarked(),
classShouldNotUseSpringNullAnnotations());
getRuleDescriptions().set(getRules().map((rules) -> rules.stream().map(ArchRule::getDescription).toList()));
}

View File

@@ -55,9 +55,9 @@ abstract class ArchitectureRules {
static ArchRule classShouldNotUseSpringNullAnnotations() {
return ArchRuleDefinition.noClasses()
.should().dependOnClassesThat()
.haveFullyQualifiedName("org.springframework.lang.NonNull")
.haveFullyQualifiedName("org.jspecify.annotations.NonNull")
.orShould().dependOnClassesThat()
.haveFullyQualifiedName("org.springframework.lang.Nullable");
.haveFullyQualifiedName("org.jspecify.annotations.Nullable");
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2020-2025 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.graphql.build.nullability;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import net.ltgt.gradle.errorprone.ErrorProneOptions;
import net.ltgt.gradle.errorprone.ErrorPronePlugin;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.DependencySet;
import org.gradle.api.plugins.ExtensionAware;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.tasks.compile.CompileOptions;
import org.gradle.api.tasks.compile.JavaCompile;
/**
* {@link Plugin} for enforcing Nullability checks on the source code.
*
* @author Brian Clozel
*/
public class NullabilityPlugin implements Plugin<Project> {
private static final Pattern COMPILE_MAIN_SOURCES_TASK_NAME = Pattern.compile("compile(\\d+)?Java");
@Override
public void apply(Project project) {
project.getPlugins().apply(ErrorPronePlugin.class);
DependencySet errorproneConfig = project.getConfigurations().getByName("errorprone").getDependencies();
errorproneConfig.add(project.getDependencies().create("com.uber.nullaway:nullaway:0.12.6"));
errorproneConfig.add(project.getDependencies().create("com.google.errorprone:error_prone_core:2.37.0"));
project.getTasks()
.withType(JavaCompile.class)
.configureEach((javaCompile) -> {
if (compilesMainSources(javaCompile)) {
doWithErrorProneOptions(javaCompile, (errorProneOptions) -> {
errorProneOptions.getDisableAllChecks().set(true);
errorProneOptions.option("NullAway:OnlyNullMarked", "true");
errorProneOptions.option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract");
errorProneOptions.option("NullAway:JSpecifyMode", "true");
errorProneOptions.error("NullAway");
});
}
else {
doWithErrorProneOptions(javaCompile, (errorProneOptions) -> {
errorProneOptions.getEnabled().set(false);
});
}
});
}
private boolean compilesMainSources(JavaCompile compileTask) {
return COMPILE_MAIN_SOURCES_TASK_NAME.matcher(compileTask.getName()).matches();
}
private void doWithErrorProneOptions(JavaCompile compileTask, Consumer<ErrorProneOptions> optionsConsumer) {
CompileOptions options = compileTask.getOptions();
ErrorProneOptions errorProneOptions = ((ExtensionAware) options).getExtensions().getByType(ErrorProneOptions.class);
optionsConsumer.accept(errorProneOptions);
}
}