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"""
18Enforces common Android public API design patterns.  It ignores lint messages from
19a previous API level, if provided.
20
21Usage: apilint.py current.txt
22Usage: apilint.py current.txt previous.txt
23
24You 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
29import re, sys, collections, traceback, argparse
30
31
32BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
33
34ALLOW_GOOGLE = False
35USE_COLOR = True
36
37def 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
53class 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
79class 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
117class 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
152class 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
166def _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
208class 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
240failures = {}
241
242def _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
252def warn(clazz, detail, rule, msg):
253    _fail(clazz, detail, False, rule, msg)
254
255def error(clazz, detail, rule, msg):
256    _fail(clazz, detail, True, rule, msg)
257
258
259def 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
277def 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
283def 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
295def 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
308def 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
328def 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
349def 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
380def 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
410def 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
422def 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
436def 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
446def 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
491def 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
524def 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
531def 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
543def 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
591def 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
620def 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
626def 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
632def 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
676def 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
716def 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
730def 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
746def 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
760def 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
776def 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
791def 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
804def 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
826def 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
845def 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
885def 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
932def 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
944def 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
957def 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
984def 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
1005def 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
1015def 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
1023def 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
1072def 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
1081def 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
1117def 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
1129def 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
1182def 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
1190def 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
1199def 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
1254if __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