Conversation
JSpecify is de-facto standard annotations for marking nullability of variables, parameters, return values and fields. Supported by all IDEs. 1. By default, we mark everything as non-nullable (see @NullMarked in package-info.java) 2. When needed, we mark the nullable parameters/fields as @nullable.
|
Awesome! Just wondering, could this start breaking things for libraries which depend on us, or Kotlin projects? Just wonder if we release this, should this be 2.x release or 3.x ? |
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1764 +/- ##
============================================
+ Coverage 92.43% 92.45% +0.02%
+ Complexity 3450 3445 -5
============================================
Files 339 339
Lines 6794 6786 -8
Branches 670 661 -9
============================================
- Hits 6280 6274 -6
Misses 351 351
+ Partials 163 161 -2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
No, it doesn't need to be 3.0 |
|
Okay. My understanding was that before the code is annotated, Kotlin will treat this as platform types. So, the following code would work: Both would work. But if I look here: https://resources.jetbrains.com/storage/products/kotlinconf-2025/may-23/JSpecify_%20Java%20Nullness%20Annotations%20and%20Kotlin%20_%20David%20Baker.pdf, they say:
So, if we'd add @nullable to something Kotlin clients assumed was non-null, they'll get compile errors if they're using it in a non-null context without null checks. So, something like this might break Kotlin consumers, I think: Also, I think the code is perhaps incorrect, I don't think it should ever return a nullable userId. (and yes, I know what the documentation says, but it's also a bit sus) Wdyt? |
@bodiam Hm... Yes, that's right: the new annotations can break compilation of existing Kotlin code. val userId1: String = faker.credentials().userId(null);
So yes, we need to release it as DataFaker 3.0.0 :) P.S. And yes, good point: method |
method `faker.credentials.userId()` never returns null.
kingthorin
left a comment
There was a problem hiding this comment.
It's kind of a lot to review, especially with the unrelated typo fixes etc.
These are the only things I noticed.
| String regex = resolve("credentials.uid_pattern"); | ||
| return faker.regexify(regex); |
There was a problem hiding this comment.
Why not inline it and avoid the intermediate storage?
| of(Schema.of(field("boolean", () -> true)), "boolean: true" + System.lineSeparator()), | ||
| of(Schema.of(field("nullValue", () -> null)), "nullValue: null" + System.lineSeparator()), | ||
| of(Schema.of(field("array", () -> new String[]{null, "test", "123"})), | ||
| of(Schema.of(field("array", () -> new @Nullable String[]{null, "test", "123"})), |
There was a problem hiding this comment.
Are users of datafaker going to have to do this?
Why is it needed here but not the one just before?
There was a problem hiding this comment.
I don't know either, but a nullable string[] which is not null? looks weird. I think this could be removed?
| @@ -19,23 +21,16 @@ private SingletonLocale(Locale locale) { | |||
| this.locale = locale; | |||
| } | |||
|
|
|||
| public static SingletonLocale get(Locale locale) { | |||
| @Nullable | |||
| public static SingletonLocale get(@Nullable Locale locale) { | |||
| if (locale == null) { | |||
| return null; | |||
| } | |||
| SingletonLocale res = LOCALE2SINGLETON_LOCALE.get(locale); | |||
| if (res != null) { | |||
| return res; | |||
| } | |||
| synchronized (SingletonLocale.class) { | |||
| res = LOCALE2SINGLETON_LOCALE.get(locale); | |||
| if (res != null) { | |||
| return res; | |||
| } | |||
| res = new SingletonLocale(locale); | |||
| LOCALE2SINGLETON_LOCALE.put(locale, res); | |||
| return res; | |||
| } | |||
| return getRequired(locale); | |||
| } | |||
|
|
|||
| public static SingletonLocale getRequired(Locale locale) { | |||
| return LOCALE2SINGLETON_LOCALE.computeIfAbsent(locale, (__) -> new SingletonLocale(locale)); | |||
There was a problem hiding this comment.
doesn't match PR description
|
is there any enforcer that could fail the build if missed? |
Just to be clear on this: I do really like the PR, I think we should absolutely do it, just may need a bit of checking on the impact, and how we're communicating this, that's all. I have no issue with releasing DF 3.x either. |
| } | ||
|
|
||
| @Test | ||
| @SuppressWarnings("deprecation") |
There was a problem hiding this comment.
Why is this deprecation warning suppressed, instead of using the Locale.of method?
| private final @Nullable Function<Matcher, String> extractor; | ||
|
|
||
| Column(String columnName, int length, Pattern pattern2extract, Function<Matcher, String> extractor) { | ||
| Column(String columnName, int length, @Nullable Pattern pattern2extract, @Nullable Function<Matcher, String> extractor) { |
There was a problem hiding this comment.
I don't think this is correct. pattern2extract cannot be null, it would throw an NPE at line 246.
| private final @Nullable Function<Matcher, String> extractor; | ||
|
|
||
| Column(String columnName, int length, Pattern pattern2extract, Function<Matcher, String> extractor) { | ||
| Column(String columnName, int length, @Nullable Pattern pattern2extract, @Nullable Function<Matcher, String> extractor) { |
There was a problem hiding this comment.
same for extractor, if it's null, it would throw an exception at line 249/250. There's no guards/optional stuff at all on this.
| @ParameterizedTest | ||
| @MethodSource("dataParameters") | ||
| void testAllFakerMethodsThatReturnStrings(Locale locale, Random random) throws Exception { | ||
| void testAllFakerMethodsThatReturnStrings(@Nullable Locale locale, @Nullable Random random) throws Exception { |
There was a problem hiding this comment.
I have no idea if these should be nullable or not. Cannot image they should be.
| int numberOfWordsToAdd = randomWordsToAdd == 0 ? 0 : faker.random().nextInt(randomWordsToAdd); | ||
| final int totalWordCount = wordCount + numberOfWordsToAdd; | ||
| StringBuilder sb = new StringBuilder(); | ||
| StringBuilder sb = new StringBuilder(wordCount * 9); |
| private Object getObject(Schema<Object, ?> schema, Object result, Constructor<?> recordConstructor) { | ||
| final Field<Object, ?>[] fields = schema.getFields(); | ||
| final Object[] values = new Object[fields.length]; | ||
| final @Nullable Object[] values = new Object[fields.length]; |
There was a problem hiding this comment.
It's not really clear to me here wheat the intention is. Why can a final reference to values which is filled by an object be nullable? I don't think it can?
| private final String name; | ||
|
|
||
| public CompositeField(String name, Field<MyObject, MyType>[] fields) { | ||
| public CompositeField(@Nullable String name, Field<MyObject, MyType>[] fields) { |
There was a problem hiding this comment.
I don't understand why a compositeField can have a nullable name.
| } | ||
|
|
||
| private String generateBatchModeStatements(Schema<IN, ?> schema, List<IN> inputs, int limit) { | ||
| private String generateBatchModeStatements(Schema<IN, ?> schema, @Nullable List<IN> inputs, int limit) { |
There was a problem hiding this comment.
I don't think collection types should ever be nullable.
| private void writeSchema(StringBuilder sb, @Nullable IN input, Schema<IN, ?> schema, String pathPrefix) { | ||
| Field<IN, ?>[] fields = schema.getFields(); | ||
| Set<String> seen = new HashSet<>(); | ||
| Set<@Nullable String> seen = new HashSet<>(); |
There was a problem hiding this comment.
do you have a case when it might be null?
It looks like annotations sometimes were placed very randomly
|
@asolntsev can you explain how you put annotations? |
| * @throws IllegalArgumentException in case of invalid path | ||
| */ | ||
| public void addPath(Locale locale, Path path) { | ||
| public void addPath(Locale locale, @Nullable Path path) { |
There was a problem hiding this comment.
this doesn't seem to be correct. It's nullable, but when it is null, we thorw an IllegalArgumentException. Don't think that's correct?
|
I used Claude Code to review it as well: SummaryThis branch introduces JSpecify (@NullMarked, @nullable) and Error Prone (@CheckReturnValue) annotations across the entire codebase. It adds package-info.java files to 22 packages, making non-null the default for FindingsCriticalNone found. Improvements
Nitpicks
ConclusionApproved. This is a high-quality, thorough nullability annotation pass. The @NullMarked + @nullable approach with JSpecify is the modern best practice for Java, the provided scope is correct, and the various code The only item I'd suggest addressing before merge is the stale Javadoc on Credentials.userId() (finding #2). |
As I explained in the PR description:
So I added @nullable in places where i see that null is actually expected. Of course, I might make a mistake in some of these places (but I hope very few). |
JSpecify is de-facto standard annotations for marking nullability of variables, parameters, return values and fields. Supported by all IDEs.
All remarkable libraries have migrated to JSpecify: Spring, JUnit etc.