Skip to content

Commit 0bd9340

Browse files
timtebeekclaude
andauthored
Add Objects.requireNonNullElse/ElseGet support to AnnotateNullableParameters (#743)
* Add support for Objects.requireNonNull in AnnotateNullableParameters The recipe now recognizes Objects.requireNonNull calls as null-checking operations and will annotate parameters passed to requireNonNull with @nullable. This handles both standalone calls and assignment statements using requireNonNull. - Added Objects.requireNonNull to the list of null-checking method matchers - Added visitMethodInvocation to handle standalone requireNonNull calls - Added tests for single and multiple parameter cases with requireNonNull * Replace requireNonNull with requireNonNullElse/ElseGet support Objects.requireNonNull throws an exception if the argument is null, enforcing non-null parameters. In contrast, requireNonNullElse and requireNonNullElseGet provide default values for null arguments, indicating they accept nullable parameters. - Replaced Objects.requireNonNull with requireNonNullElse and requireNonNullElseGet - These methods imply the parameter can be null since they provide fallback values - Updated all tests to use the new methods that actually indicate nullable parameters * Merge tests * Only check the first argument, not any fallback * Add Optional.ofNullable support to AnnotateNullableParameters Added Optional.ofNullable to the list of null-checking methods in the AnnotateNullableParameters recipe. When a method parameter is passed to Optional.ofNullable(), it will now be annotated with @nullable to indicate that the parameter can accept null values. This improves the recipe's ability to identify nullable parameters, as Optional.ofNullable is specifically designed to handle potentially null values. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * Adjust test as suggested --------- Co-authored-by: Claude <[email protected]>
1 parent 6d6fe5e commit 0bd9340

File tree

2 files changed

+127
-0
lines changed

2 files changed

+127
-0
lines changed

src/main/java/org/openrewrite/staticanalysis/AnnotateNullableParameters.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ private Map<J.VariableDeclarations, J.Identifier> buildIdentifierMap(List<J.Vari
167167
* <ul>
168168
* <li>Direct null comparisons (param == null, param != null)</li>
169169
* <li>Known null-checking method calls (Objects.isNull, StringUtils.isBlank, etc.)</li>
170+
* <li>Methods that provide default values for null parameters (Objects.requireNonNullElse, Objects.requireNonNullElseGet)</li>
171+
* <li>Methods that handle nullable values (Optional.ofNullable)</li>
170172
* <li>Negated null-checking method calls (!Objects.isNull, !StringUtils.isBlank, etc.)</li>
171173
* </ul>
172174
*/
@@ -175,6 +177,9 @@ private static class NullCheckVisitor extends JavaIsoVisitor<Set<J.Identifier>>
175177
new MethodMatcher("com.google.common.base.Strings isNullOrEmpty(..)"), // Guava
176178
new MethodMatcher("java.util.Objects isNull(..)"),
177179
new MethodMatcher("java.util.Objects nonNull(..)"),
180+
new MethodMatcher("java.util.Objects requireNonNullElse(..)"), // Provides default for null
181+
new MethodMatcher("java.util.Objects requireNonNullElseGet(..)"), // Provides default for null
182+
new MethodMatcher("java.util.Optional ofNullable(..)"), // Handles nullable values
178183
new MethodMatcher("org.apache.commons.lang3.StringUtils isBlank(..)"),
179184
new MethodMatcher("org.apache.commons.lang3.StringUtils isEmpty(..)"),
180185
new MethodMatcher("org.apache.commons.lang3.StringUtils isNotBlank(..)"),
@@ -208,6 +213,18 @@ public J.If visitIf(J.If iff, Set<J.Identifier> nullCheckedParams) {
208213
return iff;
209214
}
210215

216+
@Override
217+
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, Set<J.Identifier> nullCheckedParams) {
218+
// Handle standalone Objects.requireNonNull calls (not just in if conditions)
219+
if (isKnownNullMethodChecker(method) && method.getArguments().get(0) instanceof J.Identifier) {
220+
J.Identifier firstArgument = (J.Identifier) method.getArguments().get(0);
221+
if (containsIdentifierByName(identifiers, firstArgument)) {
222+
nullCheckedParams.add(firstArgument);
223+
}
224+
}
225+
return super.visitMethodInvocation(method, nullCheckedParams);
226+
}
227+
211228
private void handleCondition(Expression condition, Set<J.Identifier> nullCheckedParams) {
212229
if (condition instanceof J.Binary) {
213230
handleBinary((J.Binary) condition, nullCheckedParams);

src/test/java/org/openrewrite/staticanalysis/AnnotateNullableParametersTest.java

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,116 @@ boolean bar(Object obj) {
378378
@Nested
379379
class KnownNullCheckers {
380380

381+
@Test
382+
void objectsRequireNonNullElse() {
383+
rewriteRun(
384+
//language=java
385+
java(
386+
"""
387+
import java.util.Objects;
388+
389+
class PersonBuilder {
390+
private String name;
391+
private String email;
392+
393+
public PersonBuilder setName(String name) {
394+
this.name = Objects.requireNonNullElse(name, "Unknown");
395+
return this;
396+
}
397+
398+
public PersonBuilder setNameWithFallback(String name, String fallback) {
399+
this.name = Objects.requireNonNullElse(name, fallback);
400+
return this;
401+
}
402+
403+
public PersonBuilder setEmail(String email) {
404+
this.email = Objects.requireNonNullElseGet(email, () -> "[email protected]");
405+
return this;
406+
}
407+
}
408+
""",
409+
"""
410+
import org.jspecify.annotations.Nullable;
411+
412+
import java.util.Objects;
413+
414+
class PersonBuilder {
415+
private String name;
416+
private String email;
417+
418+
public PersonBuilder setName(@Nullable String name) {
419+
this.name = Objects.requireNonNullElse(name, "Unknown");
420+
return this;
421+
}
422+
423+
public PersonBuilder setNameWithFallback(@Nullable String name, String fallback) {
424+
this.name = Objects.requireNonNullElse(name, fallback);
425+
return this;
426+
}
427+
428+
public PersonBuilder setEmail(@Nullable String email) {
429+
this.email = Objects.requireNonNullElseGet(email, () -> "[email protected]");
430+
return this;
431+
}
432+
}
433+
"""
434+
)
435+
);
436+
}
437+
438+
@Test
439+
void optionalOfNullable() {
440+
rewriteRun(
441+
//language=java
442+
java(
443+
"""
444+
import java.util.Optional;
445+
446+
public class PersonService {
447+
448+
public Optional<String> processValue(String value) {
449+
return Optional.ofNullable(value)
450+
.filter(v -> !v.isEmpty())
451+
.map(String::toUpperCase);
452+
}
453+
454+
public Optional<String> wrapValue(String input) {
455+
// Direct usage of parameter in Optional.ofNullable
456+
return Optional.ofNullable(input);
457+
}
458+
459+
public void conditionalWrap(String data) {
460+
Optional.ofNullable(data).ifPresent(d -> System.out.println(d));
461+
}
462+
}
463+
""",
464+
"""
465+
import org.jspecify.annotations.Nullable;
466+
467+
import java.util.Optional;
468+
469+
public class PersonService {
470+
471+
public Optional<String> processValue(@Nullable String value) {
472+
return Optional.ofNullable(value)
473+
.filter(v -> !v.isEmpty())
474+
.map(String::toUpperCase);
475+
}
476+
477+
public Optional<String> wrapValue(@Nullable String input) {
478+
// Direct usage of parameter in Optional.ofNullable
479+
return Optional.ofNullable(input);
480+
}
481+
482+
public void conditionalWrap(@Nullable String data) {
483+
Optional.ofNullable(data).ifPresent(d -> System.out.println(d));
484+
}
485+
}
486+
"""
487+
)
488+
);
489+
}
490+
381491
@CsvSource({
382492
"java.util.Objects, Objects.nonNull",
383493
"org.apache.commons.lang3.StringUtils, StringUtils.isNotBlank",

0 commit comments

Comments
 (0)