Skip to content

Commit e495eb5

Browse files
SentryManrbygrave
andauthored
Add non-null constraints when in a Jspecify @NullMarked module/package (#257)
* Add non-null constraints using jspecify * no longer need a cosntraint annotation * no automatic nonnull for methods * Format, remove unneeded public modifier --------- Co-authored-by: Rob Bygrave <[email protected]>
1 parent 5e87706 commit e495eb5

File tree

11 files changed

+120
-5
lines changed

11 files changed

+120
-5
lines changed

blackbox-test/src/main/java/module-info.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
requires io.avaje.validation.contraints;
77
requires jakarta.validation;
88
requires jakarta.inject;
9+
requires org.jspecify;
10+
911
provides io.avaje.validation.spi.ValidationExtension with example.avaje.valid.GeneratedValidatorComponent;
1012
provides io.avaje.inject.spi.InjectExtension with example.avaje.GeneratedModule;
1113
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package example.avaje.jspecify;
2+
3+
import org.jspecify.annotations.Nullable;
4+
5+
import jakarta.validation.Valid;
6+
7+
@Valid
8+
public record JSpecifyNotNull(String basic, String withMax, @Nullable String withCustom) {}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package example.avaje.jspecify;
2+
3+
import org.jspecify.annotations.NullUnmarked;
4+
5+
import io.avaje.validation.constraints.Null;
6+
import jakarta.validation.Valid;
7+
8+
@Valid
9+
@NullUnmarked
10+
public record JSpecifyNullUnmarked(@Null String basic) {}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package example.avaje.jspecify;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.util.Locale;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
import io.avaje.validation.ConstraintViolationException;
10+
import io.avaje.validation.Validator;
11+
12+
public class JSpecifyTest {
13+
14+
final Validator validator =
15+
Validator.builder()
16+
.add(JSpecifyNotNull.class, JSpecifyNotNullValidationAdapter::new)
17+
.add(JSpecifyNullUnmarked.class, JSpecifyNullUnmarkedValidationAdapter::new)
18+
.addLocales(Locale.GERMAN)
19+
.build();
20+
21+
@Test
22+
void valid() {
23+
var value = new JSpecifyNotNull("ok", "ok", "ok");
24+
validator.validate(value);
25+
validator.validate(new JSpecifyNullUnmarked(null));
26+
}
27+
28+
@Test
29+
void inValidNull() {
30+
var value = new JSpecifyNotNull(null, null, null);
31+
try {
32+
validator.validate(value);
33+
} catch (ConstraintViolationException e) {
34+
assertThat(e.violations()).hasSize(2);
35+
}
36+
}
37+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@org.jspecify.annotations.NullMarked
2+
package example.avaje.jspecify;

validator-generator/src/main/java/io/avaje/validation/generator/ElementAnnotationContainer.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ public record ElementAnnotationContainer(
2626

2727
static ElementAnnotationContainer create(Element element) {
2828
final var hasValid = ValidPrism.isPresent(element);
29-
3029
Map<UType, String> typeUse1;
3130
Map<UType, String> typeUse2;
3231
final Map<UType, String> crossParam = new HashMap<>();
@@ -76,6 +75,11 @@ static ElementAnnotationContainer create(Element element) {
7675
a -> UType.parse(a.getAnnotationType()),
7776
a -> AnnotationUtil.annotationAttributeMap(a, element)));
7877

78+
if (Util.isNonNullable(element)) {
79+
var nonNull = UType.parse(APContext.typeElement(NonNullPrism.PRISM_TYPE).asType());
80+
annotations.put(nonNull, "Map.of(\"message\",\"{avaje.NotNull.message}\")");
81+
}
82+
7983
return new ElementAnnotationContainer(
8084
uType, hasValid, annotations, typeUse1, typeUse2, crossParam);
8185
}
@@ -89,7 +93,7 @@ static boolean hasMetaConstraintAnnotation(Element element) {
8993
return ConstraintPrism.isPresent(element);
9094
}
9195

92-
// it seems we cannot directly retrieve mirrors from var elements, for varElements needs special
96+
// it seems we cannot directly retrieve mirrors from var elements, so var Elements needs special
9397
// handling
9498

9599
static ElementAnnotationContainer create(VariableElement varElement) {
@@ -122,6 +126,11 @@ static ElementAnnotationContainer create(VariableElement varElement) {
122126

123127
final boolean hasValid = uType.annotations().stream().anyMatch(ValidPrism::isInstance);
124128

129+
if (Util.isNonNullable(varElement)) {
130+
var nonNull = UType.parse(APContext.typeElement(NonNullPrism.PRISM_TYPE).asType());
131+
annotations.put(nonNull, "Map.of(\"message\",\"{avaje.NotNull.message}\")");
132+
}
133+
125134
return new ElementAnnotationContainer(
126135
uType, hasValid, annotations, typeUse1, typeUse2, Map.of());
127136
}

validator-generator/src/main/java/io/avaje/validation/generator/TypeReader.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ private void readField(Element element, List<FieldReader> localFields) {
107107
element = mixInField;
108108
}
109109

110-
if (includeField(element)) {
110+
if (includeField(element) || Util.isNonNullable(element)) {
111111
seenFields.add(element.toString());
112112
var reader = new FieldReader(element, genericTypeParams);
113113
if (reader.hasConstraints() || ValidPrism.isPresent(element)) {

validator-generator/src/main/java/io/avaje/validation/generator/Util.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,29 @@ static String valhalla() {
168168
return "";
169169
}
170170

171+
static boolean isNonNullable(Element e) {
172+
UType uType;
173+
if (e instanceof final ExecutableElement executableElement) {
174+
uType = UType.parse(executableElement.getReturnType());
175+
} else {
176+
uType = UType.parse(e.asType());
177+
}
178+
for (var mirror : uType.annotations()) {
179+
if (mirror.getAnnotationType().toString().endsWith("Nullable")) {
180+
return false;
181+
} else if (NonNullPrism.isInstance(mirror)) {
182+
return true;
183+
}
184+
}
185+
return checkNullMarked(e);
186+
}
187+
188+
private static boolean checkNullMarked(Element e) {
189+
if (e == null || NullUnmarkedPrism.isPresent(e)) {
190+
return false;
191+
} else if (NullMarkedPrism.isPresent(e)) {
192+
return true;
193+
}
194+
return checkNullMarked(e.getEnclosingElement());
195+
}
171196
}
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
@GeneratePrism(io.avaje.validation.ImportValidPojo.class)
21
@GeneratePrism(io.avaje.validation.adapter.ConstraintAdapter.class)
2+
@GeneratePrism(io.avaje.validation.ImportValidPojo.class)
33
@GeneratePrism(io.avaje.validation.spi.MetaData.class)
44
@GeneratePrism(io.avaje.validation.spi.MetaData.Factory.class)
55
@GeneratePrism(io.avaje.validation.spi.MetaData.AnnotationFactory.class)
6-
@GeneratePrism(io.avaje.validation.ValidMethod.class)
76
@GeneratePrism(io.avaje.validation.MixIn.class)
7+
@GeneratePrism(org.jspecify.annotations.NullMarked.class)
8+
@GeneratePrism(org.jspecify.annotations.NullUnmarked.class)
9+
@GeneratePrism(org.jspecify.annotations.NonNull.class)
10+
@GeneratePrism(io.avaje.validation.ValidMethod.class)
811
package io.avaje.validation.generator;
912

1013
import io.avaje.prism.GeneratePrism;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.avaje.validation.generator.models.valid;
2+
3+
import org.jspecify.annotations.NullMarked;
4+
import org.jspecify.annotations.Nullable;
5+
6+
import jakarta.validation.Valid;
7+
8+
@Valid
9+
@NullMarked
10+
public record JSpecifyNotNull(String basic, String withMax, @Nullable String withCustom) {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.avaje.validation.generator.models.valid;
2+
3+
import org.jspecify.annotations.NullUnmarked;
4+
5+
import jakarta.validation.Valid;
6+
7+
@Valid
8+
@NullUnmarked
9+
public record JSpecifyNullUnmarked(String basic, String withMax, String withCustom) {}

0 commit comments

Comments
 (0)