Home | History | Annotate | Download | only in apilint
      1 #!/usr/bin/env python
      2 
      3 # Copyright (C) 2014 The Android Open Source Project
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the 'License');
      6 # you may not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 #      http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an 'AS IS' BASIS,
     13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 
     17 """
     18 Enforces common Android public API design patterns.  It ignores lint messages from
     19 a previous API level, if provided.
     20 
     21 Usage: apilint.py current.txt
     22 Usage: apilint.py current.txt previous.txt
     23 
     24 You can also splice in blame details like this:
     25 $ git blame api/current.txt -t -e > /tmp/currentblame.txt
     26 $ apilint.py /tmp/currentblame.txt previous.txt --no-color
     27 """
     28 
     29 import re, sys, collections, traceback, argparse, itertools
     30 
     31 
     32 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
     33 
     34 ALLOW_GOOGLE = False
     35 USE_COLOR = True
     36 
     37 def format(fg=None, bg=None, bright=False, bold=False, dim=False, reset=False):
     38     # manually derived from http://en.wikipedia.org/wiki/ANSI_escape_code#Codes
     39     if not USE_COLOR: return ""
     40     codes = []
     41     if reset: codes.append("0")
     42     else:
     43         if not fg is None: codes.append("3%d" % (fg))
     44         if not bg is None:
     45             if not bright: codes.append("4%d" % (bg))
     46             else: codes.append("10%d" % (bg))
     47         if bold: codes.append("1")
     48         elif dim: codes.append("2")
     49         else: codes.append("22")
     50     return "\033[%sm" % (";".join(codes))
     51 
     52 
     53 class Field():
     54     def __init__(self, clazz, line, raw, blame, sig_format = 1):
     55         self.clazz = clazz
     56         self.line = line
     57         self.raw = raw.strip(" {;")
     58         self.blame = blame
     59 
     60         if sig_format == 2:
     61             V2LineParser(raw).parse_into_field(self)
     62         elif sig_format == 1:
     63             # drop generics for now; may need multiple passes
     64             raw = re.sub("<[^<]+?>", "", raw)
     65             raw = re.sub("<[^<]+?>", "", raw)
     66 
     67             raw = raw.split()
     68             self.split = list(raw)
     69 
     70             for r in ["field", "volatile", "transient", "public", "protected", "static", "final", "deprecated"]:
     71                 while r in raw: raw.remove(r)
     72 
     73             # ignore annotations for now
     74             raw = [ r for r in raw if not r.startswith("@") ]
     75 
     76             self.typ = raw[0]
     77             self.name = raw[1].strip(";")
     78             if len(raw) >= 4 and raw[2] == "=":
     79                 self.value = raw[3].strip(';"')
     80             else:
     81                 self.value = None
     82             self.annotations = []
     83 
     84         self.ident = "-".join((self.typ, self.name, self.value or ""))
     85 
     86     def __hash__(self):
     87         return hash(self.raw)
     88 
     89     def __repr__(self):
     90         return self.raw
     91 
     92 
     93 class Argument(object):
     94 
     95     __slots__ = ["type", "annotations", "name", "default"]
     96 
     97     def __init__(self, type):
     98         self.type = type
     99         self.annotations = []
    100         self.name = None
    101         self.default = None
    102 
    103 
    104 class Method():
    105     def __init__(self, clazz, line, raw, blame, sig_format = 1):
    106         self.clazz = clazz
    107         self.line = line
    108         self.raw = raw.strip(" {;")
    109         self.blame = blame
    110 
    111         if sig_format == 2:
    112             V2LineParser(raw).parse_into_method(self)
    113         elif sig_format == 1:
    114             # drop generics for now; may need multiple passes
    115             raw = re.sub("<[^<]+?>", "", raw)
    116             raw = re.sub("<[^<]+?>", "", raw)
    117 
    118             # handle each clause differently
    119             raw_prefix, raw_args, _, raw_throws = re.match(r"(.*?)\((.*?)\)( throws )?(.*?);$", raw).groups()
    120 
    121             # parse prefixes
    122             raw = re.split("[\s]+", raw_prefix)
    123             for r in ["", ";"]:
    124                 while r in raw: raw.remove(r)
    125             self.split = list(raw)
    126 
    127             for r in ["method", "public", "protected", "static", "final", "deprecated", "abstract", "default", "operator", "synchronized"]:
    128                 while r in raw: raw.remove(r)
    129 
    130             self.typ = raw[0]
    131             self.name = raw[1]
    132 
    133             # parse args
    134             self.detailed_args = []
    135             for arg in re.split(",\s*", raw_args):
    136                 arg = re.split("\s", arg)
    137                 # ignore annotations for now
    138                 arg = [ a for a in arg if not a.startswith("@") ]
    139                 if len(arg[0]) > 0:
    140                     self.detailed_args.append(Argument(arg[0]))
    141 
    142             # parse throws
    143             self.throws = []
    144             for throw in re.split(",\s*", raw_throws):
    145                 self.throws.append(throw)
    146 
    147             self.annotations = []
    148         else:
    149             raise ValueError("Unknown signature format: " + sig_format)
    150 
    151         self.args = map(lambda a: a.type, self.detailed_args)
    152         self.ident = "-".join((self.typ, self.name, "-".join(self.args)))
    153 
    154     def sig_matches(self, typ, name, args):
    155         return typ == self.typ and name == self.name and args == self.args
    156 
    157     def __hash__(self):
    158         return hash(self.raw)
    159 
    160     def __repr__(self):
    161         return self.raw
    162 
    163 
    164 class Class():
    165     def __init__(self, pkg, line, raw, blame, sig_format = 1):
    166         self.pkg = pkg
    167         self.line = line
    168         self.raw = raw.strip(" {;")
    169         self.blame = blame
    170         self.ctors = []
    171         self.fields = []
    172         self.methods = []
    173         self.annotations = []
    174 
    175         if sig_format == 2:
    176             V2LineParser(raw).parse_into_class(self)
    177         elif sig_format == 1:
    178             # drop generics for now; may need multiple passes
    179             raw = re.sub("<[^<]+?>", "", raw)
    180             raw = re.sub("<[^<]+?>", "", raw)
    181 
    182             raw = raw.split()
    183             self.split = list(raw)
    184             if "class" in raw:
    185                 self.fullname = raw[raw.index("class")+1]
    186             elif "interface" in raw:
    187                 self.fullname = raw[raw.index("interface")+1]
    188             elif "@interface" in raw:
    189                 self.fullname = raw[raw.index("@interface")+1]
    190             else:
    191                 raise ValueError("Funky class type %s" % (self.raw))
    192 
    193             if "extends" in raw:
    194                 self.extends = raw[raw.index("extends")+1]
    195             else:
    196                 self.extends = None
    197 
    198             if "implements" in raw:
    199                 self.implements = raw[raw.index("implements")+1]
    200                 self.implements_all = [self.implements]
    201             else:
    202                 self.implements = None
    203                 self.implements_all = []
    204         else:
    205             raise ValueError("Unknown signature format: " + sig_format)
    206 
    207         self.fullname = self.pkg.name + "." + self.fullname
    208         self.fullname_path = self.fullname.split(".")
    209 
    210         if self.extends is not None:
    211             self.extends_path = self.extends.split(".")
    212         else:
    213             self.extends_path = []
    214 
    215         self.name = self.fullname[self.fullname.rindex(".")+1:]
    216 
    217     def merge_from(self, other):
    218         self.ctors.extend(other.ctors)
    219         self.fields.extend(other.fields)
    220         self.methods.extend(other.methods)
    221 
    222     def __hash__(self):
    223         return hash((self.raw, tuple(self.ctors), tuple(self.fields), tuple(self.methods)))
    224 
    225     def __repr__(self):
    226         return self.raw
    227 
    228 
    229 class Package():
    230     NAME = re.compile("package(?: .*)? ([A-Za-z0-9.]+)")
    231 
    232     def __init__(self, line, raw, blame):
    233         self.line = line
    234         self.raw = raw.strip(" {;")
    235         self.blame = blame
    236 
    237         self.name = Package.NAME.match(raw).group(1)
    238         self.name_path = self.name.split(".")
    239 
    240     def __repr__(self):
    241         return self.raw
    242 
    243 class V2Tokenizer(object):
    244     __slots__ = ["raw"]
    245 
    246     SIGNATURE_PREFIX = "// Signature format: "
    247     DELIMITER = re.compile(r'\s+|[()@<>;,={}/"!?]|\[\]|\.\.\.')
    248     STRING_SPECIAL = re.compile(r'["\\]')
    249 
    250     def __init__(self, raw):
    251         self.raw = raw
    252 
    253     def tokenize(self):
    254         tokens = []
    255         current = 0
    256         raw = self.raw
    257         length = len(raw)
    258 
    259         while current < length:
    260             while current < length:
    261                 start = current
    262                 match = V2Tokenizer.DELIMITER.search(raw, start)
    263                 if match is not None:
    264                     match_start = match.start()
    265                     if match_start == current:
    266                         end = match.end()
    267                     else:
    268                         end = match_start
    269                 else:
    270                     end = length
    271 
    272                 token = raw[start:end]
    273                 current = end
    274 
    275                 if token == "" or token[0] == " ":
    276                     continue
    277                 else:
    278                     break
    279 
    280             if token == "@":
    281                 if raw[start:start+11] == "@interface ":
    282                     current = start + 11
    283                     tokens.append("@interface")
    284                     continue
    285             elif token == '/':
    286                 if raw[start:start+2] == "//":
    287                     current = length
    288                     continue
    289             elif token == '"':
    290                 current, string_token = self.tokenize_string(raw, length, current)
    291                 tokens.append(token + string_token)
    292                 continue
    293 
    294             tokens.append(token)
    295 
    296         return tokens
    297 
    298     def tokenize_string(self, raw, length, current):
    299         start = current
    300         end = length
    301         while start < end:
    302             match = V2Tokenizer.STRING_SPECIAL.search(raw, start)
    303             if match:
    304                 if match.group() == '"':
    305                     end = match.end()
    306                     break
    307                 elif match.group() == '\\':
    308                     # ignore whatever is after the slash
    309                     start += 2
    310                 else:
    311                     raise ValueError("Unexpected match: `%s`" % (match.group()))
    312             else:
    313                 raise ValueError("Unexpected EOF tokenizing string: `%s`" % (raw[current - 1:],))
    314 
    315         token = raw[current:end]
    316         return end, token
    317 
    318 class V2LineParser(object):
    319     __slots__ = ["tokenized", "current", "len"]
    320 
    321     FIELD_KINDS = ("field", "property", "enum_constant")
    322     MODIFIERS = set("public protected internal private abstract default static final transient volatile synchronized native operator sealed strictfp infix inline suspend vararg".split())
    323     JAVA_LANG_TYPES = set("AbstractMethodError AbstractStringBuilder Appendable ArithmeticException ArrayIndexOutOfBoundsException ArrayStoreException AssertionError AutoCloseable Boolean BootstrapMethodError Byte Character CharSequence Class ClassCastException ClassCircularityError ClassFormatError ClassLoader ClassNotFoundException Cloneable CloneNotSupportedException Comparable Compiler Deprecated Double Enum EnumConstantNotPresentException Error Exception ExceptionInInitializerError Float FunctionalInterface IllegalAccessError IllegalAccessException IllegalArgumentException IllegalMonitorStateException IllegalStateException IllegalThreadStateException IncompatibleClassChangeError IndexOutOfBoundsException InheritableThreadLocal InstantiationError InstantiationException Integer InternalError InterruptedException Iterable LinkageError Long Math NegativeArraySizeException NoClassDefFoundError NoSuchFieldError NoSuchFieldException NoSuchMethodError NoSuchMethodException NullPointerException Number NumberFormatException Object OutOfMemoryError Override Package package-info.java Process ProcessBuilder ProcessEnvironment ProcessImpl Readable ReflectiveOperationException Runnable Runtime RuntimeException RuntimePermission SafeVarargs SecurityException SecurityManager Short StackOverflowError StackTraceElement StrictMath String StringBuffer StringBuilder StringIndexOutOfBoundsException SuppressWarnings System Thread ThreadDeath ThreadGroup ThreadLocal Throwable TypeNotPresentException UNIXProcess UnknownError UnsatisfiedLinkError UnsupportedClassVersionError UnsupportedOperationException VerifyError VirtualMachineError Void".split())
    324 
    325     def __init__(self, raw):
    326         self.tokenized = V2Tokenizer(raw).tokenize()
    327         self.current = 0
    328         self.len = len(self.tokenized)
    329 
    330     def parse_into_method(self, method):
    331         method.split = []
    332         kind = self.parse_one_of("ctor", "method")
    333         method.split.append(kind)
    334         method.annotations = self.parse_annotations()
    335         method.split.extend(self.parse_modifiers())
    336         self.parse_matching_paren("<", ">")
    337         if "@Deprecated" in method.annotations:
    338             method.split.append("deprecated")
    339         if kind == "ctor":
    340             method.typ = "ctor"
    341         else:
    342             method.typ = self.parse_type()
    343             method.split.append(method.typ)
    344         method.name = self.parse_name()
    345         method.split.append(method.name)
    346         self.parse_token("(")
    347         method.detailed_args = self.parse_args()
    348         self.parse_token(")")
    349         method.throws = self.parse_throws()
    350         if "@interface" in method.clazz.split:
    351             self.parse_annotation_default()
    352         self.parse_token(";")
    353         self.parse_eof()
    354 
    355     def parse_into_class(self, clazz):
    356         clazz.split = []
    357         clazz.annotations = self.parse_annotations()
    358         if "@Deprecated" in clazz.annotations:
    359             clazz.split.append("deprecated")
    360         clazz.split.extend(self.parse_modifiers())
    361         kind = self.parse_one_of("class", "interface", "@interface", "enum")
    362         if kind == "enum":
    363             # enums are implicitly final
    364             clazz.split.append("final")
    365         clazz.split.append(kind)
    366         clazz.fullname = self.parse_name()
    367         self.parse_matching_paren("<", ">")
    368         extends = self.parse_extends()
    369         clazz.extends = extends[0] if extends else None
    370         clazz.implements_all = self.parse_implements()
    371         # The checks assume that interfaces are always found in implements, which isn't true for
    372         # subinterfaces.
    373         if not clazz.implements_all and "interface" in clazz.split:
    374             clazz.implements_all = [clazz.extends]
    375         clazz.implements = clazz.implements_all[0] if clazz.implements_all else None
    376         self.parse_token("{")
    377         self.parse_eof()
    378 
    379     def parse_into_field(self, field):
    380         kind = self.parse_one_of(*V2LineParser.FIELD_KINDS)
    381         field.split = [kind]
    382         field.annotations = self.parse_annotations()
    383         if "@Deprecated" in field.annotations:
    384             field.split.append("deprecated")
    385         field.split.extend(self.parse_modifiers())
    386         field.typ = self.parse_type()
    387         field.split.append(field.typ)
    388         field.name = self.parse_name()
    389         field.split.append(field.name)
    390         if self.parse_if("="):
    391             field.value = self.parse_value_stripped()
    392         else:
    393             field.value = None
    394 
    395         self.parse_token(";")
    396         self.parse_eof()
    397 
    398     def lookahead(self):
    399         return self.tokenized[self.current]
    400 
    401     def parse_one_of(self, *options):
    402         found = self.lookahead()
    403         if found not in options:
    404             raise ValueError("Parsing failed, expected one of `%s` but found `%s` in %s" % (options, found, repr(self.tokenized)))
    405         return self.parse_token()
    406 
    407     def parse_token(self, tok = None):
    408         found = self.lookahead()
    409         if tok is not None and found != tok:
    410             raise ValueError("Parsing failed, expected `%s` but found `%s` in %s" % (tok, found, repr(self.tokenized)))
    411         self.current += 1
    412         return found
    413 
    414     def eof(self):
    415         return self.current == self.len
    416 
    417     def parse_eof(self):
    418         if not self.eof():
    419             raise ValueError("Parsing failed, expected EOF, but %s has not been parsed in %s" % (self.tokenized[self.current:], self.tokenized))
    420 
    421     def parse_if(self, tok):
    422         if not self.eof() and self.lookahead() == tok:
    423             self.parse_token()
    424             return True
    425         return False
    426 
    427     def parse_annotations(self):
    428         ret = []
    429         while self.lookahead() == "@":
    430             ret.append(self.parse_annotation())
    431         return ret
    432 
    433     def parse_annotation(self):
    434         ret = self.parse_token("@") + self.parse_token()
    435         self.parse_matching_paren("(", ")")
    436         return ret
    437 
    438     def parse_matching_paren(self, open, close):
    439         start = self.current
    440         if not self.parse_if(open):
    441             return
    442         length = len(self.tokenized)
    443         count = 1
    444         while count > 0:
    445             if self.current == length:
    446                 raise ValueError("Unexpected EOF looking for closing paren: `%s`" % (self.tokenized[start:],))
    447             t = self.parse_token()
    448             if t == open:
    449                 count += 1
    450             elif t == close:
    451                 count -= 1
    452         return self.tokenized[start:self.current]
    453 
    454     def parse_modifiers(self):
    455         ret = []
    456         while self.lookahead() in V2LineParser.MODIFIERS:
    457             ret.append(self.parse_token())
    458         return ret
    459 
    460     def parse_kotlin_nullability(self):
    461         t = self.lookahead()
    462         if t == "?" or t == "!":
    463             return self.parse_token()
    464         return None
    465 
    466     def parse_type(self):
    467         self.parse_annotations()
    468         type = self.parse_token()
    469         if type[-1] == '.':
    470             self.parse_annotations()
    471             type += self.parse_token()
    472         if type in V2LineParser.JAVA_LANG_TYPES:
    473             type = "java.lang." + type
    474         self.parse_matching_paren("<", ">")
    475         while True:
    476             t = self.lookahead()
    477             if t == "@":
    478                 self.parse_annotation()
    479             elif t == "[]":
    480                 type += self.parse_token()
    481             elif self.parse_kotlin_nullability() is not None:
    482                 pass  # discard nullability for now
    483             else:
    484                 break
    485         return type
    486 
    487     def parse_arg_type(self):
    488         type = self.parse_type()
    489         if self.parse_if("..."):
    490             type += "..."
    491         self.parse_kotlin_nullability() # discard nullability for now
    492         return type
    493 
    494     def parse_name(self):
    495         return self.parse_token()
    496 
    497     def parse_args(self):
    498         args = []
    499         if self.lookahead() == ")":
    500             return args
    501 
    502         while True:
    503             args.append(self.parse_arg())
    504             if self.lookahead() == ")":
    505                 return args
    506             self.parse_token(",")
    507 
    508     def parse_arg(self):
    509         self.parse_if("vararg")  # kotlin vararg
    510         annotations = self.parse_annotations()
    511         arg = Argument(self.parse_arg_type())
    512         arg.annotations = annotations
    513         l = self.lookahead()
    514         if l != "," and l != ")":
    515             if self.lookahead() != '=':
    516                 arg.name = self.parse_token()  # kotlin argument name
    517             if self.parse_if('='): # kotlin default value
    518                 arg.default = self.parse_expression()
    519         return arg
    520 
    521     def parse_expression(self):
    522         while not self.lookahead() in [')', ',', ';']:
    523             (self.parse_matching_paren('(', ')') or
    524             self.parse_matching_paren('{', '}') or
    525             self.parse_token())
    526 
    527     def parse_throws(self):
    528         ret = []
    529         if self.parse_if("throws"):
    530             ret.append(self.parse_type())
    531             while self.parse_if(","):
    532                 ret.append(self.parse_type())
    533         return ret
    534 
    535     def parse_extends(self):
    536         if self.parse_if("extends"):
    537             return self.parse_space_delimited_type_list()
    538         return []
    539 
    540     def parse_implements(self):
    541         if self.parse_if("implements"):
    542             return self.parse_space_delimited_type_list()
    543         return []
    544 
    545     def parse_space_delimited_type_list(self, terminals = ["implements", "{"]):
    546         types = []
    547         while True:
    548             types.append(self.parse_type())
    549             if self.lookahead() in terminals:
    550                 return types
    551 
    552     def parse_annotation_default(self):
    553         if self.parse_if("default"):
    554             self.parse_expression()
    555 
    556     def parse_value(self):
    557         if self.lookahead() == "{":
    558             return " ".join(self.parse_matching_paren("{", "}"))
    559         elif self.lookahead() == "(":
    560             return " ".join(self.parse_matching_paren("(", ")"))
    561         else:
    562             return self.parse_token()
    563 
    564     def parse_value_stripped(self):
    565         value = self.parse_value()
    566         if value[0] == '"':
    567             return value[1:-1]
    568         return value
    569 
    570 
    571 def _parse_stream(f, clazz_cb=None, base_f=None, out_classes_with_base=None,
    572                   in_classes_with_base=[]):
    573     api = {}
    574     in_classes_with_base = _retry_iterator(in_classes_with_base)
    575 
    576     if base_f:
    577         base_classes = _retry_iterator(_parse_stream_to_generator(base_f))
    578     else:
    579         base_classes = []
    580 
    581     def handle_class(clazz):
    582         if clazz_cb:
    583             clazz_cb(clazz)
    584         else: # In callback mode, don't keep track of the full API
    585             api[clazz.fullname] = clazz
    586 
    587     def handle_missed_classes_with_base(clazz):
    588         for c in _yield_until_matching_class(in_classes_with_base, clazz):
    589             base_class = _skip_to_matching_class(base_classes, c)
    590             if base_class:
    591                 handle_class(base_class)
    592 
    593     for clazz in _parse_stream_to_generator(f):
    594         # Before looking at clazz, let's see if there's some classes that were not present, but
    595         # may have an entry in the base stream.
    596         handle_missed_classes_with_base(clazz)
    597 
    598         base_class = _skip_to_matching_class(base_classes, clazz)
    599         if base_class:
    600             clazz.merge_from(base_class)
    601             if out_classes_with_base is not None:
    602                 out_classes_with_base.append(clazz)
    603         handle_class(clazz)
    604 
    605     handle_missed_classes_with_base(None)
    606 
    607     return api
    608 
    609 def _parse_stream_to_generator(f):
    610     line = 0
    611     pkg = None
    612     clazz = None
    613     blame = None
    614     sig_format = 1
    615 
    616     re_blame = re.compile(r"^(\^?[a-z0-9]{7,}) \(<([^>]+)>.+?\) (.+?)$")
    617 
    618     field_prefixes = map(lambda kind: "    %s" % (kind,), V2LineParser.FIELD_KINDS)
    619     def startsWithFieldPrefix(raw):
    620         for prefix in field_prefixes:
    621             if raw.startswith(prefix):
    622                 return True
    623         return False
    624 
    625     for raw in f:
    626         line += 1
    627         raw = raw.rstrip()
    628         match = re_blame.match(raw)
    629         if match is not None:
    630             blame = match.groups()[0:2]
    631             if blame[0].startswith("^"):  # Outside of blame range
    632               blame = None
    633             raw = match.groups()[2]
    634         else:
    635             blame = None
    636 
    637         if line == 1 and V2Tokenizer.SIGNATURE_PREFIX in raw:
    638             sig_format_string = raw[len(V2Tokenizer.SIGNATURE_PREFIX):]
    639             if sig_format_string in ["2.0", "3.0"]:
    640                 sig_format = 2
    641             else:
    642                 raise ValueError("Unknown format: %s" % (sig_format_string,))
    643         elif raw.startswith("package"):
    644             pkg = Package(line, raw, blame)
    645         elif raw.startswith("  ") and raw.endswith("{"):
    646             clazz = Class(pkg, line, raw, blame, sig_format=sig_format)
    647         elif raw.startswith("    ctor"):
    648             clazz.ctors.append(Method(clazz, line, raw, blame, sig_format=sig_format))
    649         elif raw.startswith("    method"):
    650             clazz.methods.append(Method(clazz, line, raw, blame, sig_format=sig_format))
    651         elif startsWithFieldPrefix(raw):
    652             clazz.fields.append(Field(clazz, line, raw, blame, sig_format=sig_format))
    653         elif raw.startswith("  }") and clazz:
    654             yield clazz
    655 
    656 def _retry_iterator(it):
    657     """Wraps an iterator, such that calling send(True) on it will redeliver the same element"""
    658     for e in it:
    659         while True:
    660             retry = yield e
    661             if not retry:
    662                 break
    663             # send() was called, asking us to redeliver clazz on next(). Still need to yield
    664             # a dummy value to the send() first though.
    665             if (yield "Returning clazz on next()"):
    666                 raise TypeError("send() must be followed by next(), not send()")
    667 
    668 def _skip_to_matching_class(classes, needle):
    669     """Takes a classes iterator and consumes entries until it returns the class we're looking for
    670 
    671     This relies on classes being sorted by package and class name."""
    672 
    673     for clazz in classes:
    674         if clazz.pkg.name < needle.pkg.name:
    675             # We haven't reached the right package yet
    676             continue
    677         if clazz.pkg.name == needle.pkg.name and clazz.fullname < needle.fullname:
    678             # We're in the right package, but not the right class yet
    679             continue
    680         if clazz.fullname == needle.fullname:
    681             return clazz
    682         # We ran past the right class. Send it back into the generator, then report failure.
    683         classes.send(clazz)
    684         return None
    685 
    686 def _yield_until_matching_class(classes, needle):
    687     """Takes a class iterator and yields entries it until it reaches the class we're looking for.
    688 
    689     This relies on classes being sorted by package and class name."""
    690 
    691     for clazz in classes:
    692         if needle is None:
    693             yield clazz
    694         elif clazz.pkg.name < needle.pkg.name:
    695             # We haven't reached the right package yet
    696             yield clazz
    697         elif clazz.pkg.name == needle.pkg.name and clazz.fullname < needle.fullname:
    698             # We're in the right package, but not the right class yet
    699             yield clazz
    700         elif clazz.fullname == needle.fullname:
    701             # Class found, abort.
    702             return
    703         else:
    704             # We ran past the right class. Send it back into the iterator, then abort.
    705             classes.send(clazz)
    706             return
    707 
    708 class Failure():
    709     def __init__(self, sig, clazz, detail, error, rule, msg):
    710         self.clazz = clazz
    711         self.sig = sig
    712         self.error = error
    713         self.rule = rule
    714         self.msg = msg
    715 
    716         if error:
    717             self.head = "Error %s" % (rule) if rule else "Error"
    718             dump = "%s%s:%s %s" % (format(fg=RED, bg=BLACK, bold=True), self.head, format(reset=True), msg)
    719         else:
    720             self.head = "Warning %s" % (rule) if rule else "Warning"
    721             dump = "%s%s:%s %s" % (format(fg=YELLOW, bg=BLACK, bold=True), self.head, format(reset=True), msg)
    722 
    723         self.line = clazz.line
    724         blame = clazz.blame
    725         if detail is not None:
    726             dump += "\n    in " + repr(detail)
    727             self.line = detail.line
    728             blame = detail.blame
    729         dump += "\n    in " + repr(clazz)
    730         dump += "\n    in " + repr(clazz.pkg)
    731         dump += "\n    at line " + repr(self.line)
    732         if blame is not None:
    733             dump += "\n    last modified by %s in %s" % (blame[1], blame[0])
    734 
    735         self.dump = dump
    736 
    737     def __repr__(self):
    738         return self.dump
    739 
    740 
    741 failures = {}
    742 
    743 def _fail(clazz, detail, error, rule, msg):
    744     """Records an API failure to be processed later."""
    745     global failures
    746 
    747     sig = "%s-%s-%s" % (clazz.fullname, detail.ident if detail else None, msg)
    748     sig = sig.replace(" deprecated ", " ")
    749 
    750     failures[sig] = Failure(sig, clazz, detail, error, rule, msg)
    751 
    752 
    753 def warn(clazz, detail, rule, msg):
    754     _fail(clazz, detail, False, rule, msg)
    755 
    756 def error(clazz, detail, rule, msg):
    757     _fail(clazz, detail, True, rule, msg)
    758 
    759 
    760 noticed = {}
    761 
    762 def notice(clazz):
    763     global noticed
    764 
    765     noticed[clazz.fullname] = hash(clazz)
    766 
    767 
    768 verifiers = {}
    769 
    770 def verifier(f):
    771     verifiers[f.__name__] = f
    772     return f
    773 
    774 
    775 @verifier
    776 def verify_constants(clazz):
    777     """All static final constants must be FOO_NAME style."""
    778     if re.match("android\.R\.[a-z]+", clazz.fullname): return
    779     if clazz.fullname.startswith("android.os.Build"): return
    780     if clazz.fullname == "android.system.OsConstants": return
    781 
    782     req = ["java.lang.String","byte","short","int","long","float","double","boolean","char"]
    783     for f in clazz.fields:
    784         if "static" in f.split and "final" in f.split:
    785             if re.match("[A-Z0-9_]+", f.name) is None:
    786                 error(clazz, f, "C2", "Constant field names must be FOO_NAME")
    787             if f.typ != "java.lang.String":
    788                 if f.name.startswith("MIN_") or f.name.startswith("MAX_"):
    789                     warn(clazz, f, "C8", "If min/max could change in future, make them dynamic methods")
    790             if f.typ in req and f.value is None:
    791                 error(clazz, f, None, "All constants must be defined at compile time")
    792 
    793 @verifier
    794 def verify_enums(clazz):
    795     """Enums are bad, mmkay?"""
    796     if clazz.extends == "java.lang.Enum" or "enum" in clazz.split:
    797         error(clazz, None, "F5", "Enums are not allowed")
    798 
    799 @verifier
    800 def verify_class_names(clazz):
    801     """Try catching malformed class names like myMtp or MTPUser."""
    802     if clazz.fullname.startswith("android.opengl"): return
    803     if clazz.fullname.startswith("android.renderscript"): return
    804     if re.match("android\.R\.[a-z]+", clazz.fullname): return
    805 
    806     if re.search("[A-Z]{2,}", clazz.name) is not None:
    807         warn(clazz, None, "S1", "Class names with acronyms should be Mtp not MTP")
    808     if re.match("[^A-Z]", clazz.name):
    809         error(clazz, None, "S1", "Class must start with uppercase char")
    810     if clazz.name.endswith("Impl"):
    811         error(clazz, None, None, "Don't expose your implementation details")
    812 
    813 
    814 @verifier
    815 def verify_method_names(clazz):
    816     """Try catching malformed method names, like Foo() or getMTU()."""
    817     if clazz.fullname.startswith("android.opengl"): return
    818     if clazz.fullname.startswith("android.renderscript"): return
    819     if clazz.fullname == "android.system.OsConstants": return
    820 
    821     for m in clazz.methods:
    822         if re.search("[A-Z]{2,}", m.name) is not None:
    823             warn(clazz, m, "S1", "Method names with acronyms should be getMtu() instead of getMTU()")
    824         if re.match("[^a-z]", m.name):
    825             error(clazz, m, "S1", "Method name must start with lowercase char")
    826 
    827 
    828 @verifier
    829 def verify_callbacks(clazz):
    830     """Verify Callback classes.
    831     All methods must follow onFoo() naming style."""
    832     if clazz.fullname == "android.speech.tts.SynthesisCallback": return
    833 
    834     if clazz.name.endswith("Callbacks"):
    835         error(clazz, None, "L1", "Callback class names should be singular")
    836     if clazz.name.endswith("Observer"):
    837         warn(clazz, None, "L1", "Class should be named FooCallback")
    838 
    839     if clazz.name.endswith("Callback"):
    840         for m in clazz.methods:
    841             if not re.match("on[A-Z][a-z]*", m.name):
    842                 error(clazz, m, "L1", "Callback method names must be onFoo() style")
    843 
    844 
    845 @verifier
    846 def verify_listeners(clazz):
    847     """Verify Listener classes.
    848     All Listener classes must be interface.
    849     All methods must follow onFoo() naming style.
    850     If only a single method, it must match class name:
    851         interface OnFooListener { void onFoo() }"""
    852 
    853     if clazz.name.endswith("Listener"):
    854         if "abstract" in clazz.split and "class" in clazz.split:
    855             error(clazz, None, "L1", "Listeners should be an interface, or otherwise renamed Callback")
    856 
    857         for m in clazz.methods:
    858             if not re.match("on[A-Z][a-z]*", m.name):
    859                 error(clazz, m, "L1", "Listener method names must be onFoo() style")
    860 
    861         if len(clazz.methods) == 1 and clazz.name.startswith("On"):
    862             m = clazz.methods[0]
    863             if (m.name + "Listener").lower() != clazz.name.lower():
    864                 error(clazz, m, "L1", "Single listener method name must match class name")
    865 
    866 
    867 @verifier
    868 def verify_actions(clazz):
    869     """Verify intent actions.
    870     All action names must be named ACTION_FOO.
    871     All action values must be scoped by package and match name:
    872         package android.foo {
    873             String ACTION_BAR = "android.foo.action.BAR";
    874         }"""
    875     for f in clazz.fields:
    876         if f.value is None: continue
    877         if f.name.startswith("EXTRA_"): continue
    878         if f.name == "SERVICE_INTERFACE" or f.name == "PROVIDER_INTERFACE": continue
    879         if "INTERACTION" in f.name: continue
    880 
    881         if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
    882             if "_ACTION" in f.name or "ACTION_" in f.name or ".action." in f.value.lower():
    883                 if not f.name.startswith("ACTION_"):
    884                     error(clazz, f, "C3", "Intent action constant name must be ACTION_FOO")
    885                 else:
    886                     if clazz.fullname == "android.content.Intent":
    887                         prefix = "android.intent.action"
    888                     elif clazz.fullname == "android.provider.Settings":
    889                         prefix = "android.settings"
    890                     elif clazz.fullname == "android.app.admin.DevicePolicyManager" or clazz.fullname == "android.app.admin.DeviceAdminReceiver":
    891                         prefix = "android.app.action"
    892                     else:
    893                         prefix = clazz.pkg.name + ".action"
    894                     expected = prefix + "." + f.name[7:]
    895                     if f.value != expected:
    896                         error(clazz, f, "C4", "Inconsistent action value; expected '%s'" % (expected))
    897 
    898 
    899 @verifier
    900 def verify_extras(clazz):
    901     """Verify intent extras.
    902     All extra names must be named EXTRA_FOO.
    903     All extra values must be scoped by package and match name:
    904         package android.foo {
    905             String EXTRA_BAR = "android.foo.extra.BAR";
    906         }"""
    907     if clazz.fullname == "android.app.Notification": return
    908     if clazz.fullname == "android.appwidget.AppWidgetManager": return
    909 
    910     for f in clazz.fields:
    911         if f.value is None: continue
    912         if f.name.startswith("ACTION_"): continue
    913 
    914         if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
    915             if "_EXTRA" in f.name or "EXTRA_" in f.name or ".extra" in f.value.lower():
    916                 if not f.name.startswith("EXTRA_"):
    917                     error(clazz, f, "C3", "Intent extra must be EXTRA_FOO")
    918                 else:
    919                     if clazz.pkg.name == "android.content" and clazz.name == "Intent":
    920                         prefix = "android.intent.extra"
    921                     elif clazz.pkg.name == "android.app.admin":
    922                         prefix = "android.app.extra"
    923                     else:
    924                         prefix = clazz.pkg.name + ".extra"
    925                     expected = prefix + "." + f.name[6:]
    926                     if f.value != expected:
    927                         error(clazz, f, "C4", "Inconsistent extra value; expected '%s'" % (expected))
    928 
    929 
    930 @verifier
    931 def verify_equals(clazz):
    932     """Verify that equals() and hashCode() must be overridden together."""
    933     eq = False
    934     hc = False
    935     for m in clazz.methods:
    936         if "static" in m.split: continue
    937         if m.sig_matches("boolean", "equals", ["java.lang.Object"]): eq = True
    938         if m.sig_matches("int", "hashCode", []): hc = True
    939     if eq != hc:
    940         error(clazz, None, "M8", "Must override both equals and hashCode; missing one")
    941 
    942 
    943 @verifier
    944 def verify_parcelable(clazz):
    945     """Verify that Parcelable objects aren't hiding required bits."""
    946     if clazz.implements == "android.os.Parcelable":
    947         creator = [ i for i in clazz.fields if i.name == "CREATOR" ]
    948         write = [ i for i in clazz.methods if i.name == "writeToParcel" ]
    949         describe = [ i for i in clazz.methods if i.name == "describeContents" ]
    950 
    951         if len(creator) == 0 or len(write) == 0 or len(describe) == 0:
    952             error(clazz, None, "FW3", "Parcelable requires CREATOR, writeToParcel, and describeContents; missing one")
    953 
    954         if "final" not in clazz.split:
    955             error(clazz, None, "FW8", "Parcelable classes must be final")
    956 
    957         for c in clazz.ctors:
    958             if c.args == ["android.os.Parcel"]:
    959                 error(clazz, c, "FW3", "Parcelable inflation is exposed through CREATOR, not raw constructors")
    960 
    961 
    962 @verifier
    963 def verify_protected(clazz):
    964     """Verify that no protected methods or fields are allowed."""
    965     for m in clazz.methods:
    966         if m.name == "finalize": continue
    967         if "protected" in m.split:
    968             error(clazz, m, "M7", "Protected methods not allowed; must be public")
    969     for f in clazz.fields:
    970         if "protected" in f.split:
    971             error(clazz, f, "M7", "Protected fields not allowed; must be public")
    972 
    973 
    974 @verifier
    975 def verify_fields(clazz):
    976     """Verify that all exposed fields are final.
    977     Exposed fields must follow myName style.
    978     Catch internal mFoo objects being exposed."""
    979 
    980     IGNORE_BARE_FIELDS = [
    981         "android.app.ActivityManager.RecentTaskInfo",
    982         "android.app.Notification",
    983         "android.content.pm.ActivityInfo",
    984         "android.content.pm.ApplicationInfo",
    985         "android.content.pm.ComponentInfo",
    986         "android.content.pm.ResolveInfo",
    987         "android.content.pm.FeatureGroupInfo",
    988         "android.content.pm.InstrumentationInfo",
    989         "android.content.pm.PackageInfo",
    990         "android.content.pm.PackageItemInfo",
    991         "android.content.res.Configuration",
    992         "android.graphics.BitmapFactory.Options",
    993         "android.os.Message",
    994         "android.system.StructPollfd",
    995     ]
    996 
    997     for f in clazz.fields:
    998         if not "final" in f.split:
    999             if clazz.fullname in IGNORE_BARE_FIELDS:
   1000                 pass
   1001             elif clazz.fullname.endswith("LayoutParams"):
   1002                 pass
   1003             elif clazz.fullname.startswith("android.util.Mutable"):
   1004                 pass
   1005             else:
   1006                 error(clazz, f, "F2", "Bare fields must be marked final, or add accessors if mutable")
   1007 
   1008         if "static" not in f.split and "property" not in f.split:
   1009             if not re.match("[a-z]([a-zA-Z]+)?", f.name):
   1010                 error(clazz, f, "S1", "Non-static fields must be named using myField style")
   1011 
   1012         if re.match("[ms][A-Z]", f.name):
   1013             error(clazz, f, "F1", "Internal objects must not be exposed")
   1014 
   1015         if re.match("[A-Z_]+", f.name):
   1016             if "static" not in f.split or "final" not in f.split:
   1017                 error(clazz, f, "C2", "Constants must be marked static final")
   1018 
   1019 
   1020 @verifier
   1021 def verify_register(clazz):
   1022     """Verify parity of registration methods.
   1023     Callback objects use register/unregister methods.
   1024     Listener objects use add/remove methods."""
   1025     methods = [ m.name for m in clazz.methods ]
   1026     for m in clazz.methods:
   1027         if "Callback" in m.raw:
   1028             if m.name.startswith("register"):
   1029                 other = "unregister" + m.name[8:]
   1030                 if other not in methods:
   1031                     error(clazz, m, "L2", "Missing unregister method")
   1032             if m.name.startswith("unregister"):
   1033                 other = "register" + m.name[10:]
   1034                 if other not in methods:
   1035                     error(clazz, m, "L2", "Missing register method")
   1036 
   1037             if m.name.startswith("add") or m.name.startswith("remove"):
   1038                 error(clazz, m, "L3", "Callback methods should be named register/unregister")
   1039 
   1040         if "Listener" in m.raw:
   1041             if m.name.startswith("add"):
   1042                 other = "remove" + m.name[3:]
   1043                 if other not in methods:
   1044                     error(clazz, m, "L2", "Missing remove method")
   1045             if m.name.startswith("remove") and not m.name.startswith("removeAll"):
   1046                 other = "add" + m.name[6:]
   1047                 if other not in methods:
   1048                     error(clazz, m, "L2", "Missing add method")
   1049 
   1050             if m.name.startswith("register") or m.name.startswith("unregister"):
   1051                 error(clazz, m, "L3", "Listener methods should be named add/remove")
   1052 
   1053 
   1054 @verifier
   1055 def verify_sync(clazz):
   1056     """Verify synchronized methods aren't exposed."""
   1057     for m in clazz.methods:
   1058         if "synchronized" in m.split:
   1059             error(clazz, m, "M5", "Internal locks must not be exposed")
   1060 
   1061 
   1062 @verifier
   1063 def verify_intent_builder(clazz):
   1064     """Verify that Intent builders are createFooIntent() style."""
   1065     if clazz.name == "Intent": return
   1066 
   1067     for m in clazz.methods:
   1068         if m.typ == "android.content.Intent":
   1069             if m.name.startswith("create") and m.name.endswith("Intent"):
   1070                 pass
   1071             else:
   1072                 warn(clazz, m, "FW1", "Methods creating an Intent should be named createFooIntent()")
   1073 
   1074 
   1075 @verifier
   1076 def verify_helper_classes(clazz):
   1077     """Verify that helper classes are named consistently with what they extend.
   1078     All developer extendable methods should be named onFoo()."""
   1079     test_methods = False
   1080     if clazz.extends == "android.app.Service":
   1081         test_methods = True
   1082         if not clazz.name.endswith("Service"):
   1083             error(clazz, None, "CL4", "Inconsistent class name; should be FooService")
   1084 
   1085         found = False
   1086         for f in clazz.fields:
   1087             if f.name == "SERVICE_INTERFACE":
   1088                 found = True
   1089                 if f.value != clazz.fullname:
   1090                     error(clazz, f, "C4", "Inconsistent interface constant; expected '%s'" % (clazz.fullname))
   1091 
   1092     if clazz.extends == "android.content.ContentProvider":
   1093         test_methods = True
   1094         if not clazz.name.endswith("Provider"):
   1095             error(clazz, None, "CL4", "Inconsistent class name; should be FooProvider")
   1096 
   1097         found = False
   1098         for f in clazz.fields:
   1099             if f.name == "PROVIDER_INTERFACE":
   1100                 found = True
   1101                 if f.value != clazz.fullname:
   1102                     error(clazz, f, "C4", "Inconsistent interface constant; expected '%s'" % (clazz.fullname))
   1103 
   1104     if clazz.extends == "android.content.BroadcastReceiver":
   1105         test_methods = True
   1106         if not clazz.name.endswith("Receiver"):
   1107             error(clazz, None, "CL4", "Inconsistent class name; should be FooReceiver")
   1108 
   1109     if clazz.extends == "android.app.Activity":
   1110         test_methods = True
   1111         if not clazz.name.endswith("Activity"):
   1112             error(clazz, None, "CL4", "Inconsistent class name; should be FooActivity")
   1113 
   1114     if test_methods:
   1115         for m in clazz.methods:
   1116             if "final" in m.split: continue
   1117             if not re.match("on[A-Z]", m.name):
   1118                 if "abstract" in m.split:
   1119                     warn(clazz, m, None, "Methods implemented by developers should be named onFoo()")
   1120                 else:
   1121                     warn(clazz, m, None, "If implemented by developer, should be named onFoo(); otherwise consider marking final")
   1122 
   1123 
   1124 @verifier
   1125 def verify_builder(clazz):
   1126     """Verify builder classes.
   1127     Methods should return the builder to enable chaining."""
   1128     if clazz.extends: return
   1129     if not clazz.name.endswith("Builder"): return
   1130 
   1131     if clazz.name != "Builder":
   1132         warn(clazz, None, None, "Builder should be defined as inner class")
   1133 
   1134     has_build = False
   1135     for m in clazz.methods:
   1136         if m.name == "build":
   1137             has_build = True
   1138             continue
   1139 
   1140         if m.name.startswith("get"): continue
   1141         if m.name.startswith("clear"): continue
   1142 
   1143         if m.name.startswith("with"):
   1144             warn(clazz, m, None, "Builder methods names should use setFoo() style")
   1145 
   1146         if m.name.startswith("set"):
   1147             if not m.typ.endswith(clazz.fullname):
   1148                 warn(clazz, m, "M4", "Methods must return the builder object")
   1149 
   1150     if not has_build:
   1151         warn(clazz, None, None, "Missing build() method")
   1152 
   1153     if "final" not in clazz.split:
   1154         error(clazz, None, None, "Builder should be final")
   1155 
   1156 
   1157 @verifier
   1158 def verify_aidl(clazz):
   1159     """Catch people exposing raw AIDL."""
   1160     if clazz.extends == "android.os.Binder" or clazz.implements == "android.os.IInterface":
   1161         error(clazz, None, None, "Raw AIDL interfaces must not be exposed")
   1162 
   1163 
   1164 @verifier
   1165 def verify_internal(clazz):
   1166     """Catch people exposing internal classes."""
   1167     if clazz.pkg.name.startswith("com.android"):
   1168         error(clazz, None, None, "Internal classes must not be exposed")
   1169 
   1170 def layering_build_ranking(ranking_list):
   1171     r = {}
   1172     for rank, ps in enumerate(ranking_list):
   1173         if not isinstance(ps, list):
   1174             ps = [ps]
   1175         for p in ps:
   1176             rs = r
   1177             for n in p.split('.'):
   1178                 if n not in rs:
   1179                     rs[n] = {}
   1180                 rs = rs[n]
   1181             rs['-rank'] = rank
   1182     return r
   1183 
   1184 LAYERING_PACKAGE_RANKING = layering_build_ranking([
   1185     ["android.service","android.accessibilityservice","android.inputmethodservice","android.printservice","android.appwidget","android.webkit","android.preference","android.gesture","android.print"],
   1186     "android.app",
   1187     "android.widget",
   1188     "android.view",
   1189     "android.animation",
   1190     "android.provider",
   1191     ["android.content","android.graphics.drawable"],
   1192     "android.database",
   1193     "android.text",
   1194     "android.graphics",
   1195     "android.os",
   1196     "android.util"
   1197 ])
   1198 
   1199 @verifier
   1200 def verify_layering(clazz):
   1201     """Catch package layering violations.
   1202     For example, something in android.os depending on android.app."""
   1203 
   1204     def rank(p):
   1205         r = None
   1206         l = LAYERING_PACKAGE_RANKING
   1207         for n in p.split('.'):
   1208             if n in l:
   1209                 l = l[n]
   1210                 if '-rank' in l:
   1211                     r = l['-rank']
   1212             else:
   1213                 break
   1214         return r
   1215 
   1216     cr = rank(clazz.pkg.name)
   1217     if cr is None: return
   1218 
   1219     for f in clazz.fields:
   1220         ir = rank(f.typ)
   1221         if ir is not None and ir < cr:
   1222             warn(clazz, f, "FW6", "Field type violates package layering")
   1223 
   1224     for m in itertools.chain(clazz.methods, clazz.ctors):
   1225         ir = rank(m.typ)
   1226         if ir is not None and ir < cr:
   1227             warn(clazz, m, "FW6", "Method return type violates package layering")
   1228         for arg in m.args:
   1229             ir = rank(arg)
   1230             if ir is not None and ir < cr:
   1231                 warn(clazz, m, "FW6", "Method argument type violates package layering")
   1232 
   1233 
   1234 @verifier
   1235 def verify_boolean(clazz):
   1236     """Verifies that boolean accessors are named correctly.
   1237     For example, hasFoo() and setHasFoo()."""
   1238 
   1239     def is_get(m): return len(m.args) == 0 and m.typ == "boolean"
   1240     def is_set(m): return len(m.args) == 1 and m.args[0] == "boolean"
   1241 
   1242     gets = [ m for m in clazz.methods if is_get(m) ]
   1243     sets = [ m for m in clazz.methods if is_set(m) ]
   1244 
   1245     def error_if_exists(methods, trigger, expected, actual):
   1246         for m in methods:
   1247             if m.name == actual:
   1248                 error(clazz, m, "M6", "Symmetric method for %s must be named %s" % (trigger, expected))
   1249 
   1250     for m in clazz.methods:
   1251         if is_get(m):
   1252             if re.match("is[A-Z]", m.name):
   1253                 target = m.name[2:]
   1254                 expected = "setIs" + target
   1255                 error_if_exists(sets, m.name, expected, "setHas" + target)
   1256             elif re.match("has[A-Z]", m.name):
   1257                 target = m.name[3:]
   1258                 expected = "setHas" + target
   1259                 error_if_exists(sets, m.name, expected, "setIs" + target)
   1260                 error_if_exists(sets, m.name, expected, "set" + target)
   1261             elif re.match("get[A-Z]", m.name):
   1262                 target = m.name[3:]
   1263                 expected = "set" + target
   1264                 error_if_exists(sets, m.name, expected, "setIs" + target)
   1265                 error_if_exists(sets, m.name, expected, "setHas" + target)
   1266 
   1267         if is_set(m):
   1268             if re.match("set[A-Z]", m.name):
   1269                 target = m.name[3:]
   1270                 expected = "get" + target
   1271                 error_if_exists(sets, m.name, expected, "is" + target)
   1272                 error_if_exists(sets, m.name, expected, "has" + target)
   1273 
   1274 
   1275 @verifier
   1276 def verify_collections(clazz):
   1277     """Verifies that collection types are interfaces."""
   1278     if clazz.fullname == "android.os.Bundle": return
   1279     if clazz.fullname == "android.os.Parcel": return
   1280 
   1281     bad = ["java.util.Vector", "java.util.LinkedList", "java.util.ArrayList", "java.util.Stack",
   1282            "java.util.HashMap", "java.util.HashSet", "android.util.ArraySet", "android.util.ArrayMap"]
   1283     for m in clazz.methods:
   1284         if m.typ in bad:
   1285             error(clazz, m, "CL2", "Return type is concrete collection; must be higher-level interface")
   1286         for arg in m.args:
   1287             if arg in bad:
   1288                 error(clazz, m, "CL2", "Argument is concrete collection; must be higher-level interface")
   1289 
   1290 
   1291 @verifier
   1292 def verify_uris(clazz):
   1293     bad = ["java.net.URL", "java.net.URI", "android.net.URL"]
   1294 
   1295     for f in clazz.fields:
   1296         if f.typ in bad:
   1297             error(clazz, f, None, "Field must be android.net.Uri instead of " + f.typ)
   1298 
   1299     for m in clazz.methods + clazz.ctors:
   1300         if m.typ in bad:
   1301             error(clazz, m, None, "Must return android.net.Uri instead of " + m.typ)
   1302         for arg in m.args:
   1303             if arg in bad:
   1304                 error(clazz, m, None, "Argument must take android.net.Uri instead of " + arg)
   1305 
   1306 
   1307 @verifier
   1308 def verify_flags(clazz):
   1309     """Verifies that flags are non-overlapping."""
   1310     known = collections.defaultdict(int)
   1311     for f in clazz.fields:
   1312         if "FLAG_" in f.name:
   1313             try:
   1314                 val = int(f.value)
   1315             except:
   1316                 continue
   1317 
   1318             scope = f.name[0:f.name.index("FLAG_")]
   1319             if val & known[scope]:
   1320                 warn(clazz, f, "C1", "Found overlapping flag constant value")
   1321             known[scope] |= val
   1322 
   1323 
   1324 @verifier
   1325 def verify_exception(clazz):
   1326     """Verifies that methods don't throw generic exceptions."""
   1327     for m in clazz.methods:
   1328         for t in m.throws:
   1329             if t in ["java.lang.Exception", "java.lang.Throwable", "java.lang.Error"]:
   1330                 error(clazz, m, "S1", "Methods must not throw generic exceptions")
   1331 
   1332             if t in ["android.os.RemoteException"]:
   1333                 if clazz.fullname == "android.content.ContentProviderClient": continue
   1334                 if clazz.fullname == "android.os.Binder": continue
   1335                 if clazz.fullname == "android.os.IBinder": continue
   1336 
   1337                 error(clazz, m, "FW9", "Methods calling into system server should rethrow RemoteException as RuntimeException")
   1338 
   1339             if len(m.args) == 0 and t in ["java.lang.IllegalArgumentException", "java.lang.NullPointerException"]:
   1340                 warn(clazz, m, "S1", "Methods taking no arguments should throw IllegalStateException")
   1341 
   1342 
   1343 GOOGLE_IGNORECASE = re.compile("google", re.IGNORECASE)
   1344 
   1345 # Not marked as @verifier, because it is only conditionally applied.
   1346 def verify_google(clazz):
   1347     """Verifies that APIs never reference Google."""
   1348 
   1349     if GOOGLE_IGNORECASE.search(clazz.raw) is not None:
   1350         error(clazz, None, None, "Must never reference Google")
   1351 
   1352     for test in clazz.ctors, clazz.fields, clazz.methods:
   1353         for t in test:
   1354             if GOOGLE_IGNORECASE.search(t.raw) is not None:
   1355                 error(clazz, t, None, "Must never reference Google")
   1356 
   1357 
   1358 @verifier
   1359 def verify_bitset(clazz):
   1360     """Verifies that we avoid using heavy BitSet."""
   1361 
   1362     for f in clazz.fields:
   1363         if f.typ == "java.util.BitSet":
   1364             error(clazz, f, None, "Field type must not be heavy BitSet")
   1365 
   1366     for m in clazz.methods:
   1367         if m.typ == "java.util.BitSet":
   1368             error(clazz, m, None, "Return type must not be heavy BitSet")
   1369         for arg in m.args:
   1370             if arg == "java.util.BitSet":
   1371                 error(clazz, m, None, "Argument type must not be heavy BitSet")
   1372 
   1373 
   1374 @verifier
   1375 def verify_manager(clazz):
   1376     """Verifies that FooManager is only obtained from Context."""
   1377 
   1378     if not clazz.name.endswith("Manager"): return
   1379 
   1380     for c in clazz.ctors:
   1381         error(clazz, c, None, "Managers must always be obtained from Context; no direct constructors")
   1382 
   1383     for m in clazz.methods:
   1384         if m.typ == clazz.fullname:
   1385             error(clazz, m, None, "Managers must always be obtained from Context")
   1386 
   1387 
   1388 @verifier
   1389 def verify_boxed(clazz):
   1390     """Verifies that methods avoid boxed primitives."""
   1391 
   1392     boxed = ["java.lang.Number","java.lang.Byte","java.lang.Double","java.lang.Float","java.lang.Integer","java.lang.Long","java.lang.Short"]
   1393 
   1394     for c in clazz.ctors:
   1395         for arg in c.args:
   1396             if arg in boxed:
   1397                 error(clazz, c, "M11", "Must avoid boxed primitives")
   1398 
   1399     for f in clazz.fields:
   1400         if f.typ in boxed:
   1401             error(clazz, f, "M11", "Must avoid boxed primitives")
   1402 
   1403     for m in clazz.methods:
   1404         if m.typ in boxed:
   1405             error(clazz, m, "M11", "Must avoid boxed primitives")
   1406         for arg in m.args:
   1407             if arg in boxed:
   1408                 error(clazz, m, "M11", "Must avoid boxed primitives")
   1409 
   1410 
   1411 @verifier
   1412 def verify_static_utils(clazz):
   1413     """Verifies that helper classes can't be constructed."""
   1414     if clazz.fullname.startswith("android.opengl"): return
   1415     if clazz.fullname.startswith("android.R"): return
   1416 
   1417     # Only care about classes with default constructors
   1418     if len(clazz.ctors) == 1 and len(clazz.ctors[0].args) == 0:
   1419         test = []
   1420         test.extend(clazz.fields)
   1421         test.extend(clazz.methods)
   1422 
   1423         if len(test) == 0: return
   1424         for t in test:
   1425             if "static" not in t.split:
   1426                 return
   1427 
   1428         error(clazz, None, None, "Fully-static utility classes must not have constructor")
   1429 
   1430 
   1431 # @verifier  # Disabled for now
   1432 def verify_overload_args(clazz):
   1433     """Verifies that method overloads add new arguments at the end."""
   1434     if clazz.fullname.startswith("android.opengl"): return
   1435 
   1436     overloads = collections.defaultdict(list)
   1437     for m in clazz.methods:
   1438         if "deprecated" in m.split: continue
   1439         overloads[m.name].append(m)
   1440 
   1441     for name, methods in overloads.items():
   1442         if len(methods) <= 1: continue
   1443 
   1444         # Look for arguments common across all overloads
   1445         def cluster(args):
   1446             count = collections.defaultdict(int)
   1447             res = set()
   1448             for i in range(len(args)):
   1449                 a = args[i]
   1450                 res.add("%s#%d" % (a, count[a]))
   1451                 count[a] += 1
   1452             return res
   1453 
   1454         common_args = cluster(methods[0].args)
   1455         for m in methods:
   1456             common_args = common_args & cluster(m.args)
   1457 
   1458         if len(common_args) == 0: continue
   1459 
   1460         # Require that all common arguments are present at start of signature
   1461         locked_sig = None
   1462         for m in methods:
   1463             sig = m.args[0:len(common_args)]
   1464             if not common_args.issubset(cluster(sig)):
   1465                 warn(clazz, m, "M2", "Expected common arguments [%s] at beginning of overloaded method" % (", ".join(common_args)))
   1466             elif not locked_sig:
   1467                 locked_sig = sig
   1468             elif locked_sig != sig:
   1469                 error(clazz, m, "M2", "Expected consistent argument ordering between overloads: %s..." % (", ".join(locked_sig)))
   1470 
   1471 
   1472 @verifier
   1473 def verify_callback_handlers(clazz):
   1474     """Verifies that methods adding listener/callback have overload
   1475     for specifying delivery thread."""
   1476 
   1477     # Ignore UI packages which assume main thread
   1478     skip = [
   1479         "animation",
   1480         "view",
   1481         "graphics",
   1482         "transition",
   1483         "widget",
   1484         "webkit",
   1485     ]
   1486     for s in skip:
   1487         if s in clazz.pkg.name_path: return
   1488         if s in clazz.extends_path: return
   1489 
   1490     # Ignore UI classes which assume main thread
   1491     if "app" in clazz.pkg.name_path or "app" in clazz.extends_path:
   1492         for s in ["ActionBar","Dialog","Application","Activity","Fragment","Loader"]:
   1493             if s in clazz.fullname: return
   1494     if "content" in clazz.pkg.name_path or "content" in clazz.extends_path:
   1495         for s in ["Loader"]:
   1496             if s in clazz.fullname: return
   1497 
   1498     found = {}
   1499     by_name = collections.defaultdict(list)
   1500     examine = clazz.ctors + clazz.methods
   1501     for m in examine:
   1502         if m.name.startswith("unregister"): continue
   1503         if m.name.startswith("remove"): continue
   1504         if re.match("on[A-Z]+", m.name): continue
   1505 
   1506         by_name[m.name].append(m)
   1507 
   1508         for a in m.args:
   1509             if a.endswith("Listener") or a.endswith("Callback") or a.endswith("Callbacks"):
   1510                 found[m.name] = m
   1511 
   1512     for f in found.values():
   1513         takes_handler = False
   1514         takes_exec = False
   1515         for m in by_name[f.name]:
   1516             if "android.os.Handler" in m.args:
   1517                 takes_handler = True
   1518             if "java.util.concurrent.Executor" in m.args:
   1519                 takes_exec = True
   1520         if not takes_exec:
   1521             warn(clazz, f, "L1", "Registration methods should have overload that accepts delivery Executor")
   1522 
   1523 
   1524 @verifier
   1525 def verify_context_first(clazz):
   1526     """Verifies that methods accepting a Context keep it the first argument."""
   1527     examine = clazz.ctors + clazz.methods
   1528     for m in examine:
   1529         if len(m.args) > 1 and m.args[0] != "android.content.Context":
   1530             if "android.content.Context" in m.args[1:]:
   1531                 error(clazz, m, "M3", "Context is distinct, so it must be the first argument")
   1532         if len(m.args) > 1 and m.args[0] != "android.content.ContentResolver":
   1533             if "android.content.ContentResolver" in m.args[1:]:
   1534                 error(clazz, m, "M3", "ContentResolver is distinct, so it must be the first argument")
   1535 
   1536 
   1537 @verifier
   1538 def verify_listener_last(clazz):
   1539     """Verifies that methods accepting a Listener or Callback keep them as last arguments."""
   1540     examine = clazz.ctors + clazz.methods
   1541     for m in examine:
   1542         if "Listener" in m.name or "Callback" in m.name: continue
   1543         found = False
   1544         for a in m.args:
   1545             if a.endswith("Callback") or a.endswith("Callbacks") or a.endswith("Listener"):
   1546                 found = True
   1547             elif found:
   1548                 warn(clazz, m, "M3", "Listeners should always be at end of argument list")
   1549 
   1550 
   1551 @verifier
   1552 def verify_resource_names(clazz):
   1553     """Verifies that resource names have consistent case."""
   1554     if not re.match("android\.R\.[a-z]+", clazz.fullname): return
   1555 
   1556     # Resources defined by files are foo_bar_baz
   1557     if clazz.name in ["anim","animator","color","dimen","drawable","interpolator","layout","transition","menu","mipmap","string","plurals","raw","xml"]:
   1558         for f in clazz.fields:
   1559             if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue
   1560             if f.name.startswith("config_"):
   1561                 error(clazz, f, None, "Expected config name to be config_fooBarBaz style")
   1562 
   1563             if re.match("[a-z1-9_]+$", f.name): continue
   1564             error(clazz, f, None, "Expected resource name in this class to be foo_bar_baz style")
   1565 
   1566     # Resources defined inside files are fooBarBaz
   1567     if clazz.name in ["array","attr","id","bool","fraction","integer"]:
   1568         for f in clazz.fields:
   1569             if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue
   1570             if re.match("layout_[a-z][a-zA-Z1-9]*$", f.name): continue
   1571             if re.match("state_[a-z_]*$", f.name): continue
   1572 
   1573             if re.match("[a-z][a-zA-Z1-9]*$", f.name): continue
   1574             error(clazz, f, "C7", "Expected resource name in this class to be fooBarBaz style")
   1575 
   1576     # Styles are FooBar_Baz
   1577     if clazz.name in ["style"]:
   1578         for f in clazz.fields:
   1579             if re.match("[A-Z][A-Za-z1-9]+(_[A-Z][A-Za-z1-9]+?)*$", f.name): continue
   1580             error(clazz, f, "C7", "Expected resource name in this class to be FooBar_Baz style")
   1581 
   1582 
   1583 @verifier
   1584 def verify_files(clazz):
   1585     """Verifies that methods accepting File also accept streams."""
   1586 
   1587     has_file = set()
   1588     has_stream = set()
   1589 
   1590     test = []
   1591     test.extend(clazz.ctors)
   1592     test.extend(clazz.methods)
   1593 
   1594     for m in test:
   1595         if "java.io.File" in m.args:
   1596             has_file.add(m)
   1597         if "java.io.FileDescriptor" in m.args or "android.os.ParcelFileDescriptor" in m.args or "java.io.InputStream" in m.args or "java.io.OutputStream" in m.args:
   1598             has_stream.add(m.name)
   1599 
   1600     for m in has_file:
   1601         if m.name not in has_stream:
   1602             warn(clazz, m, "M10", "Methods accepting File should also accept FileDescriptor or streams")
   1603 
   1604 
   1605 @verifier
   1606 def verify_manager_list(clazz):
   1607     """Verifies that managers return List<? extends Parcelable> instead of arrays."""
   1608 
   1609     if not clazz.name.endswith("Manager"): return
   1610 
   1611     for m in clazz.methods:
   1612         if m.typ.startswith("android.") and m.typ.endswith("[]"):
   1613             warn(clazz, m, None, "Methods should return List<? extends Parcelable> instead of Parcelable[] to support ParceledListSlice under the hood")
   1614 
   1615 
   1616 @verifier
   1617 def verify_abstract_inner(clazz):
   1618     """Verifies that abstract inner classes are static."""
   1619 
   1620     if re.match(".+?\.[A-Z][^\.]+\.[A-Z]", clazz.fullname):
   1621         if "abstract" in clazz.split and "static" not in clazz.split:
   1622             warn(clazz, None, None, "Abstract inner classes should be static to improve testability")
   1623 
   1624 
   1625 @verifier
   1626 def verify_runtime_exceptions(clazz):
   1627     """Verifies that runtime exceptions aren't listed in throws."""
   1628 
   1629     banned = [
   1630         "java.lang.NullPointerException",
   1631         "java.lang.ClassCastException",
   1632         "java.lang.IndexOutOfBoundsException",
   1633         "java.lang.reflect.UndeclaredThrowableException",
   1634         "java.lang.reflect.MalformedParametersException",
   1635         "java.lang.reflect.MalformedParameterizedTypeException",
   1636         "java.lang.invoke.WrongMethodTypeException",
   1637         "java.lang.EnumConstantNotPresentException",
   1638         "java.lang.IllegalMonitorStateException",
   1639         "java.lang.SecurityException",
   1640         "java.lang.UnsupportedOperationException",
   1641         "java.lang.annotation.AnnotationTypeMismatchException",
   1642         "java.lang.annotation.IncompleteAnnotationException",
   1643         "java.lang.TypeNotPresentException",
   1644         "java.lang.IllegalStateException",
   1645         "java.lang.ArithmeticException",
   1646         "java.lang.IllegalArgumentException",
   1647         "java.lang.ArrayStoreException",
   1648         "java.lang.NegativeArraySizeException",
   1649         "java.util.MissingResourceException",
   1650         "java.util.EmptyStackException",
   1651         "java.util.concurrent.CompletionException",
   1652         "java.util.concurrent.RejectedExecutionException",
   1653         "java.util.IllformedLocaleException",
   1654         "java.util.ConcurrentModificationException",
   1655         "java.util.NoSuchElementException",
   1656         "java.io.UncheckedIOException",
   1657         "java.time.DateTimeException",
   1658         "java.security.ProviderException",
   1659         "java.nio.BufferUnderflowException",
   1660         "java.nio.BufferOverflowException",
   1661     ]
   1662 
   1663     examine = clazz.ctors + clazz.methods
   1664     for m in examine:
   1665         for t in m.throws:
   1666             if t in banned:
   1667                 error(clazz, m, None, "Methods must not mention RuntimeException subclasses in throws clauses")
   1668 
   1669 
   1670 @verifier
   1671 def verify_error(clazz):
   1672     """Verifies that we always use Exception instead of Error."""
   1673     if not clazz.extends: return
   1674     if clazz.extends.endswith("Error"):
   1675         error(clazz, None, None, "Trouble must be reported through an Exception, not Error")
   1676     if clazz.extends.endswith("Exception") and not clazz.name.endswith("Exception"):
   1677         error(clazz, None, None, "Exceptions must be named FooException")
   1678 
   1679 
   1680 @verifier
   1681 def verify_units(clazz):
   1682     """Verifies that we use consistent naming for units."""
   1683 
   1684     # If we find K, recommend replacing with V
   1685     bad = {
   1686         "Ns": "Nanos",
   1687         "Ms": "Millis or Micros",
   1688         "Sec": "Seconds", "Secs": "Seconds",
   1689         "Hr": "Hours", "Hrs": "Hours",
   1690         "Mo": "Months", "Mos": "Months",
   1691         "Yr": "Years", "Yrs": "Years",
   1692         "Byte": "Bytes", "Space": "Bytes",
   1693     }
   1694 
   1695     for m in clazz.methods:
   1696         if m.typ not in ["short","int","long"]: continue
   1697         for k, v in bad.iteritems():
   1698             if m.name.endswith(k):
   1699                 error(clazz, m, None, "Expected method name units to be " + v)
   1700         if m.name.endswith("Nanos") or m.name.endswith("Micros"):
   1701             warn(clazz, m, None, "Returned time values are strongly encouraged to be in milliseconds unless you need the extra precision")
   1702         if m.name.endswith("Seconds"):
   1703             error(clazz, m, None, "Returned time values must be in milliseconds")
   1704 
   1705     for m in clazz.methods:
   1706         typ = m.typ
   1707         if typ == "void":
   1708             if len(m.args) != 1: continue
   1709             typ = m.args[0]
   1710 
   1711         if m.name.endswith("Fraction") and typ != "float":
   1712             error(clazz, m, None, "Fractions must use floats")
   1713         if m.name.endswith("Percentage") and typ != "int":
   1714             error(clazz, m, None, "Percentage must use ints")
   1715 
   1716 
   1717 @verifier
   1718 def verify_closable(clazz):
   1719     """Verifies that classes are AutoClosable."""
   1720     if "java.lang.AutoCloseable" in clazz.implements_all: return
   1721     if "java.io.Closeable" in clazz.implements_all: return
   1722 
   1723     for m in clazz.methods:
   1724         if len(m.args) > 0: continue
   1725         if m.name in ["close","release","destroy","finish","finalize","disconnect","shutdown","stop","free","quit"]:
   1726             warn(clazz, m, None, "Classes that release resources should implement AutoClosable and CloseGuard")
   1727             return
   1728 
   1729 
   1730 @verifier
   1731 def verify_member_name_not_kotlin_keyword(clazz):
   1732     """Prevent method names which are keywords in Kotlin."""
   1733 
   1734     # https://kotlinlang.org/docs/reference/keyword-reference.html#hard-keywords
   1735     # This list does not include Java keywords as those are already impossible to use.
   1736     keywords = [
   1737         'as',
   1738         'fun',
   1739         'in',
   1740         'is',
   1741         'object',
   1742         'typealias',
   1743         'val',
   1744         'var',
   1745         'when',
   1746     ]
   1747 
   1748     for m in clazz.methods:
   1749         if m.name in keywords:
   1750             error(clazz, m, None, "Method name must not be a Kotlin keyword")
   1751     for f in clazz.fields:
   1752         if f.name in keywords:
   1753             error(clazz, f, None, "Field name must not be a Kotlin keyword")
   1754 
   1755 
   1756 @verifier
   1757 def verify_method_name_not_kotlin_operator(clazz):
   1758     """Warn about method names which become operators in Kotlin."""
   1759 
   1760     binary = set()
   1761 
   1762     def unique_binary_op(m, op):
   1763         if op in binary:
   1764             error(clazz, m, None, "Only one of '{0}' and '{0}Assign' methods should be present for Kotlin".format(op))
   1765         binary.add(op)
   1766 
   1767     for m in clazz.methods:
   1768         if 'static' in m.split or 'operator' in m.split:
   1769             continue
   1770 
   1771         # https://kotlinlang.org/docs/reference/operator-overloading.html#unary-prefix-operators
   1772         if m.name in ['unaryPlus', 'unaryMinus', 'not'] and len(m.args) == 0:
   1773             warn(clazz, m, None, "Method can be invoked as a unary operator from Kotlin")
   1774 
   1775         # https://kotlinlang.org/docs/reference/operator-overloading.html#increments-and-decrements
   1776         if m.name in ['inc', 'dec'] and len(m.args) == 0 and m.typ != 'void':
   1777             # This only applies if the return type is the same or a subtype of the enclosing class, but we have no
   1778             # practical way of checking that relationship here.
   1779             warn(clazz, m, None, "Method can be invoked as a pre/postfix inc/decrement operator from Kotlin")
   1780 
   1781         # https://kotlinlang.org/docs/reference/operator-overloading.html#arithmetic
   1782         if m.name in ['plus', 'minus', 'times', 'div', 'rem', 'mod', 'rangeTo'] and len(m.args) == 1:
   1783             warn(clazz, m, None, "Method can be invoked as a binary operator from Kotlin")
   1784             unique_binary_op(m, m.name)
   1785 
   1786         # https://kotlinlang.org/docs/reference/operator-overloading.html#in
   1787         if m.name == 'contains' and len(m.args) == 1 and m.typ == 'boolean':
   1788             warn(clazz, m, None, "Method can be invoked as a 'in' operator from Kotlin")
   1789 
   1790         # https://kotlinlang.org/docs/reference/operator-overloading.html#indexed
   1791         if (m.name == 'get' and len(m.args) > 0) or (m.name == 'set' and len(m.args) > 1):
   1792             warn(clazz, m, None, "Method can be invoked with an indexing operator from Kotlin")
   1793 
   1794         # https://kotlinlang.org/docs/reference/operator-overloading.html#invoke
   1795         if m.name == 'invoke':
   1796             warn(clazz, m, None, "Method can be invoked with function call syntax from Kotlin")
   1797 
   1798         # https://kotlinlang.org/docs/reference/operator-overloading.html#assignments
   1799         if m.name in ['plusAssign', 'minusAssign', 'timesAssign', 'divAssign', 'remAssign', 'modAssign'] \
   1800                 and len(m.args) == 1 \
   1801                 and m.typ == 'void':
   1802             warn(clazz, m, None, "Method can be invoked as a compound assignment operator from Kotlin")
   1803             unique_binary_op(m, m.name[:-6])  # Remove 'Assign' suffix
   1804 
   1805 
   1806 @verifier
   1807 def verify_collections_over_arrays(clazz):
   1808     """Warn that [] should be Collections."""
   1809 
   1810     if "@interface" in clazz.split:
   1811         return
   1812 
   1813     safe = ["java.lang.String[]","byte[]","short[]","int[]","long[]","float[]","double[]","boolean[]","char[]"]
   1814     for m in clazz.methods:
   1815         if m.typ.endswith("[]") and m.typ not in safe:
   1816             warn(clazz, m, None, "Method should return Collection<> (or subclass) instead of raw array")
   1817         for arg in m.args:
   1818             if arg.endswith("[]") and arg not in safe:
   1819                 warn(clazz, m, None, "Method argument should be Collection<> (or subclass) instead of raw array")
   1820 
   1821 
   1822 @verifier
   1823 def verify_user_handle(clazz):
   1824     """Methods taking UserHandle should be ForUser or AsUser."""
   1825     if clazz.name.endswith("Listener") or clazz.name.endswith("Callback") or clazz.name.endswith("Callbacks"): return
   1826     if clazz.fullname == "android.app.admin.DeviceAdminReceiver": return
   1827     if clazz.fullname == "android.content.pm.LauncherApps": return
   1828     if clazz.fullname == "android.os.UserHandle": return
   1829     if clazz.fullname == "android.os.UserManager": return
   1830 
   1831     for m in clazz.methods:
   1832         if re.match("on[A-Z]+", m.name): continue
   1833 
   1834         has_arg = "android.os.UserHandle" in m.args
   1835         has_name = m.name.endswith("AsUser") or m.name.endswith("ForUser")
   1836 
   1837         if clazz.fullname.endswith("Manager") and has_arg:
   1838             warn(clazz, m, None, "When a method overload is needed to target a specific "
   1839                  "UserHandle, callers should be directed to use "
   1840                  "Context.createPackageContextAsUser() and re-obtain the relevant "
   1841                  "Manager, and no new API should be added")
   1842         elif has_arg and not has_name:
   1843             warn(clazz, m, None, "Method taking UserHandle should be named 'doFooAsUser' "
   1844                  "or 'queryFooForUser'")
   1845 
   1846 
   1847 @verifier
   1848 def verify_params(clazz):
   1849     """Parameter classes should be 'Params'."""
   1850     if clazz.name.endswith("Params"): return
   1851     if clazz.fullname == "android.app.ActivityOptions": return
   1852     if clazz.fullname == "android.app.BroadcastOptions": return
   1853     if clazz.fullname == "android.os.Bundle": return
   1854     if clazz.fullname == "android.os.BaseBundle": return
   1855     if clazz.fullname == "android.os.PersistableBundle": return
   1856 
   1857     bad = ["Param","Parameter","Parameters","Args","Arg","Argument","Arguments","Options","Bundle"]
   1858     for b in bad:
   1859         if clazz.name.endswith(b):
   1860             error(clazz, None, None, "Classes holding a set of parameters should be called 'FooParams'")
   1861 
   1862 
   1863 @verifier
   1864 def verify_services(clazz):
   1865     """Service name should be FOO_BAR_SERVICE = 'foo_bar'."""
   1866     if clazz.fullname != "android.content.Context": return
   1867 
   1868     for f in clazz.fields:
   1869         if f.typ != "java.lang.String": continue
   1870         found = re.match(r"([A-Z_]+)_SERVICE", f.name)
   1871         if found:
   1872             expected = found.group(1).lower()
   1873             if f.value != expected:
   1874                 error(clazz, f, "C4", "Inconsistent service value; expected '%s'" % (expected))
   1875 
   1876 
   1877 @verifier
   1878 def verify_tense(clazz):
   1879     """Verify tenses of method names."""
   1880     if clazz.fullname.startswith("android.opengl"): return
   1881 
   1882     for m in clazz.methods:
   1883         if m.name.endswith("Enable"):
   1884             warn(clazz, m, None, "Unexpected tense; probably meant 'enabled'")
   1885 
   1886 
   1887 @verifier
   1888 def verify_icu(clazz):
   1889     """Verifies that richer ICU replacements are used."""
   1890     better = {
   1891         "java.util.TimeZone": "android.icu.util.TimeZone",
   1892         "java.util.Calendar": "android.icu.util.Calendar",
   1893         "java.util.Locale": "android.icu.util.ULocale",
   1894         "java.util.ResourceBundle": "android.icu.util.UResourceBundle",
   1895         "java.util.SimpleTimeZone": "android.icu.util.SimpleTimeZone",
   1896         "java.util.StringTokenizer": "android.icu.util.StringTokenizer",
   1897         "java.util.GregorianCalendar": "android.icu.util.GregorianCalendar",
   1898         "java.lang.Character": "android.icu.lang.UCharacter",
   1899         "java.text.BreakIterator": "android.icu.text.BreakIterator",
   1900         "java.text.Collator": "android.icu.text.Collator",
   1901         "java.text.DecimalFormatSymbols": "android.icu.text.DecimalFormatSymbols",
   1902         "java.text.NumberFormat": "android.icu.text.NumberFormat",
   1903         "java.text.DateFormatSymbols": "android.icu.text.DateFormatSymbols",
   1904         "java.text.DateFormat": "android.icu.text.DateFormat",
   1905         "java.text.SimpleDateFormat": "android.icu.text.SimpleDateFormat",
   1906         "java.text.MessageFormat": "android.icu.text.MessageFormat",
   1907         "java.text.DecimalFormat": "android.icu.text.DecimalFormat",
   1908     }
   1909 
   1910     for m in clazz.ctors + clazz.methods:
   1911         types = []
   1912         types.extend(m.typ)
   1913         types.extend(m.args)
   1914         for arg in types:
   1915             if arg in better:
   1916                 warn(clazz, m, None, "Type %s should be replaced with richer ICU type %s" % (arg, better[arg]))
   1917 
   1918 
   1919 @verifier
   1920 def verify_clone(clazz):
   1921     """Verify that clone() isn't implemented; see EJ page 61."""
   1922     for m in clazz.methods:
   1923         if m.name == "clone":
   1924             error(clazz, m, None, "Provide an explicit copy constructor instead of implementing clone()")
   1925 
   1926 
   1927 @verifier
   1928 def verify_pfd(clazz):
   1929     """Verify that android APIs use PFD over FD."""
   1930     if clazz.fullname == "android.os.FileUtils" or clazz.fullname == "android.system.Os":
   1931         return
   1932 
   1933     examine = clazz.ctors + clazz.methods
   1934     for m in examine:
   1935         if m.typ == "java.io.FileDescriptor":
   1936             error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
   1937         if m.typ == "int":
   1938             if "Fd" in m.name or "FD" in m.name or "FileDescriptor" in m.name:
   1939                 error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
   1940         for arg in m.args:
   1941             if arg == "java.io.FileDescriptor":
   1942                 error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
   1943 
   1944     for f in clazz.fields:
   1945         if f.typ == "java.io.FileDescriptor":
   1946             error(clazz, f, "FW11", "Must use ParcelFileDescriptor")
   1947 
   1948 
   1949 @verifier
   1950 def verify_numbers(clazz):
   1951     """Discourage small numbers types like short and byte."""
   1952 
   1953     discouraged = ["short","byte"]
   1954 
   1955     for c in clazz.ctors:
   1956         for arg in c.args:
   1957             if arg in discouraged:
   1958                 warn(clazz, c, "FW12", "Should avoid odd sized primitives; use int instead")
   1959 
   1960     for f in clazz.fields:
   1961         if f.typ in discouraged:
   1962             warn(clazz, f, "FW12", "Should avoid odd sized primitives; use int instead")
   1963 
   1964     for m in clazz.methods:
   1965         if m.typ in discouraged:
   1966             warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead")
   1967         for arg in m.args:
   1968             if arg in discouraged:
   1969                 warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead")
   1970 
   1971 
   1972 PRIMITIVES = {"void", "int", "float", "boolean", "short", "char", "byte", "long", "double"}
   1973 
   1974 @verifier
   1975 def verify_nullability(clazz):
   1976     """Catches missing nullability annotations"""
   1977 
   1978     for f in clazz.fields:
   1979         if f.value is not None and 'static' in f.split and 'final' in f.split:
   1980             continue  # Nullability of constants can be inferred.
   1981         if f.typ not in PRIMITIVES and not has_nullability(f.annotations):
   1982             error(clazz, f, "M12", "Field must be marked either @NonNull or @Nullable")
   1983 
   1984     for c in clazz.ctors:
   1985         verify_nullability_args(clazz, c)
   1986 
   1987     for m in clazz.methods:
   1988         if m.name == "writeToParcel" or m.name == "onReceive":
   1989             continue  # Parcelable.writeToParcel() and BroadcastReceiver.onReceive() are not yet annotated
   1990 
   1991         if m.typ not in PRIMITIVES and not has_nullability(m.annotations):
   1992             error(clazz, m, "M12", "Return value must be marked either @NonNull or @Nullable")
   1993         verify_nullability_args(clazz, m)
   1994 
   1995 def verify_nullability_args(clazz, m):
   1996     for i, arg in enumerate(m.detailed_args):
   1997         if arg.type not in PRIMITIVES and not has_nullability(arg.annotations):
   1998             error(clazz, m, "M12", "Argument %d must be marked either @NonNull or @Nullable" % (i+1,))
   1999 
   2000 def has_nullability(annotations):
   2001     return "@NonNull" in annotations or "@Nullable" in annotations
   2002 
   2003 
   2004 @verifier
   2005 def verify_intdef(clazz):
   2006     """intdefs must be @hide, because the constant names cannot be stored in
   2007        the stubs (only the values are, which is not useful)"""
   2008     if "@interface" not in clazz.split:
   2009         return
   2010     if "@IntDef" in clazz.annotations or "@LongDef" in clazz.annotations:
   2011         error(clazz, None, None, "@IntDef and @LongDef annotations must be @hide")
   2012 
   2013 
   2014 @verifier
   2015 def verify_singleton(clazz):
   2016     """Catch singleton objects with constructors."""
   2017 
   2018     singleton = False
   2019     for m in clazz.methods:
   2020         if m.name.startswith("get") and m.name.endswith("Instance") and " static " in m.raw:
   2021             singleton = True
   2022 
   2023     if singleton:
   2024         for c in clazz.ctors:
   2025             error(clazz, c, None, "Singleton classes should use getInstance() methods")
   2026 
   2027 
   2028 
   2029 def is_interesting(clazz):
   2030     """Test if given class is interesting from an Android PoV."""
   2031 
   2032     if clazz.pkg.name.startswith("java"): return False
   2033     if clazz.pkg.name.startswith("junit"): return False
   2034     if clazz.pkg.name.startswith("org.apache"): return False
   2035     if clazz.pkg.name.startswith("org.xml"): return False
   2036     if clazz.pkg.name.startswith("org.json"): return False
   2037     if clazz.pkg.name.startswith("org.w3c"): return False
   2038     if clazz.pkg.name.startswith("android.icu."): return False
   2039     return True
   2040 
   2041 
   2042 def examine_clazz(clazz):
   2043     """Find all style issues in the given class."""
   2044 
   2045     notice(clazz)
   2046 
   2047     if not is_interesting(clazz): return
   2048 
   2049     for v in verifiers.itervalues():
   2050         v(clazz)
   2051 
   2052     if not ALLOW_GOOGLE: verify_google(clazz)
   2053 
   2054 
   2055 def examine_stream(stream, base_stream=None, in_classes_with_base=[], out_classes_with_base=None):
   2056     """Find all style issues in the given API stream."""
   2057     global failures, noticed
   2058     failures = {}
   2059     noticed = {}
   2060     _parse_stream(stream, examine_clazz, base_f=base_stream,
   2061                   in_classes_with_base=in_classes_with_base,
   2062                   out_classes_with_base=out_classes_with_base)
   2063     return (failures, noticed)
   2064 
   2065 
   2066 def examine_api(api):
   2067     """Find all style issues in the given parsed API."""
   2068     global failures
   2069     failures = {}
   2070     for key in sorted(api.keys()):
   2071         examine_clazz(api[key])
   2072     return failures
   2073 
   2074 
   2075 def verify_compat(cur, prev):
   2076     """Find any incompatible API changes between two levels."""
   2077     global failures
   2078 
   2079     def class_exists(api, test):
   2080         return test.fullname in api
   2081 
   2082     def ctor_exists(api, clazz, test):
   2083         for m in clazz.ctors:
   2084             if m.ident == test.ident: return True
   2085         return False
   2086 
   2087     def all_methods(api, clazz):
   2088         methods = list(clazz.methods)
   2089         if clazz.extends is not None:
   2090             methods.extend(all_methods(api, api[clazz.extends]))
   2091         return methods
   2092 
   2093     def method_exists(api, clazz, test):
   2094         methods = all_methods(api, clazz)
   2095         for m in methods:
   2096             if m.ident == test.ident: return True
   2097         return False
   2098 
   2099     def field_exists(api, clazz, test):
   2100         for f in clazz.fields:
   2101             if f.ident == test.ident: return True
   2102         return False
   2103 
   2104     failures = {}
   2105     for key in sorted(prev.keys()):
   2106         prev_clazz = prev[key]
   2107 
   2108         if not class_exists(cur, prev_clazz):
   2109             error(prev_clazz, None, None, "Class removed or incompatible change")
   2110             continue
   2111 
   2112         cur_clazz = cur[key]
   2113 
   2114         for test in prev_clazz.ctors:
   2115             if not ctor_exists(cur, cur_clazz, test):
   2116                 error(prev_clazz, prev_ctor, None, "Constructor removed or incompatible change")
   2117 
   2118         methods = all_methods(prev, prev_clazz)
   2119         for test in methods:
   2120             if not method_exists(cur, cur_clazz, test):
   2121                 error(prev_clazz, test, None, "Method removed or incompatible change")
   2122 
   2123         for test in prev_clazz.fields:
   2124             if not field_exists(cur, cur_clazz, test):
   2125                 error(prev_clazz, test, None, "Field removed or incompatible change")
   2126 
   2127     return failures
   2128 
   2129 
   2130 def match_filter(filters, fullname):
   2131     for f in filters:
   2132         if fullname == f:
   2133             return True
   2134         if fullname.startswith(f + '.'):
   2135             return True
   2136     return False
   2137 
   2138 
   2139 def show_deprecations_at_birth(cur, prev):
   2140     """Show API deprecations at birth."""
   2141     global failures
   2142 
   2143     # Remove all existing things so we're left with new
   2144     for prev_clazz in prev.values():
   2145         if prev_clazz.fullname not in cur:
   2146             # The class was removed this release; we can safely ignore it.
   2147             continue
   2148 
   2149         cur_clazz = cur[prev_clazz.fullname]
   2150         if not is_interesting(cur_clazz): continue
   2151 
   2152         sigs = { i.ident: i for i in prev_clazz.ctors }
   2153         cur_clazz.ctors = [ i for i in cur_clazz.ctors if i.ident not in sigs ]
   2154         sigs = { i.ident: i for i in prev_clazz.methods }
   2155         cur_clazz.methods = [ i for i in cur_clazz.methods if i.ident not in sigs ]
   2156         sigs = { i.ident: i for i in prev_clazz.fields }
   2157         cur_clazz.fields = [ i for i in cur_clazz.fields if i.ident not in sigs ]
   2158 
   2159         # Forget about class entirely when nothing new
   2160         if len(cur_clazz.ctors) == 0 and len(cur_clazz.methods) == 0 and len(cur_clazz.fields) == 0:
   2161             del cur[prev_clazz.fullname]
   2162 
   2163     for clazz in cur.values():
   2164         if not is_interesting(clazz): continue
   2165 
   2166         if "deprecated" in clazz.split and not clazz.fullname in prev:
   2167             error(clazz, None, None, "Found API deprecation at birth")
   2168 
   2169         for i in clazz.ctors + clazz.methods + clazz.fields:
   2170             if "deprecated" in i.split:
   2171                 error(clazz, i, None, "Found API deprecation at birth")
   2172 
   2173     print "%s Deprecated at birth %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True),
   2174                                             format(reset=True)))
   2175     for f in sorted(failures):
   2176         print failures[f]
   2177         print
   2178 
   2179 
   2180 def show_stats(cur, prev):
   2181     """Show API stats."""
   2182 
   2183     stats = collections.defaultdict(int)
   2184     for cur_clazz in cur.values():
   2185         if not is_interesting(cur_clazz): continue
   2186 
   2187         if cur_clazz.fullname not in prev:
   2188             stats['new_classes'] += 1
   2189             stats['new_ctors'] += len(cur_clazz.ctors)
   2190             stats['new_methods'] += len(cur_clazz.methods)
   2191             stats['new_fields'] += len(cur_clazz.fields)
   2192         else:
   2193             prev_clazz = prev[cur_clazz.fullname]
   2194 
   2195             sigs = { i.ident: i for i in prev_clazz.ctors }
   2196             ctors = len([ i for i in cur_clazz.ctors if i.ident not in sigs ])
   2197             sigs = { i.ident: i for i in prev_clazz.methods }
   2198             methods = len([ i for i in cur_clazz.methods if i.ident not in sigs ])
   2199             sigs = { i.ident: i for i in prev_clazz.fields }
   2200             fields = len([ i for i in cur_clazz.fields if i.ident not in sigs ])
   2201 
   2202             if ctors + methods + fields > 0:
   2203                 stats['extend_classes'] += 1
   2204                 stats['extend_ctors'] += ctors
   2205                 stats['extend_methods'] += methods
   2206                 stats['extend_fields'] += fields
   2207 
   2208     print "#", "".join([ k.ljust(20) for k in sorted(stats.keys()) ])
   2209     print " ", "".join([ str(stats[k]).ljust(20) for k in sorted(stats.keys()) ])
   2210 
   2211 
   2212 def main():
   2213     parser = argparse.ArgumentParser(description="Enforces common Android public API design \
   2214             patterns. It ignores lint messages from a previous API level, if provided.")
   2215     parser.add_argument("--base-current", nargs='?', type=argparse.FileType('r'), default=None,
   2216             help="The base current.txt to use when examining system-current.txt or"
   2217                  " test-current.txt")
   2218     parser.add_argument("--base-previous", nargs='?', type=argparse.FileType('r'), default=None,
   2219             help="The base previous.txt to use when examining system-previous.txt or"
   2220                  " test-previous.txt")
   2221     parser.add_argument("--no-color", action='store_const', const=True,
   2222             help="Disable terminal colors")
   2223     parser.add_argument("--color", action='store_const', const=True,
   2224             help="Use terminal colors")
   2225     parser.add_argument("--allow-google", action='store_const', const=True,
   2226             help="Allow references to Google")
   2227     parser.add_argument("--show-noticed", action='store_const', const=True,
   2228             help="Show API changes noticed")
   2229     parser.add_argument("--show-deprecations-at-birth", action='store_const', const=True,
   2230             help="Show API deprecations at birth")
   2231     parser.add_argument("--show-stats", action='store_const', const=True,
   2232             help="Show API stats")
   2233     parser.add_argument("--title", action='store', default=None,
   2234             help="Title to put in for display purposes")
   2235     parser.add_argument("--filter", action="append",
   2236             help="If provided, only show lint for the given packages or classes.")
   2237     parser.add_argument("current.txt", type=argparse.FileType('r'), help="current.txt")
   2238     parser.add_argument("previous.txt", nargs='?', type=argparse.FileType('r'), default=None,
   2239             help="previous.txt")
   2240     args = vars(parser.parse_args())
   2241 
   2242     if args['no_color']:
   2243         USE_COLOR = False
   2244     elif args['color']:
   2245         USE_COLOR = True
   2246     else:
   2247         USE_COLOR = sys.stdout.isatty()
   2248 
   2249     if args['allow_google']:
   2250         ALLOW_GOOGLE = True
   2251 
   2252     current_file = args['current.txt']
   2253     base_current_file = args['base_current']
   2254     previous_file = args['previous.txt']
   2255     base_previous_file = args['base_previous']
   2256     filters = args['filter']
   2257     if not filters:
   2258         filters = []
   2259     title = args['title']
   2260     if not title:
   2261         title = current_file.name
   2262 
   2263     if args['show_deprecations_at_birth']:
   2264         with current_file as f:
   2265             cur = _parse_stream(f)
   2266         with previous_file as f:
   2267             prev = _parse_stream(f)
   2268         show_deprecations_at_birth(cur, prev)
   2269         sys.exit()
   2270 
   2271     if args['show_stats']:
   2272         with current_file as f:
   2273             cur = _parse_stream(f)
   2274         with previous_file as f:
   2275             prev = _parse_stream(f)
   2276         show_stats(cur, prev)
   2277         sys.exit()
   2278 
   2279     classes_with_base = []
   2280 
   2281     with current_file as f:
   2282         if base_current_file:
   2283             with base_current_file as base_f:
   2284                 cur_fail, cur_noticed = examine_stream(f, base_f,
   2285                                                        out_classes_with_base=classes_with_base)
   2286         else:
   2287             cur_fail, cur_noticed = examine_stream(f, out_classes_with_base=classes_with_base)
   2288 
   2289     if not previous_file is None:
   2290         with previous_file as f:
   2291             if base_previous_file:
   2292                 with base_previous_file as base_f:
   2293                     prev_fail, prev_noticed = examine_stream(f, base_f,
   2294                                                              in_classes_with_base=classes_with_base)
   2295             else:
   2296                 prev_fail, prev_noticed = examine_stream(f, in_classes_with_base=classes_with_base)
   2297 
   2298         # ignore errors from previous API level
   2299         for p in prev_fail:
   2300             if p in cur_fail:
   2301                 del cur_fail[p]
   2302 
   2303         # ignore classes unchanged from previous API level
   2304         for k, v in prev_noticed.iteritems():
   2305             if k in cur_noticed and v == cur_noticed[k]:
   2306                 del cur_noticed[k]
   2307 
   2308         """
   2309         # NOTE: disabled because of memory pressure
   2310         # look for compatibility issues
   2311         compat_fail = verify_compat(cur, prev)
   2312 
   2313         print "%s API compatibility issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
   2314         for f in sorted(compat_fail):
   2315             print compat_fail[f]
   2316             print
   2317         """
   2318 
   2319     # ignore everything but the given filters, if provided
   2320     if filters:
   2321         cur_fail = dict([(key, failure) for key, failure in cur_fail.iteritems()
   2322                 if match_filter(filters, failure.clazz.fullname)])
   2323 
   2324     if args['show_noticed'] and len(cur_noticed) != 0:
   2325         print "%s API changes noticed %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
   2326         for f in sorted(cur_noticed.keys()):
   2327             print f
   2328         print
   2329 
   2330     if len(cur_fail) != 0:
   2331         print "%s API style issues: %s %s" % ((format(fg=WHITE, bg=BLUE, bold=True),
   2332                     title, format(reset=True)))
   2333         for f in filters:
   2334             print "%s   filter: %s %s" % ((format(fg=WHITE, bg=BLUE, bold=True),
   2335                         f, format(reset=True)))
   2336         print
   2337         for f in sorted(cur_fail):
   2338             print cur_fail[f]
   2339             print
   2340         print "%d errors" % len(cur_fail)
   2341         sys.exit(77)
   2342 
   2343 if __name__ == "__main__":
   2344     try:
   2345         main()
   2346     except KeyboardInterrupt:
   2347         sys.exit(1)
   2348