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