Skip to content

Commit 17df87f

Browse files
authored
Properly handle nested generics and multiple wildcard type args in JarInfer (#1114)
Our previous code would crash on nested generic types or when multiple unbounded wildcard type arguments were passed consecutively.
1 parent 728bf77 commit 17df87f

File tree

4 files changed

+140
-1
lines changed

4 files changed

+140
-1
lines changed

jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/DefinitelyDerefedParamsDriver.java

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.uber.nullaway.jarinfer;
1717

1818
import com.google.common.base.Preconditions;
19+
import com.google.common.base.Verify;
1920
import com.google.common.collect.ImmutableMap;
2021
import com.google.common.collect.ImmutableSet;
2122
import com.ibm.wala.cfg.ControlFlowGraph;
@@ -61,11 +62,13 @@
6162
import java.nio.file.Files;
6263
import java.nio.file.Paths;
6364
import java.nio.file.attribute.FileTime;
65+
import java.util.ArrayList;
6466
import java.util.Arrays;
6567
import java.util.Collections;
6668
import java.util.HashMap;
6769
import java.util.HashSet;
6870
import java.util.LinkedHashMap;
71+
import java.util.List;
6972
import java.util.Map;
7073
import java.util.Set;
7174
import java.util.jar.JarFile;
@@ -526,6 +529,13 @@ private static String getAstubxSignature(IMethod mtd) {
526529
// get types that include generic type arguments
527530
returnType = getSourceLevelQualifiedTypeName(genericSignature.getReturnType().toString());
528531
TypeSignature[] argTypeSigs = genericSignature.getArguments();
532+
Verify.verify(
533+
argTypeSigs.length == numParams,
534+
"Mismatch in number of parameters in generic signature: %s with %s vs %s with %s",
535+
mtd.getSignature(),
536+
numParams,
537+
genericSignature,
538+
argTypeSigs.length);
529539
for (int i = 0; i < argTypeSigs.length; i++) {
530540
argTypes[i] = getSourceLevelQualifiedTypeName(argTypeSigs[i].toString());
531541
}
@@ -591,7 +601,7 @@ private static String getSourceLevelQualifiedTypeName(String typeName) {
591601
int idx = typeName.indexOf("<");
592602
String baseType = typeName.substring(0, idx);
593603
// generic type args are separated by semicolons in signature stored in bytecodes
594-
String[] genericTypeArgs = typeName.substring(idx + 1, typeName.length() - 2).split(";");
604+
String[] genericTypeArgs = splitTypeArgs(typeName.substring(idx + 1, typeName.length() - 2));
595605
for (int i = 0; i < genericTypeArgs.length; i++) {
596606
genericTypeArgs[i] = getSourceLevelQualifiedTypeName(genericTypeArgs[i]);
597607
}
@@ -602,6 +612,51 @@ private static String getSourceLevelQualifiedTypeName(String typeName) {
602612
}
603613
}
604614

615+
/**
616+
* Splits out the top-level type arguments from a string representing all arguments (from the
617+
* bytecode-level signature)
618+
*
619+
* @param allTypeArgs string representing all type arguments
620+
* @return array of strings representing top-level type arguments
621+
*/
622+
private static String[] splitTypeArgs(String allTypeArgs) {
623+
List<String> result = new ArrayList<>();
624+
StringBuilder currentTypeArg = new StringBuilder();
625+
// track angle bracket depth to handle nested generic types
626+
int angleBracketDepth = 0;
627+
628+
for (int i = 0; i < allTypeArgs.length(); i++) {
629+
char c = allTypeArgs.charAt(i);
630+
if (c == '<') {
631+
angleBracketDepth++;
632+
currentTypeArg.append(c);
633+
} else if (c == '>') {
634+
angleBracketDepth--;
635+
currentTypeArg.append(c);
636+
} else if (c == '*' && angleBracketDepth == 0) {
637+
// Wildcard (not followed by semicolon)
638+
currentTypeArg.append(c);
639+
result.add(currentTypeArg.toString());
640+
currentTypeArg.setLength(0);
641+
} else if (c == ';' && angleBracketDepth == 0) {
642+
// Split on semicolon only if not nested within <>
643+
result.add(currentTypeArg.toString());
644+
currentTypeArg.setLength(0);
645+
} else {
646+
currentTypeArg.append(c);
647+
}
648+
}
649+
650+
// there should be no extra characters left
651+
Verify.verify(
652+
currentTypeArg.length() == 0,
653+
"unexpected characters left in generic type args string %s: %s",
654+
allTypeArgs,
655+
currentTypeArg);
656+
657+
return result.toArray(new String[0]);
658+
}
659+
605660
private static boolean isWildcard(String typeName) {
606661
char firstChar = typeName.charAt(0);
607662
return firstChar == '*' || firstChar == '+' || firstChar == '-';

jar-infer/jar-infer-lib/src/test/java/com/uber/nullaway/jarinfer/JarInferTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,27 @@ public void testMethodWithGenericParameter() throws Exception {
481481
"}");
482482
}
483483

484+
@Test
485+
public void nestedGeneric() throws Exception {
486+
testTemplate(
487+
"nestedGeneric",
488+
"generic",
489+
"TestGeneric",
490+
ImmutableMap.of(
491+
"generic.TestGeneric:java.lang.String getString(generic.TestGeneric.Generic<generic.TestGeneric.Generic<java.lang.String>>)",
492+
Sets.newHashSet(0)),
493+
"public class TestGeneric {",
494+
" static class Generic<T> {",
495+
" public String foo(T t) {",
496+
" return \"hi\";",
497+
" }",
498+
" }",
499+
" public String getString(Generic<Generic<String>> g) {",
500+
" return g.foo(null);",
501+
" }",
502+
"}");
503+
}
504+
484505
@Test
485506
public void wildcards() throws Exception {
486507
testTemplate(
@@ -508,6 +529,40 @@ public void wildcards() throws Exception {
508529
"}");
509530
}
510531

532+
@Test
533+
public void multiArgWildcards() throws Exception {
534+
testTemplate(
535+
"multiArgWildcards",
536+
"generic",
537+
"TestGeneric",
538+
ImmutableMap.of(
539+
"generic.TestGeneric:void genericMultiWildcard(java.lang.String, generic.TestGeneric.Generic<?,?>)",
540+
Sets.newHashSet(1)),
541+
"public class TestGeneric {",
542+
" public abstract static class Generic<T,U> {",
543+
" public void doNothing() {}",
544+
" }",
545+
" public static void genericMultiWildcard(String s, Generic<?,?> g) { g.doNothing(); };",
546+
"}");
547+
}
548+
549+
@Test
550+
public void nestedWildcard() throws Exception {
551+
testTemplate(
552+
"nestedWildcard",
553+
"generic",
554+
"TestGeneric",
555+
ImmutableMap.of(
556+
"generic.TestGeneric:void nestedWildcard(generic.TestGeneric.Generic<generic.TestGeneric.Generic<?>>)",
557+
Sets.newHashSet(0)),
558+
"public class TestGeneric {",
559+
" public abstract static class Generic<T> {",
560+
" public void doNothing() {}",
561+
" }",
562+
" public static void nestedWildcard(Generic<Generic<?>> g) { g.doNothing(); };",
563+
"}");
564+
}
565+
511566
@Test
512567
public void toyJARAnnotatingClasses() throws Exception {
513568
testAnnotationInJarTemplate(

jar-infer/nullaway-integration-test/src/test/java/com/uber/nullaway/jarinfer/JarInferIntegrationTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ public void genericsTest() {
8989
" g.getString(null);",
9090
" // BUG: Diagnostic contains: passing @Nullable parameter 'null'",
9191
" Toys.genericParam(null);",
92+
" // BUG: Diagnostic contains: passing @Nullable parameter 'null'",
93+
" Toys.nestedGenericParam(null);",
9294
" }",
9395
"}")
9496
.doTest();
@@ -114,9 +116,14 @@ public void wildcards() {
114116
" // BUG: Diagnostic contains: passing @Nullable parameter 'null'",
115117
" Toys.genericWildcard(null);",
116118
" // BUG: Diagnostic contains: passing @Nullable parameter 'null'",
119+
" Toys.nestedGenericWildcard(null);",
120+
" // BUG: Diagnostic contains: passing @Nullable parameter 'null'",
117121
" Toys.genericWildcardUpper(null);",
118122
" // BUG: Diagnostic contains: passing @Nullable parameter 'null'",
119123
" Toys.genericWildcardLower(null);",
124+
" // BUG: Diagnostic contains: passing @Nullable parameter 'null'",
125+
" Toys.doubleGenericWildcard(\"\", null);",
126+
" Toys.doubleGenericWildcardNullOk(\"\", null);",
120127
" }",
121128
"}")
122129
.doTest();

jar-infer/test-java-lib-jarinfer/src/main/java/com/uber/nullaway/jarinfer/toys/unannotated/Toys.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,18 @@ public static void genericParam(Generic<String> g) {
5151
g.getString("hello");
5252
}
5353

54+
public static void nestedGenericParam(Generic<Generic<String>> g) {
55+
g.getString(null);
56+
}
57+
5458
public static void genericWildcard(Generic<?> g) {
5559
g.doNothing();
5660
}
5761

62+
public static void nestedGenericWildcard(Generic<Generic<?>> g) {
63+
g.doNothing();
64+
}
65+
5866
public static String genericWildcardUpper(Generic<? extends String> g) {
5967
return g.getSomething();
6068
}
@@ -63,6 +71,20 @@ public static void genericWildcardLower(Generic<? super String> g) {
6371
g.getString("hello");
6472
}
6573

74+
public abstract static class DoubleGeneric<T, U> {
75+
public void doNothing() {}
76+
}
77+
78+
public static void doubleGenericWildcard(String s, DoubleGeneric<?, ?> g) {
79+
g.doNothing();
80+
}
81+
82+
public static void doubleGenericWildcardNullOk(String s, DoubleGeneric<?, ?> g) {
83+
if (g != null) {
84+
g.doNothing();
85+
}
86+
}
87+
6688
public static void main(String arg[]) throws java.io.IOException {
6789
String s = "test string...";
6890
Foo f = new Foo("let's");

0 commit comments

Comments
 (0)