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
     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):
     55         self.clazz = clazz
     56         self.line = line
     57         self.raw = raw.strip(" {;")
     58         self.blame = blame
     59 
     60         raw = raw.split()
     61         self.split = list(raw)
     62 
     63         for r in ["field", "volatile", "transient", "public", "protected", "static", "final", "deprecated"]:
     64             while r in raw: raw.remove(r)
     65 
     66         self.typ = raw[0]
     67         self.name = raw[1].strip(";")
     68         if len(raw) >= 4 and raw[2] == "=":
     69             self.value = raw[3].strip(';"')
     70         else:
     71             self.value = None
     72 
     73         self.ident = self.raw.replace(" deprecated ", " ")
     74 
     75     def __repr__(self):
     76         return self.raw
     77 
     78 
     79 class Method():
     80     def __init__(self, clazz, line, raw, blame):
     81         self.clazz = clazz
     82         self.line = line
     83         self.raw = raw.strip(" {;")
     84         self.blame = blame
     85 
     86         # drop generics for now
     87         raw = re.sub("<.+?>", "", raw)
     88 
     89         raw = re.split("[\s(),;]+", raw)
     90         for r in ["", ";"]:
     91             while r in raw: raw.remove(r)
     92         self.split = list(raw)
     93 
     94         for r in ["method", "public", "protected", "static", "final", "deprecated", "abstract", "default"]:
     95             while r in raw: raw.remove(r)
     96 
     97         self.typ = raw[0]
     98         self.name = raw[1]
     99         self.args = []
    100         for r in raw[2:]:
    101             if r == "throws": break
    102             self.args.append(r)
    103 
    104         # identity for compat purposes
    105         ident = self.raw
    106         ident = ident.replace(" deprecated ", " ")
    107         ident = ident.replace(" synchronized ", " ")
    108         ident = re.sub("<.+?>", "", ident)
    109         if " throws " in ident:
    110             ident = ident[:ident.index(" throws ")]
    111         self.ident = ident
    112 
    113     def __repr__(self):
    114         return self.raw
    115 
    116 
    117 class Class():
    118     def __init__(self, pkg, line, raw, blame):
    119         self.pkg = pkg
    120         self.line = line
    121         self.raw = raw.strip(" {;")
    122         self.blame = blame
    123         self.ctors = []
    124         self.fields = []
    125         self.methods = []
    126 
    127         raw = raw.split()
    128         self.split = list(raw)
    129         if "class" in raw:
    130             self.fullname = raw[raw.index("class")+1]
    131         elif "interface" in raw:
    132             self.fullname = raw[raw.index("interface")+1]
    133         else:
    134             raise ValueError("Funky class type %s" % (self.raw))
    135 
    136         if "extends" in raw:
    137             self.extends = raw[raw.index("extends")+1]
    138             self.extends_path = self.extends.split(".")
    139         else:
    140             self.extends = None
    141             self.extends_path = []
    142 
    143         self.fullname = self.pkg.name + "." + self.fullname
    144         self.fullname_path = self.fullname.split(".")
    145 
    146         self.name = self.fullname[self.fullname.rindex(".")+1:]
    147 
    148     def __repr__(self):
    149         return self.raw
    150 
    151 
    152 class Package():
    153     def __init__(self, line, raw, blame):
    154         self.line = line
    155         self.raw = raw.strip(" {;")
    156         self.blame = blame
    157 
    158         raw = raw.split()
    159         self.name = raw[raw.index("package")+1]
    160         self.name_path = self.name.split(".")
    161 
    162     def __repr__(self):
    163         return self.raw
    164 
    165 
    166 def _parse_stream(f, clazz_cb=None):
    167     line = 0
    168     api = {}
    169     pkg = None
    170     clazz = None
    171     blame = None
    172 
    173     re_blame = re.compile("^([a-z0-9]{7,}) \(<([^>]+)>.+?\) (.+?)$")
    174     for raw in f:
    175         line += 1
    176         raw = raw.rstrip()
    177         match = re_blame.match(raw)
    178         if match is not None:
    179             blame = match.groups()[0:2]
    180             raw = match.groups()[2]
    181         else:
    182             blame = None
    183 
    184         if raw.startswith("package"):
    185             pkg = Package(line, raw, blame)
    186         elif raw.startswith("  ") and raw.endswith("{"):
    187             # When provided with class callback, we treat as incremental
    188             # parse and don't build up entire API
    189             if clazz and clazz_cb:
    190                 clazz_cb(clazz)
    191             clazz = Class(pkg, line, raw, blame)
    192             if not clazz_cb:
    193                 api[clazz.fullname] = clazz
    194         elif raw.startswith("    ctor"):
    195             clazz.ctors.append(Method(clazz, line, raw, blame))
    196         elif raw.startswith("    method"):
    197             clazz.methods.append(Method(clazz, line, raw, blame))
    198         elif raw.startswith("    field"):
    199             clazz.fields.append(Field(clazz, line, raw, blame))
    200 
    201     # Handle last trailing class
    202     if clazz and clazz_cb:
    203         clazz_cb(clazz)
    204 
    205     return api
    206 
    207 
    208 class Failure():
    209     def __init__(self, sig, clazz, detail, error, rule, msg):
    210         self.sig = sig
    211         self.error = error
    212         self.rule = rule
    213         self.msg = msg
    214 
    215         if error:
    216             self.head = "Error %s" % (rule) if rule else "Error"
    217             dump = "%s%s:%s %s" % (format(fg=RED, bg=BLACK, bold=True), self.head, format(reset=True), msg)
    218         else:
    219             self.head = "Warning %s" % (rule) if rule else "Warning"
    220             dump = "%s%s:%s %s" % (format(fg=YELLOW, bg=BLACK, bold=True), self.head, format(reset=True), msg)
    221 
    222         self.line = clazz.line
    223         blame = clazz.blame
    224         if detail is not None:
    225             dump += "\n    in " + repr(detail)
    226             self.line = detail.line
    227             blame = detail.blame
    228         dump += "\n    in " + repr(clazz)
    229         dump += "\n    in " + repr(clazz.pkg)
    230         dump += "\n    at line " + repr(self.line)
    231         if blame is not None:
    232             dump += "\n    last modified by %s in %s" % (blame[1], blame[0])
    233 
    234         self.dump = dump
    235 
    236     def __repr__(self):
    237         return self.dump
    238 
    239 
    240 failures = {}
    241 
    242 def _fail(clazz, detail, error, rule, msg):
    243     """Records an API failure to be processed later."""
    244     global failures
    245 
    246     sig = "%s-%s-%s" % (clazz.fullname, repr(detail), msg)
    247     sig = sig.replace(" deprecated ", " ")
    248 
    249     failures[sig] = Failure(sig, clazz, detail, error, rule, msg)
    250 
    251 
    252 def warn(clazz, detail, rule, msg):
    253     _fail(clazz, detail, False, rule, msg)
    254 
    255 def error(clazz, detail, rule, msg):
    256     _fail(clazz, detail, True, rule, msg)
    257 
    258 
    259 def verify_constants(clazz):
    260     """All static final constants must be FOO_NAME style."""
    261     if re.match("android\.R\.[a-z]+", clazz.fullname): return
    262     if clazz.fullname.startswith("android.os.Build"): return
    263     if clazz.fullname == "android.system.OsConstants": return
    264 
    265     req = ["java.lang.String","byte","short","int","long","float","double","boolean","char"]
    266     for f in clazz.fields:
    267         if "static" in f.split and "final" in f.split:
    268             if re.match("[A-Z0-9_]+", f.name) is None:
    269                 error(clazz, f, "C2", "Constant field names must be FOO_NAME")
    270             if f.typ != "java.lang.String":
    271                 if f.name.startswith("MIN_") or f.name.startswith("MAX_"):
    272                     warn(clazz, f, "C8", "If min/max could change in future, make them dynamic methods")
    273             if f.typ in req and f.value is None:
    274                 error(clazz, f, None, "All constants must be defined at compile time")
    275 
    276 
    277 def verify_enums(clazz):
    278     """Enums are bad, mmkay?"""
    279     if "extends java.lang.Enum" in clazz.raw:
    280         error(clazz, None, "F5", "Enums are not allowed")
    281 
    282 
    283 def verify_class_names(clazz):
    284     """Try catching malformed class names like myMtp or MTPUser."""
    285     if clazz.fullname.startswith("android.opengl"): return
    286     if clazz.fullname.startswith("android.renderscript"): return
    287     if re.match("android\.R\.[a-z]+", clazz.fullname): return
    288 
    289     if re.search("[A-Z]{2,}", clazz.name) is not None:
    290         warn(clazz, None, "S1", "Class names with acronyms should be Mtp not MTP")
    291     if re.match("[^A-Z]", clazz.name):
    292         error(clazz, None, "S1", "Class must start with uppercase char")
    293 
    294 
    295 def verify_method_names(clazz):
    296     """Try catching malformed method names, like Foo() or getMTU()."""
    297     if clazz.fullname.startswith("android.opengl"): return
    298     if clazz.fullname.startswith("android.renderscript"): return
    299     if clazz.fullname == "android.system.OsConstants": return
    300 
    301     for m in clazz.methods:
    302         if re.search("[A-Z]{2,}", m.name) is not None:
    303             warn(clazz, m, "S1", "Method names with acronyms should be getMtu() instead of getMTU()")
    304         if re.match("[^a-z]", m.name):
    305             error(clazz, m, "S1", "Method name must start with lowercase char")
    306 
    307 
    308 def verify_callbacks(clazz):
    309     """Verify Callback classes.
    310     All callback classes must be abstract.
    311     All methods must follow onFoo() naming style."""
    312     if clazz.fullname == "android.speech.tts.SynthesisCallback": return
    313 
    314     if clazz.name.endswith("Callbacks"):
    315         error(clazz, None, "L1", "Callback class names should be singular")
    316     if clazz.name.endswith("Observer"):
    317         warn(clazz, None, "L1", "Class should be named FooCallback")
    318 
    319     if clazz.name.endswith("Callback"):
    320         if "interface" in clazz.split:
    321             error(clazz, None, "CL3", "Callbacks must be abstract class to enable extension in future API levels")
    322 
    323         for m in clazz.methods:
    324             if not re.match("on[A-Z][a-z]*", m.name):
    325                 error(clazz, m, "L1", "Callback method names must be onFoo() style")
    326 
    327 
    328 def verify_listeners(clazz):
    329     """Verify Listener classes.
    330     All Listener classes must be interface.
    331     All methods must follow onFoo() naming style.
    332     If only a single method, it must match class name:
    333         interface OnFooListener { void onFoo() }"""
    334 
    335     if clazz.name.endswith("Listener"):
    336         if " abstract class " in clazz.raw:
    337             error(clazz, None, "L1", "Listeners should be an interface, or otherwise renamed Callback")
    338 
    339         for m in clazz.methods:
    340             if not re.match("on[A-Z][a-z]*", m.name):
    341                 error(clazz, m, "L1", "Listener method names must be onFoo() style")
    342 
    343         if len(clazz.methods) == 1 and clazz.name.startswith("On"):
    344             m = clazz.methods[0]
    345             if (m.name + "Listener").lower() != clazz.name.lower():
    346                 error(clazz, m, "L1", "Single listener method name must match class name")
    347 
    348 
    349 def verify_actions(clazz):
    350     """Verify intent actions.
    351     All action names must be named ACTION_FOO.
    352     All action values must be scoped by package and match name:
    353         package android.foo {
    354             String ACTION_BAR = "android.foo.action.BAR";
    355         }"""
    356     for f in clazz.fields:
    357         if f.value is None: continue
    358         if f.name.startswith("EXTRA_"): continue
    359         if f.name == "SERVICE_INTERFACE" or f.name == "PROVIDER_INTERFACE": continue
    360         if "INTERACTION" in f.name: continue
    361 
    362         if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
    363             if "_ACTION" in f.name or "ACTION_" in f.name or ".action." in f.value.lower():
    364                 if not f.name.startswith("ACTION_"):
    365                     error(clazz, f, "C3", "Intent action constant name must be ACTION_FOO")
    366                 else:
    367                     if clazz.fullname == "android.content.Intent":
    368                         prefix = "android.intent.action"
    369                     elif clazz.fullname == "android.provider.Settings":
    370                         prefix = "android.settings"
    371                     elif clazz.fullname == "android.app.admin.DevicePolicyManager" or clazz.fullname == "android.app.admin.DeviceAdminReceiver":
    372                         prefix = "android.app.action"
    373                     else:
    374                         prefix = clazz.pkg.name + ".action"
    375                     expected = prefix + "." + f.name[7:]
    376                     if f.value != expected:
    377                         error(clazz, f, "C4", "Inconsistent action value; expected %s" % (expected))
    378 
    379 
    380 def verify_extras(clazz):
    381     """Verify intent extras.
    382     All extra names must be named EXTRA_FOO.
    383     All extra values must be scoped by package and match name:
    384         package android.foo {
    385             String EXTRA_BAR = "android.foo.extra.BAR";
    386         }"""
    387     if clazz.fullname == "android.app.Notification": return
    388     if clazz.fullname == "android.appwidget.AppWidgetManager": return
    389 
    390     for f in clazz.fields:
    391         if f.value is None: continue
    392         if f.name.startswith("ACTION_"): continue
    393 
    394         if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
    395             if "_EXTRA" in f.name or "EXTRA_" in f.name or ".extra" in f.value.lower():
    396                 if not f.name.startswith("EXTRA_"):
    397                     error(clazz, f, "C3", "Intent extra must be EXTRA_FOO")
    398                 else:
    399                     if clazz.pkg.name == "android.content" and clazz.name == "Intent":
    400                         prefix = "android.intent.extra"
    401                     elif clazz.pkg.name == "android.app.admin":
    402                         prefix = "android.app.extra"
    403                     else:
    404                         prefix = clazz.pkg.name + ".extra"
    405                     expected = prefix + "." + f.name[6:]
    406                     if f.value != expected:
    407                         error(clazz, f, "C4", "Inconsistent extra value; expected %s" % (expected))
    408 
    409 
    410 def verify_equals(clazz):
    411     """Verify that equals() and hashCode() must be overridden together."""
    412     eq = False
    413     hc = False
    414     for m in clazz.methods:
    415         if " static " in m.raw: continue
    416         if "boolean equals(java.lang.Object)" in m.raw: eq = True
    417         if "int hashCode()" in m.raw: hc = True
    418     if eq != hc:
    419         error(clazz, None, "M8", "Must override both equals and hashCode; missing one")
    420 
    421 
    422 def verify_parcelable(clazz):
    423     """Verify that Parcelable objects aren't hiding required bits."""
    424     if "implements android.os.Parcelable" in clazz.raw:
    425         creator = [ i for i in clazz.fields if i.name == "CREATOR" ]
    426         write = [ i for i in clazz.methods if i.name == "writeToParcel" ]
    427         describe = [ i for i in clazz.methods if i.name == "describeContents" ]
    428 
    429         if len(creator) == 0 or len(write) == 0 or len(describe) == 0:
    430             error(clazz, None, "FW3", "Parcelable requires CREATOR, writeToParcel, and describeContents; missing one")
    431 
    432         if " final class " not in clazz.raw:
    433             error(clazz, None, "FW8", "Parcelable classes must be final")
    434 
    435 
    436 def verify_protected(clazz):
    437     """Verify that no protected methods or fields are allowed."""
    438     for m in clazz.methods:
    439         if "protected" in m.split:
    440             error(clazz, m, "M7", "Protected methods not allowed; must be public")
    441     for f in clazz.fields:
    442         if "protected" in f.split:
    443             error(clazz, f, "M7", "Protected fields not allowed; must be public")
    444 
    445 
    446 def verify_fields(clazz):
    447     """Verify that all exposed fields are final.
    448     Exposed fields must follow myName style.
    449     Catch internal mFoo objects being exposed."""
    450 
    451     IGNORE_BARE_FIELDS = [
    452         "android.app.ActivityManager.RecentTaskInfo",
    453         "android.app.Notification",
    454         "android.content.pm.ActivityInfo",
    455         "android.content.pm.ApplicationInfo",
    456         "android.content.pm.ComponentInfo",
    457         "android.content.pm.ResolveInfo",
    458         "android.content.pm.FeatureGroupInfo",
    459         "android.content.pm.InstrumentationInfo",
    460         "android.content.pm.PackageInfo",
    461         "android.content.pm.PackageItemInfo",
    462         "android.content.res.Configuration",
    463         "android.graphics.BitmapFactory.Options",
    464         "android.os.Message",
    465         "android.system.StructPollfd",
    466     ]
    467 
    468     for f in clazz.fields:
    469         if not "final" in f.split:
    470             if clazz.fullname in IGNORE_BARE_FIELDS:
    471                 pass
    472             elif clazz.fullname.endswith("LayoutParams"):
    473                 pass
    474             elif clazz.fullname.startswith("android.util.Mutable"):
    475                 pass
    476             else:
    477                 error(clazz, f, "F2", "Bare fields must be marked final, or add accessors if mutable")
    478 
    479         if not "static" in f.split:
    480             if not re.match("[a-z]([a-zA-Z]+)?", f.name):
    481                 error(clazz, f, "S1", "Non-static fields must be named using myField style")
    482 
    483         if re.match("[ms][A-Z]", f.name):
    484             error(clazz, f, "F1", "Internal objects must not be exposed")
    485 
    486         if re.match("[A-Z_]+", f.name):
    487             if "static" not in f.split or "final" not in f.split:
    488                 error(clazz, f, "C2", "Constants must be marked static final")
    489 
    490 
    491 def verify_register(clazz):
    492     """Verify parity of registration methods.
    493     Callback objects use register/unregister methods.
    494     Listener objects use add/remove methods."""
    495     methods = [ m.name for m in clazz.methods ]
    496     for m in clazz.methods:
    497         if "Callback" in m.raw:
    498             if m.name.startswith("register"):
    499                 other = "unregister" + m.name[8:]
    500                 if other not in methods:
    501                     error(clazz, m, "L2", "Missing unregister method")
    502             if m.name.startswith("unregister"):
    503                 other = "register" + m.name[10:]
    504                 if other not in methods:
    505                     error(clazz, m, "L2", "Missing register method")
    506 
    507             if m.name.startswith("add") or m.name.startswith("remove"):
    508                 error(clazz, m, "L3", "Callback methods should be named register/unregister")
    509 
    510         if "Listener" in m.raw:
    511             if m.name.startswith("add"):
    512                 other = "remove" + m.name[3:]
    513                 if other not in methods:
    514                     error(clazz, m, "L2", "Missing remove method")
    515             if m.name.startswith("remove") and not m.name.startswith("removeAll"):
    516                 other = "add" + m.name[6:]
    517                 if other not in methods:
    518                     error(clazz, m, "L2", "Missing add method")
    519 
    520             if m.name.startswith("register") or m.name.startswith("unregister"):
    521                 error(clazz, m, "L3", "Listener methods should be named add/remove")
    522 
    523 
    524 def verify_sync(clazz):
    525     """Verify synchronized methods aren't exposed."""
    526     for m in clazz.methods:
    527         if "synchronized" in m.split:
    528             error(clazz, m, "M5", "Internal locks must not be exposed")
    529 
    530 
    531 def verify_intent_builder(clazz):
    532     """Verify that Intent builders are createFooIntent() style."""
    533     if clazz.name == "Intent": return
    534 
    535     for m in clazz.methods:
    536         if m.typ == "android.content.Intent":
    537             if m.name.startswith("create") and m.name.endswith("Intent"):
    538                 pass
    539             else:
    540                 warn(clazz, m, "FW1", "Methods creating an Intent should be named createFooIntent()")
    541 
    542 
    543 def verify_helper_classes(clazz):
    544     """Verify that helper classes are named consistently with what they extend.
    545     All developer extendable methods should be named onFoo()."""
    546     test_methods = False
    547     if "extends android.app.Service" in clazz.raw:
    548         test_methods = True
    549         if not clazz.name.endswith("Service"):
    550             error(clazz, None, "CL4", "Inconsistent class name; should be FooService")
    551 
    552         found = False
    553         for f in clazz.fields:
    554             if f.name == "SERVICE_INTERFACE":
    555                 found = True
    556                 if f.value != clazz.fullname:
    557                     error(clazz, f, "C4", "Inconsistent interface constant; expected %s" % (clazz.fullname))
    558 
    559     if "extends android.content.ContentProvider" in clazz.raw:
    560         test_methods = True
    561         if not clazz.name.endswith("Provider"):
    562             error(clazz, None, "CL4", "Inconsistent class name; should be FooProvider")
    563 
    564         found = False
    565         for f in clazz.fields:
    566             if f.name == "PROVIDER_INTERFACE":
    567                 found = True
    568                 if f.value != clazz.fullname:
    569                     error(clazz, f, "C4", "Inconsistent interface constant; expected %s" % (clazz.fullname))
    570 
    571     if "extends android.content.BroadcastReceiver" in clazz.raw:
    572         test_methods = True
    573         if not clazz.name.endswith("Receiver"):
    574             error(clazz, None, "CL4", "Inconsistent class name; should be FooReceiver")
    575 
    576     if "extends android.app.Activity" in clazz.raw:
    577         test_methods = True
    578         if not clazz.name.endswith("Activity"):
    579             error(clazz, None, "CL4", "Inconsistent class name; should be FooActivity")
    580 
    581     if test_methods:
    582         for m in clazz.methods:
    583             if "final" in m.split: continue
    584             if not re.match("on[A-Z]", m.name):
    585                 if "abstract" in m.split:
    586                     warn(clazz, m, None, "Methods implemented by developers should be named onFoo()")
    587                 else:
    588                     warn(clazz, m, None, "If implemented by developer, should be named onFoo(); otherwise consider marking final")
    589 
    590 
    591 def verify_builder(clazz):
    592     """Verify builder classes.
    593     Methods should return the builder to enable chaining."""
    594     if " extends " in clazz.raw: return
    595     if not clazz.name.endswith("Builder"): return
    596 
    597     if clazz.name != "Builder":
    598         warn(clazz, None, None, "Builder should be defined as inner class")
    599 
    600     has_build = False
    601     for m in clazz.methods:
    602         if m.name == "build":
    603             has_build = True
    604             continue
    605 
    606         if m.name.startswith("get"): continue
    607         if m.name.startswith("clear"): continue
    608 
    609         if m.name.startswith("with"):
    610             warn(clazz, m, None, "Builder methods names should use setFoo() style")
    611 
    612         if m.name.startswith("set"):
    613             if not m.typ.endswith(clazz.fullname):
    614                 warn(clazz, m, "M4", "Methods must return the builder object")
    615 
    616     if not has_build:
    617         warn(clazz, None, None, "Missing build() method")
    618 
    619 
    620 def verify_aidl(clazz):
    621     """Catch people exposing raw AIDL."""
    622     if "extends android.os.Binder" in clazz.raw or "implements android.os.IInterface" in clazz.raw:
    623         error(clazz, None, None, "Raw AIDL interfaces must not be exposed")
    624 
    625 
    626 def verify_internal(clazz):
    627     """Catch people exposing internal classes."""
    628     if clazz.pkg.name.startswith("com.android"):
    629         error(clazz, None, None, "Internal classes must not be exposed")
    630 
    631 
    632 def verify_layering(clazz):
    633     """Catch package layering violations.
    634     For example, something in android.os depending on android.app."""
    635     ranking = [
    636         ["android.service","android.accessibilityservice","android.inputmethodservice","android.printservice","android.appwidget","android.webkit","android.preference","android.gesture","android.print"],
    637         "android.app",
    638         "android.widget",
    639         "android.view",
    640         "android.animation",
    641         "android.provider",
    642         ["android.content","android.graphics.drawable"],
    643         "android.database",
    644         "android.graphics",
    645         "android.text",
    646         "android.os",
    647         "android.util"
    648     ]
    649 
    650     def rank(p):
    651         for i in range(len(ranking)):
    652             if isinstance(ranking[i], list):
    653                 for j in ranking[i]:
    654                     if p.startswith(j): return i
    655             else:
    656                 if p.startswith(ranking[i]): return i
    657 
    658     cr = rank(clazz.pkg.name)
    659     if cr is None: return
    660 
    661     for f in clazz.fields:
    662         ir = rank(f.typ)
    663         if ir and ir < cr:
    664             warn(clazz, f, "FW6", "Field type violates package layering")
    665 
    666     for m in clazz.methods:
    667         ir = rank(m.typ)
    668         if ir and ir < cr:
    669             warn(clazz, m, "FW6", "Method return type violates package layering")
    670         for arg in m.args:
    671             ir = rank(arg)
    672             if ir and ir < cr:
    673                 warn(clazz, m, "FW6", "Method argument type violates package layering")
    674 
    675 
    676 def verify_boolean(clazz):
    677     """Verifies that boolean accessors are named correctly.
    678     For example, hasFoo() and setHasFoo()."""
    679 
    680     def is_get(m): return len(m.args) == 0 and m.typ == "boolean"
    681     def is_set(m): return len(m.args) == 1 and m.args[0] == "boolean"
    682 
    683     gets = [ m for m in clazz.methods if is_get(m) ]
    684     sets = [ m for m in clazz.methods if is_set(m) ]
    685 
    686     def error_if_exists(methods, trigger, expected, actual):
    687         for m in methods:
    688             if m.name == actual:
    689                 error(clazz, m, "M6", "Symmetric method for %s must be named %s" % (trigger, expected))
    690 
    691     for m in clazz.methods:
    692         if is_get(m):
    693             if re.match("is[A-Z]", m.name):
    694                 target = m.name[2:]
    695                 expected = "setIs" + target
    696                 error_if_exists(sets, m.name, expected, "setHas" + target)
    697             elif re.match("has[A-Z]", m.name):
    698                 target = m.name[3:]
    699                 expected = "setHas" + target
    700                 error_if_exists(sets, m.name, expected, "setIs" + target)
    701                 error_if_exists(sets, m.name, expected, "set" + target)
    702             elif re.match("get[A-Z]", m.name):
    703                 target = m.name[3:]
    704                 expected = "set" + target
    705                 error_if_exists(sets, m.name, expected, "setIs" + target)
    706                 error_if_exists(sets, m.name, expected, "setHas" + target)
    707 
    708         if is_set(m):
    709             if re.match("set[A-Z]", m.name):
    710                 target = m.name[3:]
    711                 expected = "get" + target
    712                 error_if_exists(sets, m.name, expected, "is" + target)
    713                 error_if_exists(sets, m.name, expected, "has" + target)
    714 
    715 
    716 def verify_collections(clazz):
    717     """Verifies that collection types are interfaces."""
    718     if clazz.fullname == "android.os.Bundle": return
    719 
    720     bad = ["java.util.Vector", "java.util.LinkedList", "java.util.ArrayList", "java.util.Stack",
    721            "java.util.HashMap", "java.util.HashSet", "android.util.ArraySet", "android.util.ArrayMap"]
    722     for m in clazz.methods:
    723         if m.typ in bad:
    724             error(clazz, m, "CL2", "Return type is concrete collection; must be higher-level interface")
    725         for arg in m.args:
    726             if arg in bad:
    727                 error(clazz, m, "CL2", "Argument is concrete collection; must be higher-level interface")
    728 
    729 
    730 def verify_flags(clazz):
    731     """Verifies that flags are non-overlapping."""
    732     known = collections.defaultdict(int)
    733     for f in clazz.fields:
    734         if "FLAG_" in f.name:
    735             try:
    736                 val = int(f.value)
    737             except:
    738                 continue
    739 
    740             scope = f.name[0:f.name.index("FLAG_")]
    741             if val & known[scope]:
    742                 warn(clazz, f, "C1", "Found overlapping flag constant value")
    743             known[scope] |= val
    744 
    745 
    746 def verify_exception(clazz):
    747     """Verifies that methods don't throw generic exceptions."""
    748     for m in clazz.methods:
    749         if "throws java.lang.Exception" in m.raw or "throws java.lang.Throwable" in m.raw or "throws java.lang.Error" in m.raw:
    750             error(clazz, m, "S1", "Methods must not throw generic exceptions")
    751 
    752         if "throws android.os.RemoteException" in m.raw:
    753             if clazz.name == "android.content.ContentProviderClient": continue
    754             if clazz.name == "android.os.Binder": continue
    755             if clazz.name == "android.os.IBinder": continue
    756 
    757             error(clazz, m, "FW9", "Methods calling into system server should rethrow RemoteException as RuntimeException")
    758 
    759 
    760 def verify_google(clazz):
    761     """Verifies that APIs never reference Google."""
    762 
    763     if re.search("google", clazz.raw, re.IGNORECASE):
    764         error(clazz, None, None, "Must never reference Google")
    765 
    766     test = []
    767     test.extend(clazz.ctors)
    768     test.extend(clazz.fields)
    769     test.extend(clazz.methods)
    770 
    771     for t in test:
    772         if re.search("google", t.raw, re.IGNORECASE):
    773             error(clazz, t, None, "Must never reference Google")
    774 
    775 
    776 def verify_bitset(clazz):
    777     """Verifies that we avoid using heavy BitSet."""
    778 
    779     for f in clazz.fields:
    780         if f.typ == "java.util.BitSet":
    781             error(clazz, f, None, "Field type must not be heavy BitSet")
    782 
    783     for m in clazz.methods:
    784         if m.typ == "java.util.BitSet":
    785             error(clazz, m, None, "Return type must not be heavy BitSet")
    786         for arg in m.args:
    787             if arg == "java.util.BitSet":
    788                 error(clazz, m, None, "Argument type must not be heavy BitSet")
    789 
    790 
    791 def verify_manager(clazz):
    792     """Verifies that FooManager is only obtained from Context."""
    793 
    794     if not clazz.name.endswith("Manager"): return
    795 
    796     for c in clazz.ctors:
    797         error(clazz, c, None, "Managers must always be obtained from Context; no direct constructors")
    798 
    799     for m in clazz.methods:
    800         if m.typ == clazz.fullname:
    801             error(clazz, m, None, "Managers must always be obtained from Context")
    802 
    803 
    804 def verify_boxed(clazz):
    805     """Verifies that methods avoid boxed primitives."""
    806 
    807     boxed = ["java.lang.Number","java.lang.Byte","java.lang.Double","java.lang.Float","java.lang.Integer","java.lang.Long","java.lang.Short"]
    808 
    809     for c in clazz.ctors:
    810         for arg in c.args:
    811             if arg in boxed:
    812                 error(clazz, c, "M11", "Must avoid boxed primitives")
    813 
    814     for f in clazz.fields:
    815         if f.typ in boxed:
    816             error(clazz, f, "M11", "Must avoid boxed primitives")
    817 
    818     for m in clazz.methods:
    819         if m.typ in boxed:
    820             error(clazz, m, "M11", "Must avoid boxed primitives")
    821         for arg in m.args:
    822             if arg in boxed:
    823                 error(clazz, m, "M11", "Must avoid boxed primitives")
    824 
    825 
    826 def verify_static_utils(clazz):
    827     """Verifies that helper classes can't be constructed."""
    828     if clazz.fullname.startswith("android.opengl"): return
    829     if clazz.fullname.startswith("android.R"): return
    830 
    831     # Only care about classes with default constructors
    832     if len(clazz.ctors) == 1 and len(clazz.ctors[0].args) == 0:
    833         test = []
    834         test.extend(clazz.fields)
    835         test.extend(clazz.methods)
    836 
    837         if len(test) == 0: return
    838         for t in test:
    839             if "static" not in t.split:
    840                 return
    841 
    842         error(clazz, None, None, "Fully-static utility classes must not have constructor")
    843 
    844 
    845 def verify_overload_args(clazz):
    846     """Verifies that method overloads add new arguments at the end."""
    847     if clazz.fullname.startswith("android.opengl"): return
    848 
    849     overloads = collections.defaultdict(list)
    850     for m in clazz.methods:
    851         if "deprecated" in m.split: continue
    852         overloads[m.name].append(m)
    853 
    854     for name, methods in overloads.items():
    855         if len(methods) <= 1: continue
    856 
    857         # Look for arguments common across all overloads
    858         def cluster(args):
    859             count = collections.defaultdict(int)
    860             res = set()
    861             for i in range(len(args)):
    862                 a = args[i]
    863                 res.add("%s#%d" % (a, count[a]))
    864                 count[a] += 1
    865             return res
    866 
    867         common_args = cluster(methods[0].args)
    868         for m in methods:
    869             common_args = common_args & cluster(m.args)
    870 
    871         if len(common_args) == 0: continue
    872 
    873         # Require that all common arguments are present at start of signature
    874         locked_sig = None
    875         for m in methods:
    876             sig = m.args[0:len(common_args)]
    877             if not common_args.issubset(cluster(sig)):
    878                 warn(clazz, m, "M2", "Expected common arguments [%s] at beginning of overloaded method" % (", ".join(common_args)))
    879             elif not locked_sig:
    880                 locked_sig = sig
    881             elif locked_sig != sig:
    882                 error(clazz, m, "M2", "Expected consistent argument ordering between overloads: %s..." % (", ".join(locked_sig)))
    883 
    884 
    885 def verify_callback_handlers(clazz):
    886     """Verifies that methods adding listener/callback have overload
    887     for specifying delivery thread."""
    888 
    889     # Ignore UI packages which assume main thread
    890     skip = [
    891         "animation",
    892         "view",
    893         "graphics",
    894         "transition",
    895         "widget",
    896         "webkit",
    897     ]
    898     for s in skip:
    899         if s in clazz.pkg.name_path: return
    900         if s in clazz.extends_path: return
    901 
    902     # Ignore UI classes which assume main thread
    903     if "app" in clazz.pkg.name_path or "app" in clazz.extends_path:
    904         for s in ["ActionBar","Dialog","Application","Activity","Fragment","Loader"]:
    905             if s in clazz.fullname: return
    906     if "content" in clazz.pkg.name_path or "content" in clazz.extends_path:
    907         for s in ["Loader"]:
    908             if s in clazz.fullname: return
    909 
    910     found = {}
    911     by_name = collections.defaultdict(list)
    912     for m in clazz.methods:
    913         if m.name.startswith("unregister"): continue
    914         if m.name.startswith("remove"): continue
    915         if re.match("on[A-Z]+", m.name): continue
    916 
    917         by_name[m.name].append(m)
    918 
    919         for a in m.args:
    920             if a.endswith("Listener") or a.endswith("Callback") or a.endswith("Callbacks"):
    921                 found[m.name] = m
    922 
    923     for f in found.values():
    924         takes_handler = False
    925         for m in by_name[f.name]:
    926             if "android.os.Handler" in m.args:
    927                 takes_handler = True
    928         if not takes_handler:
    929             warn(clazz, f, "L1", "Registration methods should have overload that accepts delivery Handler")
    930 
    931 
    932 def verify_context_first(clazz):
    933     """Verifies that methods accepting a Context keep it the first argument."""
    934     examine = clazz.ctors + clazz.methods
    935     for m in examine:
    936         if len(m.args) > 1 and m.args[0] != "android.content.Context":
    937             if "android.content.Context" in m.args[1:]:
    938                 error(clazz, m, "M3", "Context is distinct, so it must be the first argument")
    939         if len(m.args) > 1 and m.args[0] != "android.content.ContentResolver":
    940             if "android.content.ContentResolver" in m.args[1:]:
    941                 error(clazz, m, "M3", "ContentResolver is distinct, so it must be the first argument")
    942 
    943 
    944 def verify_listener_last(clazz):
    945     """Verifies that methods accepting a Listener or Callback keep them as last arguments."""
    946     examine = clazz.ctors + clazz.methods
    947     for m in examine:
    948         if "Listener" in m.name or "Callback" in m.name: continue
    949         found = False
    950         for a in m.args:
    951             if a.endswith("Callback") or a.endswith("Callbacks") or a.endswith("Listener"):
    952                 found = True
    953             elif found and a != "android.os.Handler":
    954                 warn(clazz, m, "M3", "Listeners should always be at end of argument list")
    955 
    956 
    957 def verify_resource_names(clazz):
    958     """Verifies that resource names have consistent case."""
    959     if not re.match("android\.R\.[a-z]+", clazz.fullname): return
    960 
    961     # Resources defined by files are foo_bar_baz
    962     if clazz.name in ["anim","animator","color","dimen","drawable","interpolator","layout","transition","menu","mipmap","string","plurals","raw","xml"]:
    963         for f in clazz.fields:
    964             if re.match("[a-z1-9_]+$", f.name): continue
    965             error(clazz, f, None, "Expected resource name in this class to be foo_bar_baz style")
    966 
    967     # Resources defined inside files are fooBarBaz
    968     if clazz.name in ["array","attr","id","bool","fraction","integer"]:
    969         for f in clazz.fields:
    970             if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue
    971             if re.match("layout_[a-z][a-zA-Z1-9]*$", f.name): continue
    972             if re.match("state_[a-z_]*$", f.name): continue
    973 
    974             if re.match("[a-z][a-zA-Z1-9]*$", f.name): continue
    975             error(clazz, f, "C7", "Expected resource name in this class to be fooBarBaz style")
    976 
    977     # Styles are FooBar_Baz
    978     if clazz.name in ["style"]:
    979         for f in clazz.fields:
    980             if re.match("[A-Z][A-Za-z1-9]+(_[A-Z][A-Za-z1-9]+?)*$", f.name): continue
    981             error(clazz, f, "C7", "Expected resource name in this class to be FooBar_Baz style")
    982 
    983 
    984 def verify_files(clazz):
    985     """Verifies that methods accepting File also accept streams."""
    986 
    987     has_file = set()
    988     has_stream = set()
    989 
    990     test = []
    991     test.extend(clazz.ctors)
    992     test.extend(clazz.methods)
    993 
    994     for m in test:
    995         if "java.io.File" in m.args:
    996             has_file.add(m)
    997         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:
    998             has_stream.add(m.name)
    999 
   1000     for m in has_file:
   1001         if m.name not in has_stream:
   1002             warn(clazz, m, "M10", "Methods accepting File should also accept FileDescriptor or streams")
   1003 
   1004 
   1005 def verify_manager_list(clazz):
   1006     """Verifies that managers return List<? extends Parcelable> instead of arrays."""
   1007 
   1008     if not clazz.name.endswith("Manager"): return
   1009 
   1010     for m in clazz.methods:
   1011         if m.typ.startswith("android.") and m.typ.endswith("[]"):
   1012             warn(clazz, m, None, "Methods should return List<? extends Parcelable> instead of Parcelable[] to support ParceledListSlice under the hood")
   1013 
   1014 
   1015 def verify_abstract_inner(clazz):
   1016     """Verifies that abstract inner classes are static."""
   1017 
   1018     if re.match(".+?\.[A-Z][^\.]+\.[A-Z]", clazz.fullname):
   1019         if " abstract " in clazz.raw and " static " not in clazz.raw:
   1020             warn(clazz, None, None, "Abstract inner classes should be static to improve testability")
   1021 
   1022 
   1023 def verify_runtime_exceptions(clazz):
   1024     """Verifies that runtime exceptions aren't listed in throws."""
   1025 
   1026     banned = [
   1027         "java.lang.NullPointerException",
   1028         "java.lang.ClassCastException",
   1029         "java.lang.IndexOutOfBoundsException",
   1030         "java.lang.reflect.UndeclaredThrowableException",
   1031         "java.lang.reflect.MalformedParametersException",
   1032         "java.lang.reflect.MalformedParameterizedTypeException",
   1033         "java.lang.invoke.WrongMethodTypeException",
   1034         "java.lang.EnumConstantNotPresentException",
   1035         "java.lang.IllegalMonitorStateException",
   1036         "java.lang.SecurityException",
   1037         "java.lang.UnsupportedOperationException",
   1038         "java.lang.annotation.AnnotationTypeMismatchException",
   1039         "java.lang.annotation.IncompleteAnnotationException",
   1040         "java.lang.TypeNotPresentException",
   1041         "java.lang.IllegalStateException",
   1042         "java.lang.ArithmeticException",
   1043         "java.lang.IllegalArgumentException",
   1044         "java.lang.ArrayStoreException",
   1045         "java.lang.NegativeArraySizeException",
   1046         "java.util.MissingResourceException",
   1047         "java.util.EmptyStackException",
   1048         "java.util.concurrent.CompletionException",
   1049         "java.util.concurrent.RejectedExecutionException",
   1050         "java.util.IllformedLocaleException",
   1051         "java.util.ConcurrentModificationException",
   1052         "java.util.NoSuchElementException",
   1053         "java.io.UncheckedIOException",
   1054         "java.time.DateTimeException",
   1055         "java.security.ProviderException",
   1056         "java.nio.BufferUnderflowException",
   1057         "java.nio.BufferOverflowException",
   1058     ]
   1059 
   1060     test = []
   1061     test.extend(clazz.ctors)
   1062     test.extend(clazz.methods)
   1063 
   1064     for t in test:
   1065         if " throws " not in t.raw: continue
   1066         throws = t.raw[t.raw.index(" throws "):]
   1067         for b in banned:
   1068             if b in throws:
   1069                 error(clazz, t, None, "Methods must not mention RuntimeException subclasses in throws clauses")
   1070 
   1071 
   1072 def verify_error(clazz):
   1073     """Verifies that we always use Exception instead of Error."""
   1074     if not clazz.extends: return
   1075     if clazz.extends.endswith("Error"):
   1076         error(clazz, None, None, "Trouble must be reported through an Exception, not Error")
   1077     if clazz.extends.endswith("Exception") and not clazz.name.endswith("Exception"):
   1078         error(clazz, None, None, "Exceptions must be named FooException")
   1079 
   1080 
   1081 def verify_units(clazz):
   1082     """Verifies that we use consistent naming for units."""
   1083 
   1084     # If we find K, recommend replacing with V
   1085     bad = {
   1086         "Ns": "Nanos",
   1087         "Ms": "Millis or Micros",
   1088         "Sec": "Seconds", "Secs": "Seconds",
   1089         "Hr": "Hours", "Hrs": "Hours",
   1090         "Mo": "Months", "Mos": "Months",
   1091         "Yr": "Years", "Yrs": "Years",
   1092         "Byte": "Bytes", "Space": "Bytes",
   1093     }
   1094 
   1095     for m in clazz.methods:
   1096         if m.typ not in ["short","int","long"]: continue
   1097         for k, v in bad.iteritems():
   1098             if m.name.endswith(k):
   1099                 error(clazz, m, None, "Expected method name units to be " + v)
   1100         if m.name.endswith("Nanos") or m.name.endswith("Micros"):
   1101             warn(clazz, m, None, "Returned time values are strongly encouraged to be in milliseconds unless you need the extra precision")
   1102         if m.name.endswith("Seconds"):
   1103             error(clazz, m, None, "Returned time values must be in milliseconds")
   1104 
   1105     for m in clazz.methods:
   1106         typ = m.typ
   1107         if typ == "void":
   1108             if len(m.args) != 1: continue
   1109             typ = m.args[0]
   1110 
   1111         if m.name.endswith("Fraction") and typ != "float":
   1112             error(clazz, m, None, "Fractions must use floats")
   1113         if m.name.endswith("Percentage") and typ != "int":
   1114             error(clazz, m, None, "Percentage must use ints")
   1115 
   1116 
   1117 def verify_closable(clazz):
   1118     """Verifies that classes are AutoClosable."""
   1119     if "implements java.lang.AutoCloseable" in clazz.raw: return
   1120     if "implements java.io.Closeable" in clazz.raw: return
   1121 
   1122     for m in clazz.methods:
   1123         if len(m.args) > 0: continue
   1124         if m.name in ["close","release","destroy","finish","finalize","disconnect","shutdown","stop","free","quit"]:
   1125             warn(clazz, m, None, "Classes that release resources should implement AutoClosable and CloseGuard")
   1126             return
   1127 
   1128 
   1129 def examine_clazz(clazz):
   1130     """Find all style issues in the given class."""
   1131     if clazz.pkg.name.startswith("java"): return
   1132     if clazz.pkg.name.startswith("junit"): return
   1133     if clazz.pkg.name.startswith("org.apache"): return
   1134     if clazz.pkg.name.startswith("org.xml"): return
   1135     if clazz.pkg.name.startswith("org.json"): return
   1136     if clazz.pkg.name.startswith("org.w3c"): return
   1137     if clazz.pkg.name.startswith("android.icu."): return
   1138 
   1139     verify_constants(clazz)
   1140     verify_enums(clazz)
   1141     verify_class_names(clazz)
   1142     verify_method_names(clazz)
   1143     verify_callbacks(clazz)
   1144     verify_listeners(clazz)
   1145     verify_actions(clazz)
   1146     verify_extras(clazz)
   1147     verify_equals(clazz)
   1148     verify_parcelable(clazz)
   1149     verify_protected(clazz)
   1150     verify_fields(clazz)
   1151     verify_register(clazz)
   1152     verify_sync(clazz)
   1153     verify_intent_builder(clazz)
   1154     verify_helper_classes(clazz)
   1155     verify_builder(clazz)
   1156     verify_aidl(clazz)
   1157     verify_internal(clazz)
   1158     verify_layering(clazz)
   1159     verify_boolean(clazz)
   1160     verify_collections(clazz)
   1161     verify_flags(clazz)
   1162     verify_exception(clazz)
   1163     if not ALLOW_GOOGLE: verify_google(clazz)
   1164     verify_bitset(clazz)
   1165     verify_manager(clazz)
   1166     verify_boxed(clazz)
   1167     verify_static_utils(clazz)
   1168     verify_overload_args(clazz)
   1169     verify_callback_handlers(clazz)
   1170     verify_context_first(clazz)
   1171     verify_listener_last(clazz)
   1172     verify_resource_names(clazz)
   1173     verify_files(clazz)
   1174     verify_manager_list(clazz)
   1175     verify_abstract_inner(clazz)
   1176     verify_runtime_exceptions(clazz)
   1177     verify_error(clazz)
   1178     verify_units(clazz)
   1179     verify_closable(clazz)
   1180 
   1181 
   1182 def examine_stream(stream):
   1183     """Find all style issues in the given API stream."""
   1184     global failures
   1185     failures = {}
   1186     _parse_stream(stream, examine_clazz)
   1187     return failures
   1188 
   1189 
   1190 def examine_api(api):
   1191     """Find all style issues in the given parsed API."""
   1192     global failures
   1193     failures = {}
   1194     for key in sorted(api.keys()):
   1195         examine_clazz(api[key])
   1196     return failures
   1197 
   1198 
   1199 def verify_compat(cur, prev):
   1200     """Find any incompatible API changes between two levels."""
   1201     global failures
   1202 
   1203     def class_exists(api, test):
   1204         return test.fullname in api
   1205 
   1206     def ctor_exists(api, clazz, test):
   1207         for m in clazz.ctors:
   1208             if m.ident == test.ident: return True
   1209         return False
   1210 
   1211     def all_methods(api, clazz):
   1212         methods = list(clazz.methods)
   1213         if clazz.extends is not None:
   1214             methods.extend(all_methods(api, api[clazz.extends]))
   1215         return methods
   1216 
   1217     def method_exists(api, clazz, test):
   1218         methods = all_methods(api, clazz)
   1219         for m in methods:
   1220             if m.ident == test.ident: return True
   1221         return False
   1222 
   1223     def field_exists(api, clazz, test):
   1224         for f in clazz.fields:
   1225             if f.ident == test.ident: return True
   1226         return False
   1227 
   1228     failures = {}
   1229     for key in sorted(prev.keys()):
   1230         prev_clazz = prev[key]
   1231 
   1232         if not class_exists(cur, prev_clazz):
   1233             error(prev_clazz, None, None, "Class removed or incompatible change")
   1234             continue
   1235 
   1236         cur_clazz = cur[key]
   1237 
   1238         for test in prev_clazz.ctors:
   1239             if not ctor_exists(cur, cur_clazz, test):
   1240                 error(prev_clazz, prev_ctor, None, "Constructor removed or incompatible change")
   1241 
   1242         methods = all_methods(prev, prev_clazz)
   1243         for test in methods:
   1244             if not method_exists(cur, cur_clazz, test):
   1245                 error(prev_clazz, test, None, "Method removed or incompatible change")
   1246 
   1247         for test in prev_clazz.fields:
   1248             if not field_exists(cur, cur_clazz, test):
   1249                 error(prev_clazz, test, None, "Field removed or incompatible change")
   1250 
   1251     return failures
   1252 
   1253 
   1254 if __name__ == "__main__":
   1255     parser = argparse.ArgumentParser(description="Enforces common Android public API design \
   1256             patterns. It ignores lint messages from a previous API level, if provided.")
   1257     parser.add_argument("current.txt", type=argparse.FileType('r'), help="current.txt")
   1258     parser.add_argument("previous.txt", nargs='?', type=argparse.FileType('r'), default=None,
   1259             help="previous.txt")
   1260     parser.add_argument("--no-color", action='store_const', const=True,
   1261             help="Disable terminal colors")
   1262     parser.add_argument("--allow-google", action='store_const', const=True,
   1263             help="Allow references to Google")
   1264     args = vars(parser.parse_args())
   1265 
   1266     if args['no_color']:
   1267         USE_COLOR = False
   1268 
   1269     if args['allow_google']:
   1270         ALLOW_GOOGLE = True
   1271 
   1272     current_file = args['current.txt']
   1273     previous_file = args['previous.txt']
   1274 
   1275     with current_file as f:
   1276         cur_fail = examine_stream(f)
   1277     if not previous_file is None:
   1278         with previous_file as f:
   1279             prev_fail = examine_stream(f)
   1280 
   1281         # ignore errors from previous API level
   1282         for p in prev_fail:
   1283             if p in cur_fail:
   1284                 del cur_fail[p]
   1285 
   1286         """
   1287         # NOTE: disabled because of memory pressure
   1288         # look for compatibility issues
   1289         compat_fail = verify_compat(cur, prev)
   1290 
   1291         print "%s API compatibility issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
   1292         for f in sorted(compat_fail):
   1293             print compat_fail[f]
   1294             print
   1295         """
   1296 
   1297     print "%s API style issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
   1298     for f in sorted(cur_fail):
   1299         print cur_fail[f]
   1300         print
   1301