summaryrefslogtreecommitdiff
path: root/tools/apilint/apilint.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/apilint/apilint.py')
-rw-r--r--tools/apilint/apilint.py171
1 files changed, 155 insertions, 16 deletions
diff --git a/tools/apilint/apilint.py b/tools/apilint/apilint.py
index 70a47cf42755..91cd1cba964d 100644
--- a/tools/apilint/apilint.py
+++ b/tools/apilint/apilint.py
@@ -69,12 +69,19 @@ class Field():
self.raw = raw.strip(" {;")
self.blame = blame
+ # drop generics for now; may need multiple passes
+ raw = re.sub("<[^<]+?>", "", raw)
+ raw = re.sub("<[^<]+?>", "", raw)
+
raw = raw.split()
self.split = list(raw)
for r in ["field", "volatile", "transient", "public", "protected", "static", "final", "deprecated"]:
while r in raw: raw.remove(r)
+ # ignore annotations for now
+ raw = [ r for r in raw if not r.startswith("@") ]
+
self.typ = raw[0]
self.name = raw[1].strip(";")
if len(raw) >= 4 and raw[2] == "=":
@@ -97,25 +104,39 @@ class Method():
self.raw = raw.strip(" {;")
self.blame = blame
- # drop generics for now
- raw = re.sub("<.+?>", "", raw)
+ # drop generics for now; may need multiple passes
+ raw = re.sub("<[^<]+?>", "", raw)
+ raw = re.sub("<[^<]+?>", "", raw)
- raw = re.split("[\s(),;]+", raw)
+ # handle each clause differently
+ raw_prefix, raw_args, _, raw_throws = re.match(r"(.*?)\((.*?)\)( throws )?(.*?);$", raw).groups()
+
+ # parse prefixes
+ raw = re.split("[\s]+", raw_prefix)
for r in ["", ";"]:
while r in raw: raw.remove(r)
self.split = list(raw)
- for r in ["method", "public", "protected", "static", "final", "deprecated", "abstract", "default"]:
+ for r in ["method", "public", "protected", "static", "final", "deprecated", "abstract", "default", "operator"]:
while r in raw: raw.remove(r)
self.typ = raw[0]
self.name = raw[1]
+
+ # parse args
self.args = []
+ for arg in re.split(",\s*", raw_args):
+ arg = re.split("\s", arg)
+ # ignore annotations for now
+ arg = [ a for a in arg if not a.startswith("@") ]
+ if len(arg[0]) > 0:
+ self.args.append(arg[0])
+
+ # parse throws
self.throws = []
- target = self.args
- for r in raw[2:]:
- if r == "throws": target = self.throws
- else: target.append(r)
+ for throw in re.split(",\s*", raw_throws):
+ self.throws.append(throw)
+
self.ident = ident(self.raw)
def __hash__(self):
@@ -135,12 +156,18 @@ class Class():
self.fields = []
self.methods = []
+ # drop generics for now; may need multiple passes
+ raw = re.sub("<[^<]+?>", "", raw)
+ raw = re.sub("<[^<]+?>", "", raw)
+
raw = raw.split()
self.split = list(raw)
if "class" in raw:
self.fullname = raw[raw.index("class")+1]
elif "interface" in raw:
self.fullname = raw[raw.index("interface")+1]
+ elif "@interface" in raw:
+ self.fullname = raw[raw.index("@interface")+1]
else:
raise ValueError("Funky class type %s" % (self.raw))
@@ -465,6 +492,7 @@ def verify_parcelable(clazz):
def verify_protected(clazz):
"""Verify that no protected methods or fields are allowed."""
for m in clazz.methods:
+ if m.name == "finalize": continue
if "protected" in m.split:
error(clazz, m, "M7", "Protected methods not allowed; must be public")
for f in clazz.fields:
@@ -670,8 +698,8 @@ def verify_layering(clazz):
"android.provider",
["android.content","android.graphics.drawable"],
"android.database",
- "android.graphics",
"android.text",
+ "android.graphics",
"android.os",
"android.util"
]
@@ -998,6 +1026,10 @@ def verify_resource_names(clazz):
# Resources defined by files are foo_bar_baz
if clazz.name in ["anim","animator","color","dimen","drawable","interpolator","layout","transition","menu","mipmap","string","plurals","raw","xml"]:
for f in clazz.fields:
+ if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue
+ if f.name.startswith("config_"):
+ error(clazz, f, None, "Expected config name to be config_fooBarBaz style")
+
if re.match("[a-z1-9_]+$", f.name): continue
error(clazz, f, None, "Expected resource name in this class to be foo_bar_baz style")
@@ -1334,18 +1366,79 @@ def verify_clone(clazz):
error(clazz, m, None, "Provide an explicit copy constructor instead of implementing clone()")
+def verify_pfd(clazz):
+ """Verify that android APIs use PFD over FD."""
+ examine = clazz.ctors + clazz.methods
+ for m in examine:
+ if m.typ == "java.io.FileDescriptor":
+ error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
+ if m.typ == "int":
+ if "Fd" in m.name or "FD" in m.name or "FileDescriptor" in m.name:
+ error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
+ for arg in m.args:
+ if arg == "java.io.FileDescriptor":
+ error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
+
+ for f in clazz.fields:
+ if f.typ == "java.io.FileDescriptor":
+ error(clazz, f, "FW11", "Must use ParcelFileDescriptor")
+
+
+def verify_numbers(clazz):
+ """Discourage small numbers types like short and byte."""
+
+ discouraged = ["short","byte"]
+
+ for c in clazz.ctors:
+ for arg in c.args:
+ if arg in discouraged:
+ warn(clazz, c, "FW12", "Should avoid odd sized primitives; use int instead")
+
+ for f in clazz.fields:
+ if f.typ in discouraged:
+ warn(clazz, f, "FW12", "Should avoid odd sized primitives; use int instead")
+
+ for m in clazz.methods:
+ if m.typ in discouraged:
+ warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead")
+ for arg in m.args:
+ if arg in discouraged:
+ warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead")
+
+
+def verify_singleton(clazz):
+ """Catch singleton objects with constructors."""
+
+ singleton = False
+ for m in clazz.methods:
+ if m.name.startswith("get") and m.name.endswith("Instance") and " static " in m.raw:
+ singleton = True
+
+ if singleton:
+ for c in clazz.ctors:
+ error(clazz, c, None, "Singleton classes should use getInstance() methods")
+
+
+
+def is_interesting(clazz):
+ """Test if given class is interesting from an Android PoV."""
+
+ if clazz.pkg.name.startswith("java"): return False
+ if clazz.pkg.name.startswith("junit"): return False
+ if clazz.pkg.name.startswith("org.apache"): return False
+ if clazz.pkg.name.startswith("org.xml"): return False
+ if clazz.pkg.name.startswith("org.json"): return False
+ if clazz.pkg.name.startswith("org.w3c"): return False
+ if clazz.pkg.name.startswith("android.icu."): return False
+ return True
+
+
def examine_clazz(clazz):
"""Find all style issues in the given class."""
notice(clazz)
- if clazz.pkg.name.startswith("java"): return
- if clazz.pkg.name.startswith("junit"): return
- if clazz.pkg.name.startswith("org.apache"): return
- if clazz.pkg.name.startswith("org.xml"): return
- if clazz.pkg.name.startswith("org.json"): return
- if clazz.pkg.name.startswith("org.w3c"): return
- if clazz.pkg.name.startswith("android.icu."): return
+ if not is_interesting(clazz): return
verify_constants(clazz)
verify_enums(clazz)
@@ -1397,6 +1490,9 @@ def examine_clazz(clazz):
verify_tense(clazz)
verify_icu(clazz)
verify_clone(clazz)
+ verify_pfd(clazz)
+ verify_numbers(clazz)
+ verify_singleton(clazz)
def examine_stream(stream):
@@ -1479,6 +1575,7 @@ def show_deprecations_at_birth(cur, prev):
# Remove all existing things so we're left with new
for prev_clazz in prev.values():
cur_clazz = cur[prev_clazz.fullname]
+ if not is_interesting(cur_clazz): continue
sigs = { i.ident: i for i in prev_clazz.ctors }
cur_clazz.ctors = [ i for i in cur_clazz.ctors if i.ident not in sigs ]
@@ -1506,6 +1603,38 @@ def show_deprecations_at_birth(cur, prev):
print
+def show_stats(cur, prev):
+ """Show API stats."""
+
+ stats = collections.defaultdict(int)
+ for cur_clazz in cur.values():
+ if not is_interesting(cur_clazz): continue
+
+ if cur_clazz.fullname not in prev:
+ stats['new_classes'] += 1
+ stats['new_ctors'] += len(cur_clazz.ctors)
+ stats['new_methods'] += len(cur_clazz.methods)
+ stats['new_fields'] += len(cur_clazz.fields)
+ else:
+ prev_clazz = prev[cur_clazz.fullname]
+
+ sigs = { i.ident: i for i in prev_clazz.ctors }
+ ctors = len([ i for i in cur_clazz.ctors if i.ident not in sigs ])
+ sigs = { i.ident: i for i in prev_clazz.methods }
+ methods = len([ i for i in cur_clazz.methods if i.ident not in sigs ])
+ sigs = { i.ident: i for i in prev_clazz.fields }
+ fields = len([ i for i in cur_clazz.fields if i.ident not in sigs ])
+
+ if ctors + methods + fields > 0:
+ stats['extend_classes'] += 1
+ stats['extend_ctors'] += ctors
+ stats['extend_methods'] += methods
+ stats['extend_fields'] += fields
+
+ print "#", "".join([ k.ljust(20) for k in sorted(stats.keys()) ])
+ print " ", "".join([ str(stats[k]).ljust(20) for k in sorted(stats.keys()) ])
+
+
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Enforces common Android public API design \
patterns. It ignores lint messages from a previous API level, if provided.")
@@ -1520,6 +1649,8 @@ if __name__ == "__main__":
help="Show API changes noticed")
parser.add_argument("--show-deprecations-at-birth", action='store_const', const=True,
help="Show API deprecations at birth")
+ parser.add_argument("--show-stats", action='store_const', const=True,
+ help="Show API stats")
args = vars(parser.parse_args())
if args['no_color']:
@@ -1539,6 +1670,14 @@ if __name__ == "__main__":
show_deprecations_at_birth(cur, prev)
sys.exit()
+ if args['show_stats']:
+ with current_file as f:
+ cur = _parse_stream(f)
+ with previous_file as f:
+ prev = _parse_stream(f)
+ show_stats(cur, prev)
+ sys.exit()
+
with current_file as f:
cur_fail, cur_noticed = examine_stream(f)
if not previous_file is None: