diff --git a/blackbox-test/src/main/java/module-info.java b/blackbox-test/src/main/java/module-info.java index d523685..4f068da 100644 --- a/blackbox-test/src/main/java/module-info.java +++ b/blackbox-test/src/main/java/module-info.java @@ -6,6 +6,8 @@ requires io.avaje.validation.contraints; requires jakarta.validation; requires jakarta.inject; + requires org.jspecify; + provides io.avaje.validation.spi.ValidationExtension with example.avaje.valid.GeneratedValidatorComponent; provides io.avaje.inject.spi.InjectExtension with example.avaje.GeneratedModule; } diff --git a/blackbox-test/src/test/java/example/avaje/jspecify/JSpecifyNotNull.java b/blackbox-test/src/test/java/example/avaje/jspecify/JSpecifyNotNull.java new file mode 100644 index 0000000..d0daddc --- /dev/null +++ b/blackbox-test/src/test/java/example/avaje/jspecify/JSpecifyNotNull.java @@ -0,0 +1,8 @@ +package example.avaje.jspecify; + +import org.jspecify.annotations.Nullable; + +import jakarta.validation.Valid; + +@Valid +public record JSpecifyNotNull(String basic, String withMax, @Nullable String withCustom) {} diff --git a/blackbox-test/src/test/java/example/avaje/jspecify/JSpecifyNullUnmarked.java b/blackbox-test/src/test/java/example/avaje/jspecify/JSpecifyNullUnmarked.java new file mode 100644 index 0000000..b8a125c --- /dev/null +++ b/blackbox-test/src/test/java/example/avaje/jspecify/JSpecifyNullUnmarked.java @@ -0,0 +1,10 @@ +package example.avaje.jspecify; + +import org.jspecify.annotations.NullUnmarked; + +import io.avaje.validation.constraints.Null; +import jakarta.validation.Valid; + +@Valid +@NullUnmarked +public record JSpecifyNullUnmarked(@Null String basic) {} diff --git a/blackbox-test/src/test/java/example/avaje/jspecify/JSpecifyTest.java b/blackbox-test/src/test/java/example/avaje/jspecify/JSpecifyTest.java new file mode 100644 index 0000000..ba05ee0 --- /dev/null +++ b/blackbox-test/src/test/java/example/avaje/jspecify/JSpecifyTest.java @@ -0,0 +1,37 @@ +package example.avaje.jspecify; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +import io.avaje.validation.ConstraintViolationException; +import io.avaje.validation.Validator; + +public class JSpecifyTest { + + final Validator validator = + Validator.builder() + .add(JSpecifyNotNull.class, JSpecifyNotNullValidationAdapter::new) + .add(JSpecifyNullUnmarked.class, JSpecifyNullUnmarkedValidationAdapter::new) + .addLocales(Locale.GERMAN) + .build(); + + @Test + void valid() { + var value = new JSpecifyNotNull("ok", "ok", "ok"); + validator.validate(value); + validator.validate(new JSpecifyNullUnmarked(null)); + } + + @Test + void inValidNull() { + var value = new JSpecifyNotNull(null, null, null); + try { + validator.validate(value); + } catch (ConstraintViolationException e) { + assertThat(e.violations()).hasSize(2); + } + } +} diff --git a/blackbox-test/src/test/java/example/avaje/jspecify/package-info.java b/blackbox-test/src/test/java/example/avaje/jspecify/package-info.java new file mode 100644 index 0000000..cdfb9cd --- /dev/null +++ b/blackbox-test/src/test/java/example/avaje/jspecify/package-info.java @@ -0,0 +1,2 @@ +@org.jspecify.annotations.NullMarked +package example.avaje.jspecify; diff --git a/validator-generator/src/main/java/io/avaje/validation/generator/ElementAnnotationContainer.java b/validator-generator/src/main/java/io/avaje/validation/generator/ElementAnnotationContainer.java index 94a4b7c..5057036 100644 --- a/validator-generator/src/main/java/io/avaje/validation/generator/ElementAnnotationContainer.java +++ b/validator-generator/src/main/java/io/avaje/validation/generator/ElementAnnotationContainer.java @@ -26,7 +26,6 @@ public record ElementAnnotationContainer( static ElementAnnotationContainer create(Element element) { final var hasValid = ValidPrism.isPresent(element); - Map typeUse1; Map typeUse2; final Map crossParam = new HashMap<>(); @@ -76,6 +75,11 @@ static ElementAnnotationContainer create(Element element) { a -> UType.parse(a.getAnnotationType()), a -> AnnotationUtil.annotationAttributeMap(a, element))); + if (Util.isNonNullable(element)) { + var nonNull = UType.parse(APContext.typeElement(NonNullPrism.PRISM_TYPE).asType()); + annotations.put(nonNull, "Map.of(\"message\",\"{avaje.NotNull.message}\")"); + } + return new ElementAnnotationContainer( uType, hasValid, annotations, typeUse1, typeUse2, crossParam); } @@ -89,7 +93,7 @@ static boolean hasMetaConstraintAnnotation(Element element) { return ConstraintPrism.isPresent(element); } - // it seems we cannot directly retrieve mirrors from var elements, for varElements needs special + // it seems we cannot directly retrieve mirrors from var elements, so var Elements needs special // handling static ElementAnnotationContainer create(VariableElement varElement) { @@ -122,6 +126,11 @@ static ElementAnnotationContainer create(VariableElement varElement) { final boolean hasValid = uType.annotations().stream().anyMatch(ValidPrism::isInstance); + if (Util.isNonNullable(varElement)) { + var nonNull = UType.parse(APContext.typeElement(NonNullPrism.PRISM_TYPE).asType()); + annotations.put(nonNull, "Map.of(\"message\",\"{avaje.NotNull.message}\")"); + } + return new ElementAnnotationContainer( uType, hasValid, annotations, typeUse1, typeUse2, Map.of()); } diff --git a/validator-generator/src/main/java/io/avaje/validation/generator/TypeReader.java b/validator-generator/src/main/java/io/avaje/validation/generator/TypeReader.java index c05009e..d3a2d79 100644 --- a/validator-generator/src/main/java/io/avaje/validation/generator/TypeReader.java +++ b/validator-generator/src/main/java/io/avaje/validation/generator/TypeReader.java @@ -107,7 +107,7 @@ private void readField(Element element, List localFields) { element = mixInField; } - if (includeField(element)) { + if (includeField(element) || Util.isNonNullable(element)) { seenFields.add(element.toString()); var reader = new FieldReader(element, genericTypeParams); if (reader.hasConstraints() || ValidPrism.isPresent(element)) { diff --git a/validator-generator/src/main/java/io/avaje/validation/generator/Util.java b/validator-generator/src/main/java/io/avaje/validation/generator/Util.java index 2220848..218f555 100644 --- a/validator-generator/src/main/java/io/avaje/validation/generator/Util.java +++ b/validator-generator/src/main/java/io/avaje/validation/generator/Util.java @@ -168,4 +168,29 @@ static String valhalla() { return ""; } + static boolean isNonNullable(Element e) { + UType uType; + if (e instanceof final ExecutableElement executableElement) { + uType = UType.parse(executableElement.getReturnType()); + } else { + uType = UType.parse(e.asType()); + } + for (var mirror : uType.annotations()) { + if (mirror.getAnnotationType().toString().endsWith("Nullable")) { + return false; + } else if (NonNullPrism.isInstance(mirror)) { + return true; + } + } + return checkNullMarked(e); + } + + private static boolean checkNullMarked(Element e) { + if (e == null || NullUnmarkedPrism.isPresent(e)) { + return false; + } else if (NullMarkedPrism.isPresent(e)) { + return true; + } + return checkNullMarked(e.getEnclosingElement()); + } } diff --git a/validator-generator/src/main/java/io/avaje/validation/generator/package-info.java b/validator-generator/src/main/java/io/avaje/validation/generator/package-info.java index b919b67..51a8cfd 100644 --- a/validator-generator/src/main/java/io/avaje/validation/generator/package-info.java +++ b/validator-generator/src/main/java/io/avaje/validation/generator/package-info.java @@ -1,10 +1,13 @@ -@GeneratePrism(io.avaje.validation.ImportValidPojo.class) @GeneratePrism(io.avaje.validation.adapter.ConstraintAdapter.class) +@GeneratePrism(io.avaje.validation.ImportValidPojo.class) @GeneratePrism(io.avaje.validation.spi.MetaData.class) @GeneratePrism(io.avaje.validation.spi.MetaData.Factory.class) @GeneratePrism(io.avaje.validation.spi.MetaData.AnnotationFactory.class) -@GeneratePrism(io.avaje.validation.ValidMethod.class) @GeneratePrism(io.avaje.validation.MixIn.class) +@GeneratePrism(org.jspecify.annotations.NullMarked.class) +@GeneratePrism(org.jspecify.annotations.NullUnmarked.class) +@GeneratePrism(org.jspecify.annotations.NonNull.class) +@GeneratePrism(io.avaje.validation.ValidMethod.class) package io.avaje.validation.generator; import io.avaje.prism.GeneratePrism; diff --git a/validator-generator/src/test/java/io/avaje/validation/generator/models/valid/JSpecifyNotNull.java b/validator-generator/src/test/java/io/avaje/validation/generator/models/valid/JSpecifyNotNull.java new file mode 100644 index 0000000..98a88af --- /dev/null +++ b/validator-generator/src/test/java/io/avaje/validation/generator/models/valid/JSpecifyNotNull.java @@ -0,0 +1,10 @@ +package io.avaje.validation.generator.models.valid; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import jakarta.validation.Valid; + +@Valid +@NullMarked +public record JSpecifyNotNull(String basic, String withMax, @Nullable String withCustom) {} diff --git a/validator-generator/src/test/java/io/avaje/validation/generator/models/valid/JSpecifyNullUnmarked.java b/validator-generator/src/test/java/io/avaje/validation/generator/models/valid/JSpecifyNullUnmarked.java new file mode 100644 index 0000000..5892ad6 --- /dev/null +++ b/validator-generator/src/test/java/io/avaje/validation/generator/models/valid/JSpecifyNullUnmarked.java @@ -0,0 +1,9 @@ +package io.avaje.validation.generator.models.valid; + +import org.jspecify.annotations.NullUnmarked; + +import jakarta.validation.Valid; + +@Valid +@NullUnmarked +public record JSpecifyNullUnmarked(String basic, String withMax, String withCustom) {}