Skip to content

Commit f8595b1

Browse files
stefanodallapalmastedapagithub-actions[bot]timtebeek
authored
AnnotateNullableMethods: annotate methods that return nullable method… (#738)
* AnnotateNullableMethods: annotate methods that return nullable method invocations * Update src/main/java/org/openrewrite/staticanalysis/AnnotateNullableMethods.java Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Minor polish * Clarify scope of search and use `reduce` --------- Co-authored-by: stefanod <stefano.dallapalma@adyen.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tim te Beek <tim@moderne.io>
1 parent add1534 commit f8595b1

File tree

2 files changed

+120
-5
lines changed

2 files changed

+120
-5
lines changed

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

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.Arrays;
3030
import java.util.Comparator;
3131
import java.util.List;
32+
import java.util.Optional;
3233
import java.util.concurrent.atomic.AtomicBoolean;
3334

3435
@EqualsAndHashCode(callSuper = false)
@@ -86,7 +87,7 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration methodDecl
8687

8788
J.MethodDeclaration md = super.visitMethodDeclaration(methodDeclaration, ctx);
8889
updateCursor(md);
89-
if (FindNullableReturnStatements.find(md.getBody(), getCursor().getParentTreeCursor())) {
90+
if (FindNullableReturnStatements.find(md.getBody(), getCursor().getParentTreeCursor(), nullableAnnotationClass)) {
9091
J.MethodDeclaration annotatedMethod = JavaTemplate.builder("@" + fullyQualifiedName)
9192
.javaParser(JavaParser.fromJavaVersion().dependsOn(
9293
String.format("package %s;public @interface %s {}", fullyQualifiedPackage, simpleName)))
@@ -145,8 +146,14 @@ private static class FindNullableReturnStatements extends JavaIsoVisitor<AtomicB
145146
new MethodMatcher("java.util.Spliterator trySplit(..)")
146147
);
147148

148-
static boolean find(@Nullable J subtree, Cursor parentTreeCursor) {
149-
return new FindNullableReturnStatements().reduce(subtree, new AtomicBoolean(), parentTreeCursor).get();
149+
private final String nullableAnnotationClass;
150+
151+
private FindNullableReturnStatements(@Nullable String nullableAnnotationClass) {
152+
this.nullableAnnotationClass = Optional.ofNullable(nullableAnnotationClass).orElse(DEFAULT_NULLABLE_ANN_CLASS);
153+
}
154+
155+
static boolean find(@Nullable J subtree, Cursor parentTreeCursor, @Nullable String nullableAnnotationClass) {
156+
return new FindNullableReturnStatements(nullableAnnotationClass).reduce(subtree, new AtomicBoolean(), parentTreeCursor).get();
150157
}
151158

152159
@Override
@@ -176,7 +183,7 @@ private boolean maybeIsNull(@Nullable Expression returnExpression) {
176183
return ((J.Literal) returnExpression).getValue() == null;
177184
}
178185
if (returnExpression instanceof J.MethodInvocation) {
179-
return isKnowNullableMethod((J.MethodInvocation) returnExpression);
186+
return isLocalNullableMethod((J.MethodInvocation) returnExpression) || isKnownNullableMethod((J.MethodInvocation) returnExpression);
180187
}
181188
if (returnExpression instanceof J.Ternary) {
182189
J.Ternary ternary = (J.Ternary) returnExpression;
@@ -185,13 +192,42 @@ private boolean maybeIsNull(@Nullable Expression returnExpression) {
185192
return false;
186193
}
187194

188-
private boolean isKnowNullableMethod(J.MethodInvocation methodInvocation) {
195+
private boolean isKnownNullableMethod(J.MethodInvocation methodInvocation) {
189196
for (MethodMatcher m : KNOWN_NULLABLE_METHODS) {
190197
if (m.matches(methodInvocation)) {
191198
return true;
192199
}
193200
}
194201
return false;
195202
}
203+
204+
private boolean isLocalNullableMethod(J.MethodInvocation methodInvocation) {
205+
JavaType.Method targetMethod = methodInvocation.getMethodType();
206+
if (targetMethod == null) {
207+
return false;
208+
}
209+
210+
// Visit the entire compilation unit to find the method declaration
211+
AnnotationMatcher annotationMatcher = new AnnotationMatcher("@" + nullableAnnotationClass);
212+
J.CompilationUnit cu = getCursor().firstEnclosingOrThrow(J.CompilationUnit.class);
213+
return new JavaIsoVisitor<AtomicBoolean>() {
214+
@Override
215+
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, AtomicBoolean p) {
216+
if (p.get()) {
217+
return method;
218+
}
219+
if (targetMethod.equals(method.getMethodType()) && method.getReturnTypeExpression() instanceof J.AnnotatedType) {
220+
for (J.Annotation annotation : ((J.AnnotatedType) method.getReturnTypeExpression()).getAnnotations()) {
221+
if (annotationMatcher.matches(annotation)) {
222+
p.set(true);
223+
break;
224+
}
225+
}
226+
return method;
227+
}
228+
return super.visitMethodDeclaration(method, p);
229+
}
230+
}.reduce(cu, new AtomicBoolean(false)).get();
231+
}
196232
}
197233
}

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,4 +373,83 @@ public class Foo {
373373
)
374374
);
375375
}
376+
377+
@Test
378+
void nullableMethodsInvocationsWithDefaultNullableClass() {
379+
rewriteRun(
380+
//language=java
381+
java(
382+
"""
383+
import org.jspecify.annotations.Nullable;
384+
385+
import java.util.Random;
386+
387+
public class Test {
388+
public @Nullable String maybeNullString() {
389+
return new Random().nextBoolean() ? "Not null" : null;
390+
}
391+
392+
public String getString() {
393+
return maybeNullString();
394+
}
395+
}
396+
""",
397+
"""
398+
import org.jspecify.annotations.Nullable;
399+
400+
import java.util.Random;
401+
402+
public class Test {
403+
public @Nullable String maybeNullString() {
404+
return new Random().nextBoolean() ? "Not null" : null;
405+
}
406+
407+
public @Nullable String getString() {
408+
return maybeNullString();
409+
}
410+
}
411+
"""
412+
)
413+
);
414+
}
415+
416+
@Test
417+
void nullableMethodsInvocationsWithCustomNullableClass() {
418+
rewriteRun(
419+
spec -> spec.recipe(new AnnotateNullableMethods("org.openrewrite.jgit.annotations.Nullable")),
420+
//language=java
421+
java(
422+
"""
423+
import org.openrewrite.jgit.annotations.Nullable;
424+
425+
import java.util.Random;
426+
427+
public class Test {
428+
public @Nullable String maybeNullString() {
429+
return new Random().nextBoolean() ? "Not null" : null;
430+
}
431+
432+
public String getString() {
433+
return maybeNullString();
434+
}
435+
}
436+
""",
437+
"""
438+
import org.openrewrite.jgit.annotations.Nullable;
439+
440+
import java.util.Random;
441+
442+
public class Test {
443+
public @Nullable String maybeNullString() {
444+
return new Random().nextBoolean() ? "Not null" : null;
445+
}
446+
447+
public @Nullable String getString() {
448+
return maybeNullString();
449+
}
450+
}
451+
"""
452+
)
453+
);
454+
}
376455
}

0 commit comments

Comments
 (0)