Skip to content

Commit 448b7ea

Browse files
committed
Add nullability annotations
Closes gh-972
1 parent d38dd75 commit 448b7ea

File tree

94 files changed

+568
-269
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+568
-269
lines changed

docs/src/docs/asciidoc/documenting-your-api.adoc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ This section provides more details about using Spring REST Docs to document your
55

66

77

8+
[[documenting-your-api-null-safety]]
9+
=== Null Safety
10+
Spring REST Docs is annotated with https://jspecify.dev/docs/start-here/[JSpecify ] annotations to declare the nullability of its API.
11+
To learn more about JSpecify, its https://jspecify.dev/docs/user-guide/[user guide] is recommended reading.
12+
13+
The primary goal of declaring the nullability of the API is to prevent a `NullPointerException` from being thrown at runtime.
14+
This is achieved through build-time checks that are available with both Java and Kotlin.
15+
Performing the checks with Java requires some tooling such as https://github.com/uber/NullAway[NullAway] or an IDE that supports JSpecify annotations such as IntelliJ IDEA.
16+
The checks are available automatically with Kotlin which translates the JSpecify annotations into Kotlin's null safety.
17+
18+
To learn more about null safety with Spring, refer to the {spring-framework-docs}/core/null-safety.html[Spring Framework reference documentation].
19+
20+
21+
822
[[documenting-your-api-hypermedia]]
923
=== Hypermedia
1024

gradle/plugins/conventions/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ gradlePlugin {
1818

1919
dependencies {
2020
implementation(project(":toolchain"))
21+
implementation("io.spring.gradle.nullability:nullability-plugin:0.0.2")
2122
implementation("io.spring.javaformat:spring-javaformat-gradle-plugin:$javaFormatVersion")
2223
implementation("io.spring.nohttp:nohttp-gradle:0.0.11")
2324
}

gradle/plugins/conventions/src/main/java/org/springframework/restdocs/build/conventions/JavaBasePluginConventions.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.List;
2020
import java.util.Map;
2121

22+
import io.spring.gradle.nullability.NullabilityPlugin;
2223
import io.spring.javaformat.gradle.SpringJavaFormatPlugin;
2324
import org.gradle.api.JavaVersion;
2425
import org.gradle.api.Project;
@@ -49,6 +50,7 @@ class JavaBasePluginConventions extends Conventions<JavaBasePlugin> {
4950

5051
@Override
5152
void apply(JavaBasePlugin plugin) {
53+
configureNullability();
5254
configureToolchains();
5355
configureCheckstyle();
5456
configureJavaFormat();
@@ -58,6 +60,10 @@ void apply(JavaBasePlugin plugin) {
5860
configureDependencyManagement();
5961
}
6062

63+
private void configureNullability() {
64+
getProject().getPlugins().apply(NullabilityPlugin.class);
65+
}
66+
6167
private void configureToolchains() {
6268
getProject().getPlugins().apply(ToolchainPlugin.class);
6369
}

spring-restdocs-asciidoctor/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ plugins {
77
description = "Spring REST Docs Asciidoctor Extension"
88

99
dependencies {
10+
compileOnly("org.jspecify:jspecify")
11+
1012
implementation("org.asciidoctor:asciidoctorj")
1113

1214
testImplementation("org.apache.pdfbox:pdfbox")

spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.util.Map;
2424
import java.util.function.Supplier;
2525

26+
import org.jspecify.annotations.Nullable;
27+
2628
/**
2729
* Resolves the directory from which snippets can be read for inclusion in an Asciidoctor
2830
* document. The resolved directory is absolute.
@@ -40,7 +42,12 @@ File getSnippetsDirectory(Map<String, Object> attributes) {
4042

4143
private File getMavenSnippetsDirectory(Map<String, Object> attributes) {
4244
Path docdir = Paths.get(getRequiredAttribute(attributes, "docdir"));
43-
return new File(findPom(docdir).getParent().toFile(), "target/generated-snippets").getAbsoluteFile();
45+
Path pom = findPom(docdir);
46+
Path parent = pom.getParent();
47+
if (parent == null) {
48+
throw new IllegalStateException("Pom '" + pom + "' has no parent directory");
49+
}
50+
return new File(parent.toFile(), "target/generated-snippets").getAbsoluteFile();
4451
}
4552

4653
private Path findPom(Path docdir) {
@@ -65,7 +72,8 @@ private String getRequiredAttribute(Map<String, Object> attributes, String name)
6572
return getRequiredAttribute(attributes, name, null);
6673
}
6774

68-
private String getRequiredAttribute(Map<String, Object> attributes, String name, Supplier<String> fallback) {
75+
private String getRequiredAttribute(Map<String, Object> attributes, String name,
76+
@Nullable Supplier<String> fallback) {
6977
String attribute = (String) attributes.get(name);
7078
if (attribute == null || attribute.length() == 0) {
7179
if (fallback != null) {

spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/package-info.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,7 @@
1717
/**
1818
* Support for Asciidoctor.
1919
*/
20+
@NullMarked
2021
package org.springframework.restdocs.asciidoctor;
22+
23+
import org.jspecify.annotations.NullMarked;

spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@
1818

1919
import java.io.File;
2020

21+
import org.jspecify.annotations.Nullable;
2122
import org.junit.jupiter.api.extension.Extension;
2223

24+
import org.springframework.util.Assert;
25+
2326
/**
2427
* {@code ManualRestDocumentation} is used to manually manage the
2528
* {@link RestDocumentationContext}. Primarly intended for use with TestNG, but suitable
@@ -35,7 +38,7 @@ public final class ManualRestDocumentation implements RestDocumentationContextPr
3538

3639
private final File outputDirectory;
3740

38-
private StandardRestDocumentationContext context;
41+
private @Nullable StandardRestDocumentationContext context;
3942

4043
/**
4144
* Creates a new {@code ManualRestDocumentation} instance that will generate snippets
@@ -68,9 +71,7 @@ private ManualRestDocumentation(File outputDirectory) {
6871
* @throws IllegalStateException if a context has already be created
6972
*/
7073
public void beforeTest(Class<?> testClass, String testMethodName) {
71-
if (this.context != null) {
72-
throw new IllegalStateException("Context already exists. Did you forget to call afterTest()?");
73-
}
74+
Assert.isNull(this.context, () -> "Context already exists. Did you forget to call afterTest()?");
7475
this.context = new StandardRestDocumentationContext(testClass, testMethodName, this.outputDirectory);
7576
}
7677

@@ -84,6 +85,7 @@ public void afterTest() {
8485

8586
@Override
8687
public RestDocumentationContext beforeOperation() {
88+
Assert.notNull(this.context, () -> "Context is null. Did you forget to call beforeTest(Class, String)?");
8789
this.context.getAndIncrementStepCount();
8890
return this.context;
8991
}

spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationExtension.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.restdocs;
1818

19+
import org.jspecify.annotations.Nullable;
1920
import org.junit.jupiter.api.extension.AfterEachCallback;
2021
import org.junit.jupiter.api.extension.BeforeEachCallback;
2122
import org.junit.jupiter.api.extension.Extension;
@@ -32,7 +33,7 @@
3233
*/
3334
public class RestDocumentationExtension implements BeforeEachCallback, AfterEachCallback, ParameterResolver {
3435

35-
private final String outputDirectory;
36+
private final @Nullable String outputDirectory;
3637

3738
/**
3839
* Creates a new {@code RestDocumentationExtension} that will use the default output
@@ -48,7 +49,7 @@ public RestDocumentationExtension() {
4849
* @param outputDirectory snippet output directory
4950
* @since 2.0.4
5051
*/
51-
public RestDocumentationExtension(String outputDirectory) {
52+
public RestDocumentationExtension(@Nullable String outputDirectory) {
5253
this.outputDirectory = outputDirectory;
5354
}
5455

spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import java.util.Map;
2020

21+
import org.jspecify.annotations.Nullable;
22+
2123
import org.springframework.restdocs.snippet.Snippet;
2224

2325
/**
@@ -80,7 +82,7 @@ public static Snippet curlRequest(CommandFormatter commandFormatter) {
8082
* @return the snippet that will document the curl request
8183
* @since 1.2.0
8284
*/
83-
public static Snippet curlRequest(Map<String, Object> attributes, CommandFormatter commandFormatter) {
85+
public static Snippet curlRequest(@Nullable Map<String, Object> attributes, CommandFormatter commandFormatter) {
8486
return new CurlRequestSnippet(attributes, commandFormatter);
8587
}
8688

@@ -126,7 +128,7 @@ public static Snippet httpieRequest(CommandFormatter commandFormatter) {
126128
* @return the snippet that will document the HTTPie request
127129
* @since 1.2.0
128130
*/
129-
public static Snippet httpieRequest(Map<String, Object> attributes, CommandFormatter commandFormatter) {
131+
public static Snippet httpieRequest(@Nullable Map<String, Object> attributes, CommandFormatter commandFormatter) {
130132
return new HttpieRequestSnippet(attributes, commandFormatter);
131133
}
132134

spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@
2626
import java.util.Map.Entry;
2727
import java.util.Set;
2828

29+
import org.jspecify.annotations.Nullable;
30+
2931
import org.springframework.http.HttpHeaders;
3032
import org.springframework.http.HttpMethod;
33+
import org.springframework.lang.Contract;
3134
import org.springframework.restdocs.operation.OperationRequest;
3235
import org.springframework.restdocs.operation.OperationRequestPart;
3336
import org.springframework.restdocs.operation.RequestCookie;
@@ -55,7 +58,7 @@ boolean isPutOrPost() {
5558
return HttpMethod.PUT.equals(this.delegate.getMethod()) || HttpMethod.POST.equals(this.delegate.getMethod());
5659
}
5760

58-
String getBasicAuthCredentials() {
61+
@Nullable String getBasicAuthCredentials() {
5962
List<String> headerValue = this.delegate.getHeaders().get(HttpHeaders.AUTHORIZATION);
6063
if (BasicAuthHeaderFilter.isBasicAuthHeader(headerValue)) {
6164
return BasicAuthHeaderFilter.decodeBasicAuthHeader(headerValue);
@@ -132,7 +135,8 @@ public boolean allow(String name, List<String> value) {
132135
return !(HttpHeaders.AUTHORIZATION.equals(name) && isBasicAuthHeader(value));
133136
}
134137

135-
static boolean isBasicAuthHeader(List<String> value) {
138+
@Contract("null -> false")
139+
static boolean isBasicAuthHeader(@Nullable List<String> value) {
136140
return value != null && (!value.isEmpty()) && value.get(0).startsWith("Basic ");
137141
}
138142

0 commit comments

Comments
 (0)