Merge "Apilint: Lint missing nullability annotations"
This commit is contained in:
@@ -79,6 +79,7 @@ class Field():
|
|||||||
self.value = raw[3].strip(';"')
|
self.value = raw[3].strip(';"')
|
||||||
else:
|
else:
|
||||||
self.value = None
|
self.value = None
|
||||||
|
self.annotations = []
|
||||||
|
|
||||||
self.ident = "-".join((self.typ, self.name, self.value or ""))
|
self.ident = "-".join((self.typ, self.name, self.value or ""))
|
||||||
|
|
||||||
@@ -88,6 +89,18 @@ class Field():
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.raw
|
return self.raw
|
||||||
|
|
||||||
|
|
||||||
|
class Argument(object):
|
||||||
|
|
||||||
|
__slots__ = ["type", "annotations", "name", "default"]
|
||||||
|
|
||||||
|
def __init__(self, type):
|
||||||
|
self.type = type
|
||||||
|
self.annotations = []
|
||||||
|
self.name = None
|
||||||
|
self.default = None
|
||||||
|
|
||||||
|
|
||||||
class Method():
|
class Method():
|
||||||
def __init__(self, clazz, line, raw, blame, sig_format = 1):
|
def __init__(self, clazz, line, raw, blame, sig_format = 1):
|
||||||
self.clazz = clazz
|
self.clazz = clazz
|
||||||
@@ -118,21 +131,24 @@ class Method():
|
|||||||
self.name = raw[1]
|
self.name = raw[1]
|
||||||
|
|
||||||
# parse args
|
# parse args
|
||||||
self.args = []
|
self.detailed_args = []
|
||||||
for arg in re.split(",\s*", raw_args):
|
for arg in re.split(",\s*", raw_args):
|
||||||
arg = re.split("\s", arg)
|
arg = re.split("\s", arg)
|
||||||
# ignore annotations for now
|
# ignore annotations for now
|
||||||
arg = [ a for a in arg if not a.startswith("@") ]
|
arg = [ a for a in arg if not a.startswith("@") ]
|
||||||
if len(arg[0]) > 0:
|
if len(arg[0]) > 0:
|
||||||
self.args.append(arg[0])
|
self.detailed_args.append(Argument(arg[0]))
|
||||||
|
|
||||||
# parse throws
|
# parse throws
|
||||||
self.throws = []
|
self.throws = []
|
||||||
for throw in re.split(",\s*", raw_throws):
|
for throw in re.split(",\s*", raw_throws):
|
||||||
self.throws.append(throw)
|
self.throws.append(throw)
|
||||||
|
|
||||||
|
self.annotations = []
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown signature format: " + sig_format)
|
raise ValueError("Unknown signature format: " + sig_format)
|
||||||
|
|
||||||
|
self.args = map(lambda a: a.type, self.detailed_args)
|
||||||
self.ident = "-".join((self.typ, self.name, "-".join(self.args)))
|
self.ident = "-".join((self.typ, self.name, "-".join(self.args)))
|
||||||
|
|
||||||
def sig_matches(self, typ, name, args):
|
def sig_matches(self, typ, name, args):
|
||||||
@@ -312,10 +328,10 @@ class V2LineParser(object):
|
|||||||
method.split = []
|
method.split = []
|
||||||
kind = self.parse_one_of("ctor", "method")
|
kind = self.parse_one_of("ctor", "method")
|
||||||
method.split.append(kind)
|
method.split.append(kind)
|
||||||
annotations = self.parse_annotations()
|
method.annotations = self.parse_annotations()
|
||||||
method.split.extend(self.parse_modifiers())
|
method.split.extend(self.parse_modifiers())
|
||||||
self.parse_matching_paren("<", ">")
|
self.parse_matching_paren("<", ">")
|
||||||
if "@Deprecated" in annotations:
|
if "@Deprecated" in method.annotations:
|
||||||
method.split.append("deprecated")
|
method.split.append("deprecated")
|
||||||
if kind == "ctor":
|
if kind == "ctor":
|
||||||
method.typ = "ctor"
|
method.typ = "ctor"
|
||||||
@@ -325,7 +341,7 @@ class V2LineParser(object):
|
|||||||
method.name = self.parse_name()
|
method.name = self.parse_name()
|
||||||
method.split.append(method.name)
|
method.split.append(method.name)
|
||||||
self.parse_token("(")
|
self.parse_token("(")
|
||||||
method.args = self.parse_args()
|
method.detailed_args = self.parse_args()
|
||||||
self.parse_token(")")
|
self.parse_token(")")
|
||||||
method.throws = self.parse_throws()
|
method.throws = self.parse_throws()
|
||||||
if "@interface" in method.clazz.split:
|
if "@interface" in method.clazz.split:
|
||||||
@@ -360,8 +376,8 @@ class V2LineParser(object):
|
|||||||
def parse_into_field(self, field):
|
def parse_into_field(self, field):
|
||||||
kind = self.parse_one_of(*V2LineParser.FIELD_KINDS)
|
kind = self.parse_one_of(*V2LineParser.FIELD_KINDS)
|
||||||
field.split = [kind]
|
field.split = [kind]
|
||||||
annotations = self.parse_annotations()
|
field.annotations = self.parse_annotations()
|
||||||
if "@Deprecated" in annotations:
|
if "@Deprecated" in field.annotations:
|
||||||
field.split.append("deprecated")
|
field.split.append("deprecated")
|
||||||
field.split.extend(self.parse_modifiers())
|
field.split.extend(self.parse_modifiers())
|
||||||
field.typ = self.parse_type()
|
field.typ = self.parse_type()
|
||||||
@@ -488,15 +504,16 @@ class V2LineParser(object):
|
|||||||
|
|
||||||
def parse_arg(self):
|
def parse_arg(self):
|
||||||
self.parse_if("vararg") # kotlin vararg
|
self.parse_if("vararg") # kotlin vararg
|
||||||
self.parse_annotations()
|
annotations = self.parse_annotations()
|
||||||
type = self.parse_arg_type()
|
arg = Argument(self.parse_arg_type())
|
||||||
|
arg.annotations = annotations
|
||||||
l = self.lookahead()
|
l = self.lookahead()
|
||||||
if l != "," and l != ")":
|
if l != "," and l != ")":
|
||||||
if self.lookahead() != '=':
|
if self.lookahead() != '=':
|
||||||
self.parse_token() # kotlin argument name
|
arg.name = self.parse_token() # kotlin argument name
|
||||||
if self.parse_if('='): # kotlin default value
|
if self.parse_if('='): # kotlin default value
|
||||||
self.parse_expression()
|
arg.default = self.parse_expression()
|
||||||
return type
|
return arg
|
||||||
|
|
||||||
def parse_expression(self):
|
def parse_expression(self):
|
||||||
while not self.lookahead() in [')', ',', ';']:
|
while not self.lookahead() in [')', ',', ';']:
|
||||||
@@ -593,7 +610,7 @@ def _parse_stream_to_generator(f):
|
|||||||
blame = None
|
blame = None
|
||||||
sig_format = 1
|
sig_format = 1
|
||||||
|
|
||||||
re_blame = re.compile("^([a-z0-9]{7,}) \(<([^>]+)>.+?\) (.+?)$")
|
re_blame = re.compile(r"^(\^?[a-z0-9]{7,}) \(<([^>]+)>.+?\) (.+?)$")
|
||||||
|
|
||||||
field_prefixes = map(lambda kind: " %s" % (kind,), V2LineParser.FIELD_KINDS)
|
field_prefixes = map(lambda kind: " %s" % (kind,), V2LineParser.FIELD_KINDS)
|
||||||
def startsWithFieldPrefix(raw):
|
def startsWithFieldPrefix(raw):
|
||||||
@@ -608,11 +625,13 @@ def _parse_stream_to_generator(f):
|
|||||||
match = re_blame.match(raw)
|
match = re_blame.match(raw)
|
||||||
if match is not None:
|
if match is not None:
|
||||||
blame = match.groups()[0:2]
|
blame = match.groups()[0:2]
|
||||||
|
if blame[0].startswith("^"): # Outside of blame range
|
||||||
|
blame = None
|
||||||
raw = match.groups()[2]
|
raw = match.groups()[2]
|
||||||
else:
|
else:
|
||||||
blame = None
|
blame = None
|
||||||
|
|
||||||
if line == 1 and raw.startswith("// Signature format: "):
|
if line == 1 and V2Tokenizer.SIGNATURE_PREFIX in raw:
|
||||||
sig_format_string = raw[len(V2Tokenizer.SIGNATURE_PREFIX):]
|
sig_format_string = raw[len(V2Tokenizer.SIGNATURE_PREFIX):]
|
||||||
if sig_format_string in ["2.0", "3.0"]:
|
if sig_format_string in ["2.0", "3.0"]:
|
||||||
sig_format = 2
|
sig_format = 2
|
||||||
@@ -1871,6 +1890,35 @@ def verify_numbers(clazz):
|
|||||||
if arg in discouraged:
|
if arg in discouraged:
|
||||||
warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead")
|
warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead")
|
||||||
|
|
||||||
|
PRIMITIVES = {"void", "int", "float", "boolean", "short", "char", "byte", "long", "double"}
|
||||||
|
|
||||||
|
def verify_nullability(clazz):
|
||||||
|
"""Catches missing nullability annotations"""
|
||||||
|
|
||||||
|
for f in clazz.fields:
|
||||||
|
if f.value is not None and 'static' in f.split and 'final' in f.split:
|
||||||
|
continue # Nullability of constants can be inferred.
|
||||||
|
if f.typ not in PRIMITIVES and not has_nullability(f.annotations):
|
||||||
|
error(clazz, f, "M12", "Field must be marked either @NonNull or @Nullable")
|
||||||
|
|
||||||
|
for c in clazz.ctors:
|
||||||
|
verify_nullability_args(clazz, c)
|
||||||
|
|
||||||
|
for m in clazz.methods:
|
||||||
|
if m.name == "writeToParcel" or m.name == "onReceive":
|
||||||
|
continue # Parcelable.writeToParcel() and BroadcastReceiver.onReceive() are not yet annotated
|
||||||
|
|
||||||
|
if m.typ not in PRIMITIVES and not has_nullability(m.annotations):
|
||||||
|
error(clazz, m, "M12", "Return value must be marked either @NonNull or @Nullable")
|
||||||
|
verify_nullability_args(clazz, m)
|
||||||
|
|
||||||
|
def verify_nullability_args(clazz, m):
|
||||||
|
for i, arg in enumerate(m.detailed_args):
|
||||||
|
if arg.type not in PRIMITIVES and not has_nullability(arg.annotations):
|
||||||
|
error(clazz, m, "M12", "Argument %d must be marked either @NonNull or @Nullable" % (i+1,))
|
||||||
|
|
||||||
|
def has_nullability(annotations):
|
||||||
|
return "@NonNull" in annotations or "@Nullable" in annotations
|
||||||
|
|
||||||
def verify_singleton(clazz):
|
def verify_singleton(clazz):
|
||||||
"""Catch singleton objects with constructors."""
|
"""Catch singleton objects with constructors."""
|
||||||
@@ -1959,6 +2007,7 @@ def examine_clazz(clazz):
|
|||||||
verify_pfd(clazz)
|
verify_pfd(clazz)
|
||||||
verify_numbers(clazz)
|
verify_numbers(clazz)
|
||||||
verify_singleton(clazz)
|
verify_singleton(clazz)
|
||||||
|
verify_nullability(clazz)
|
||||||
|
|
||||||
|
|
||||||
def examine_stream(stream, base_stream=None, in_classes_with_base=[], out_classes_with_base=None):
|
def examine_stream(stream, base_stream=None, in_classes_with_base=[], out_classes_with_base=None):
|
||||||
|
|||||||
@@ -88,20 +88,22 @@ class UtilTests(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
faulty_current_txt = """
|
faulty_current_txt = """
|
||||||
|
// Signature format: 2.0
|
||||||
package android.app {
|
package android.app {
|
||||||
public final class Activity {
|
public final class Activity {
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class WallpaperColors implements android.os.Parcelable {
|
public final class WallpaperColors implements android.os.Parcelable {
|
||||||
ctor public WallpaperColors(android.os.Parcel);
|
ctor public WallpaperColors(@NonNull android.os.Parcel);
|
||||||
method public int describeContents();
|
method public int describeContents();
|
||||||
method public void writeToParcel(android.os.Parcel, int);
|
method public void writeToParcel(@NonNull android.os.Parcel, int);
|
||||||
field public static final android.os.Parcelable.Creator<android.app.WallpaperColors> CREATOR;
|
field @NonNull public static final android.os.Parcelable.Creator<android.app.WallpaperColors> CREATOR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""".split('\n')
|
""".strip().split('\n')
|
||||||
|
|
||||||
ok_current_txt = """
|
ok_current_txt = """
|
||||||
|
// Signature format: 2.0
|
||||||
package android.app {
|
package android.app {
|
||||||
public final class Activity {
|
public final class Activity {
|
||||||
}
|
}
|
||||||
@@ -109,19 +111,20 @@ package android.app {
|
|||||||
public final class WallpaperColors implements android.os.Parcelable {
|
public final class WallpaperColors implements android.os.Parcelable {
|
||||||
ctor public WallpaperColors();
|
ctor public WallpaperColors();
|
||||||
method public int describeContents();
|
method public int describeContents();
|
||||||
method public void writeToParcel(android.os.Parcel, int);
|
method public void writeToParcel(@NonNull android.os.Parcel, int);
|
||||||
field public static final android.os.Parcelable.Creator<android.app.WallpaperColors> CREATOR;
|
field @NonNull public static final android.os.Parcelable.Creator<android.app.WallpaperColors> CREATOR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""".split('\n')
|
""".strip().split('\n')
|
||||||
|
|
||||||
system_current_txt = """
|
system_current_txt = """
|
||||||
|
// Signature format: 2.0
|
||||||
package android.app {
|
package android.app {
|
||||||
public final class WallpaperColors implements android.os.Parcelable {
|
public final class WallpaperColors implements android.os.Parcelable {
|
||||||
method public int getSomething();
|
method public int getSomething();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""".split('\n')
|
""".strip().split('\n')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user