-
Notifications
You must be signed in to change notification settings - Fork 88
Created AnnotateNullableParameters recipe #578
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
src/main/java/org/openrewrite/staticanalysis/AnnotateNullableParameters.java
Outdated
Show resolved
Hide resolved
src/main/java/org/openrewrite/staticanalysis/AnnotateNullableParameters.java
Outdated
Show resolved
Hide resolved
src/main/java/org/openrewrite/staticanalysis/AnnotateNullableParameters.java
Outdated
Show resolved
Hide resolved
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Great to see! Your design decisions make sense to me; indeed best to start conservatively and iterate from there. I imagine we'll discover more as we run these recipes, same as we saw with the recent ternary addition. Reading your description I'd briefly wondered if we'd want to cover cases like |
I had a similar thought about including methods like |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great addition @stefanodallapalma ! Look forward to seeing what undocumented nullable arguments this will turn up when run against OpenRewrite, Moderne and even Spring Framework 7. 🙏🏻 Hope it helps you there as well! I'm thinking of bundling up this recipe along with a few others into a JSpecify best practices either here or in rewrite-migrate-java.
@timtebeek Thank you for taking care of additional changes needed to clean up and tweak the recipe. They provide useful learning material for me to develop future recipes! One more consideration. We'll likely have to compare params' annotations against the target annotation's simple name (along with similar known annotations like CheckForNull rather than their fqn, in order to avoid duplicates). Think of the many Jakarta Nullable annotations used for DTOs or by Spotbugs, for example. I'll work on it after my holidays if that makes sense. |
On a different note, I love the idea of a JSpecify best practices bundle! |
You're welcome! Didn't want to leave you with a puzzle, but also wanted to not forget to cover those cases, hence why I only added the test earlier before I found time for the fix.
Thanks! We already have a few recipes to migrate off of alternative nullable annotations; I'd recommend folks run those first, possible as part of the JSpecify best practices before adding new annotations: Thanks again! |
* Handle nested types in AnnotateNullableMethods - As discovered on #578 * No need to update cursor after changing course on implementation
|
Ran the recipe just now, and saw a somewhat curious case: diff --git a/src/main/java/org/openrewrite/java/dependencies/internal/VersionParser.java b/src/main/java/org/openrewrite/java/dependencies/internal/VersionParser.java
index d8e85f0..a1fa7c9 100644
--- a/src/main/java/org/openrewrite/java/dependencies/internal/VersionParser.java
+++ b/src/main/java/org/openrewrite/java/dependencies/internal/VersionParser.java
@@ -16,6 +16,8 @@ org.openrewrite.staticanalysis.AnnotateNullableParameters
package org.openrewrite.java.dependencies.internal;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -92,7 +94,7 @@
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (obj == this) {
return true;
} Any quick thoughts? Should we exclude any methods that are overrides and assume annotations are placed on the parent class/interface? |
I understand, and that's the approach I would take as well. However, to provide context: I was recently standardizing Nullable annotations to jspecify in one of my repositories (using a simple type change) and encountered numerous conflicts where classes using Jakarta Nullable had fields that aren't present in jspecify. This is why running those recipes first may not be sufficient. I'll take a closer look at the ones you shared, and search for some reproducible examples. |
I can't see it in this diff, but usually equals have null checks. So, I'd say the recipe works as expected, but yes, we can think of excluding overrides. Maybe we can start with known methods like equals? Then see if it makes sense to exclude overridden methods as well? Tbh, I've no clues how static analysis tools behave if annotations are defined in the parent class but not in its subclasses. |
Thanks! For now I've pushed these two small fixes to reduce some of the noise otherwise seen after running this recipe: We're aiming for clear improvements here, and can look at a more fine grained approach for overridden methods later if needed. |
Another couple small fixes in 9deb55d ; we really ought to get you the tools to test these at scale. :) |
Overview
Automatically adds
@Nullable
annotations to method parameters that are explicitly null-checked within the method body. This helps teams incrementally adopt null safety annotations in existing codebases without manual effort.What's your motivation?
Many legacy codebases already contain defensive null checks but lack formal null safety annotations. If developers write if
(param == null)
, they clearly intended that parameter to be nullable. This recipe makes that existing intent explicit through annotations, enabling better static analysis and API documentation.What it detects
param == null
,param != null
Objects.isNull(param)
,StringUtils.isBlank(param)
, etc.!Objects.isNull(param)
,!StringUtils.isNotBlank(param)
param == null || param.isEmpty()
Before:
After:
Anything in particular you'd like reviewers to focus on?
Configuration
The default annotation is
org.jspecify.annotations.Nullable
as a widely used standard annotation and already default for the AnnotateNullableMethods recipe. The annotation can be customized via the recipe paramnullableAnnotationClass
(kept the same name as in AnnotateNullableMethods.java for consistency).Do note that the annotation detection only processes parameters not already annotated with the default or provided annotation (based on fully qualified name). This means that if the parameter has already a
@Nullable
annotation from a different package (e.g.,javax.annotation.Nullable
vsorg.jspecify.annotations.Nullable
), this recipe may end up adding a duplicate annotation. Should we compare against simple name instead to avoid this?.Limitations
I want to ensure these design decisions make sense:
Public methods only: The recipe only analyzes public methods to avoid over-annotating internal utility methods that may change more frequently than public API contracts. Public methods represent stable interfaces where null safety documentation provides the most value. Is this scope appropriate or too restrictive?
Direct if conditions only: Currently only detects null checks in direct if conditions (not in nested method calls or complex control flow). This only annotates when there's clear evidence of null checking while keeping the implementation simple and covering the most common patterns, but reviewers should consider if this misses too many real-world cases.
Predefined method matchers: The recipe uses a curated list of known null-checking methods rather than a more flexible pattern. This ensures precision but may miss custom utility methods. Should we provide a way to extend this list, or is the current approach sufficient?
No data flow analysis: The recipe assumes parameters aren't dereferenced before null checks. However, this assumption is intentionally conservative and aligns with the recipe's core purpose: documenting developer intent rather than verifying correctness. The presence of if
(param == null)
anywhere in the method is strong evidence of nullable intent, regardless of whether that check is technically correct or optimally placed. Should we add documentation clarifying this intent-focused approach, or is this reasoning sufficient?Anyone you would like to review specifically?
@timtebeek
Checklist