• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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) and
433            (" final deprecated class " not in clazz.raw)):
434            error(clazz, None, "FW8", "Parcelable classes must be final")
435
436
437def verify_protected(clazz):
438    """Verify that no protected methods or fields are allowed."""
439    for m in clazz.methods:
440        if "protected" in m.split:
441            error(clazz, m, "M7", "Protected methods not allowed; must be public")
442    for f in clazz.fields:
443        if "protected" in f.split:
444            error(clazz, f, "M7", "Protected fields not allowed; must be public")
445
446
447def verify_fields(clazz):
448    """Verify that all exposed fields are final.
449    Exposed fields must follow myName style.
450    Catch internal mFoo objects being exposed."""
451
452    IGNORE_BARE_FIELDS = [
453        "android.app.ActivityManager.RecentTaskInfo",
454        "android.app.Notification",
455        "android.content.pm.ActivityInfo",
456        "android.content.pm.ApplicationInfo",
457        "android.content.pm.ComponentInfo",
458        "android.content.pm.ResolveInfo",
459        "android.content.pm.FeatureGroupInfo",
460        "android.content.pm.InstrumentationInfo",
461        "android.content.pm.PackageInfo",
462        "android.content.pm.PackageItemInfo",
463        "android.content.res.Configuration",
464        "android.graphics.BitmapFactory.Options",
465        "android.os.Message",
466        "android.system.StructPollfd",
467    ]
468
469    for f in clazz.fields:
470        if not "final" in f.split:
471            if clazz.fullname in IGNORE_BARE_FIELDS:
472                pass
473            elif clazz.fullname.endswith("LayoutParams"):
474                pass
475            elif clazz.fullname.startswith("android.util.Mutable"):
476                pass
477            else:
478                error(clazz, f, "F2", "Bare fields must be marked final, or add accessors if mutable")
479
480        if not "static" in f.split:
481            if not re.match("[a-z]([a-zA-Z]+)?", f.name):
482                error(clazz, f, "S1", "Non-static fields must be named using myField style")
483
484        if re.match("[ms][A-Z]", f.name):
485            error(clazz, f, "F1", "Internal objects must not be exposed")
486
487        if re.match("[A-Z_]+", f.name):
488            if "static" not in f.split or "final" not in f.split:
489                error(clazz, f, "C2", "Constants must be marked static final")
490
491
492def verify_register(clazz):
493    """Verify parity of registration methods.
494    Callback objects use register/unregister methods.
495    Listener objects use add/remove methods."""
496    methods = [ m.name for m in clazz.methods ]
497    for m in clazz.methods:
498        if "Callback" in m.raw:
499            if m.name.startswith("register"):
500                other = "unregister" + m.name[8:]
501                if other not in methods:
502                    error(clazz, m, "L2", "Missing unregister method")
503            if m.name.startswith("unregister"):
504                other = "register" + m.name[10:]
505                if other not in methods:
506                    error(clazz, m, "L2", "Missing register method")
507
508            if m.name.startswith("add") or m.name.startswith("remove"):
509                error(clazz, m, "L3", "Callback methods should be named register/unregister")
510
511        if "Listener" in m.raw:
512            if m.name.startswith("add"):
513                other = "remove" + m.name[3:]
514                if other not in methods:
515                    error(clazz, m, "L2", "Missing remove method")
516            if m.name.startswith("remove") and not m.name.startswith("removeAll"):
517                other = "add" + m.name[6:]
518                if other not in methods:
519                    error(clazz, m, "L2", "Missing add method")
520
521            if m.name.startswith("register") or m.name.startswith("unregister"):
522                error(clazz, m, "L3", "Listener methods should be named add/remove")
523
524
525def verify_sync(clazz):
526    """Verify synchronized methods aren't exposed."""
527    for m in clazz.methods:
528        if "synchronized" in m.split:
529            error(clazz, m, "M5", "Internal locks must not be exposed")
530
531
532def verify_intent_builder(clazz):
533    """Verify that Intent builders are createFooIntent() style."""
534    if clazz.name == "Intent": return
535
536    for m in clazz.methods:
537        if m.typ == "android.content.Intent":
538            if m.name.startswith("create") and m.name.endswith("Intent"):
539                pass
540            else:
541                warn(clazz, m, "FW1", "Methods creating an Intent should be named createFooIntent()")
542
543
544def verify_helper_classes(clazz):
545    """Verify that helper classes are named consistently with what they extend.
546    All developer extendable methods should be named onFoo()."""
547    test_methods = False
548    if "extends android.app.Service" in clazz.raw:
549        test_methods = True
550        if not clazz.name.endswith("Service"):
551            error(clazz, None, "CL4", "Inconsistent class name; should be FooService")
552
553        found = False
554        for f in clazz.fields:
555            if f.name == "SERVICE_INTERFACE":
556                found = True
557                if f.value != clazz.fullname:
558                    error(clazz, f, "C4", "Inconsistent interface constant; expected %s" % (clazz.fullname))
559
560    if "extends android.content.ContentProvider" in clazz.raw:
561        test_methods = True
562        if not clazz.name.endswith("Provider"):
563            error(clazz, None, "CL4", "Inconsistent class name; should be FooProvider")
564
565        found = False
566        for f in clazz.fields:
567            if f.name == "PROVIDER_INTERFACE":
568                found = True
569                if f.value != clazz.fullname:
570                    error(clazz, f, "C4", "Inconsistent interface constant; expected %s" % (clazz.fullname))
571
572    if "extends android.content.BroadcastReceiver" in clazz.raw:
573        test_methods = True
574        if not clazz.name.endswith("Receiver"):
575            error(clazz, None, "CL4", "Inconsistent class name; should be FooReceiver")
576
577    if "extends android.app.Activity" in clazz.raw:
578        test_methods = True
579        if not clazz.name.endswith("Activity"):
580            error(clazz, None, "CL4", "Inconsistent class name; should be FooActivity")
581
582    if test_methods:
583        for m in clazz.methods:
584            if "final" in m.split: continue
585            if not re.match("on[A-Z]", m.name):
586                if "abstract" in m.split:
587                    warn(clazz, m, None, "Methods implemented by developers should be named onFoo()")
588                else:
589                    warn(clazz, m, None, "If implemented by developer, should be named onFoo(); otherwise consider marking final")
590
591
592def verify_builder(clazz):
593    """Verify builder classes.
594    Methods should return the builder to enable chaining."""
595    if " extends " in clazz.raw: return
596    if not clazz.name.endswith("Builder"): return
597
598    if clazz.name != "Builder":
599        warn(clazz, None, None, "Builder should be defined as inner class")
600
601    has_build = False
602    for m in clazz.methods:
603        if m.name == "build":
604            has_build = True
605            continue
606
607        if m.name.startswith("get"): continue
608        if m.name.startswith("clear"): continue
609
610        if m.name.startswith("with"):
611            warn(clazz, m, None, "Builder methods names should use setFoo() style")
612
613        if m.name.startswith("set"):
614            if not m.typ.endswith(clazz.fullname):
615                warn(clazz, m, "M4", "Methods must return the builder object")
616
617    if not has_build:
618        warn(clazz, None, None, "Missing build() method")
619
620
621def verify_aidl(clazz):
622    """Catch people exposing raw AIDL."""
623    if "extends android.os.Binder" in clazz.raw or "implements android.os.IInterface" in clazz.raw:
624        error(clazz, None, None, "Raw AIDL interfaces must not be exposed")
625
626
627def verify_internal(clazz):
628    """Catch people exposing internal classes."""
629    if clazz.pkg.name.startswith("com.android"):
630        error(clazz, None, None, "Internal classes must not be exposed")
631
632
633def verify_layering(clazz):
634    """Catch package layering violations.
635    For example, something in android.os depending on android.app."""
636    ranking = [
637        ["android.service","android.accessibilityservice","android.inputmethodservice","android.printservice","android.appwidget","android.webkit","android.preference","android.gesture","android.print"],
638        "android.app",
639        "android.widget",
640        "android.view",
641        "android.animation",
642        "android.provider",
643        ["android.content","android.graphics.drawable"],
644        "android.database",
645        "android.graphics",
646        "android.text",
647        "android.os",
648        "android.util"
649    ]
650
651    def rank(p):
652        for i in range(len(ranking)):
653            if isinstance(ranking[i], list):
654                for j in ranking[i]:
655                    if p.startswith(j): return i
656            else:
657                if p.startswith(ranking[i]): return i
658
659    cr = rank(clazz.pkg.name)
660    if cr is None: return
661
662    for f in clazz.fields:
663        ir = rank(f.typ)
664        if ir and ir < cr:
665            warn(clazz, f, "FW6", "Field type violates package layering")
666
667    for m in clazz.methods:
668        ir = rank(m.typ)
669        if ir and ir < cr:
670            warn(clazz, m, "FW6", "Method return type violates package layering")
671        for arg in m.args:
672            ir = rank(arg)
673            if ir and ir < cr:
674                warn(clazz, m, "FW6", "Method argument type violates package layering")
675
676
677def verify_boolean(clazz):
678    """Verifies that boolean accessors are named correctly.
679    For example, hasFoo() and setHasFoo()."""
680
681    def is_get(m): return len(m.args) == 0 and m.typ == "boolean"
682    def is_set(m): return len(m.args) == 1 and m.args[0] == "boolean"
683
684    gets = [ m for m in clazz.methods if is_get(m) ]
685    sets = [ m for m in clazz.methods if is_set(m) ]
686
687    def error_if_exists(methods, trigger, expected, actual):
688        for m in methods:
689            if m.name == actual:
690                error(clazz, m, "M6", "Symmetric method for %s must be named %s" % (trigger, expected))
691
692    for m in clazz.methods:
693        if is_get(m):
694            if re.match("is[A-Z]", m.name):
695                target = m.name[2:]
696                expected = "setIs" + target
697                error_if_exists(sets, m.name, expected, "setHas" + target)
698            elif re.match("has[A-Z]", m.name):
699                target = m.name[3:]
700                expected = "setHas" + target
701                error_if_exists(sets, m.name, expected, "setIs" + target)
702                error_if_exists(sets, m.name, expected, "set" + target)
703            elif re.match("get[A-Z]", m.name):
704                target = m.name[3:]
705                expected = "set" + target
706                error_if_exists(sets, m.name, expected, "setIs" + target)
707                error_if_exists(sets, m.name, expected, "setHas" + target)
708
709        if is_set(m):
710            if re.match("set[A-Z]", m.name):
711                target = m.name[3:]
712                expected = "get" + target
713                error_if_exists(sets, m.name, expected, "is" + target)
714                error_if_exists(sets, m.name, expected, "has" + target)
715
716
717def verify_collections(clazz):
718    """Verifies that collection types are interfaces."""
719    if clazz.fullname == "android.os.Bundle": return
720
721    bad = ["java.util.Vector", "java.util.LinkedList", "java.util.ArrayList", "java.util.Stack",
722           "java.util.HashMap", "java.util.HashSet", "android.util.ArraySet", "android.util.ArrayMap"]
723    for m in clazz.methods:
724        if m.typ in bad:
725            error(clazz, m, "CL2", "Return type is concrete collection; must be higher-level interface")
726        for arg in m.args:
727            if arg in bad:
728                error(clazz, m, "CL2", "Argument is concrete collection; must be higher-level interface")
729
730
731def verify_flags(clazz):
732    """Verifies that flags are non-overlapping."""
733    known = collections.defaultdict(int)
734    for f in clazz.fields:
735        if "FLAG_" in f.name:
736            try:
737                val = int(f.value)
738            except:
739                continue
740
741            scope = f.name[0:f.name.index("FLAG_")]
742            if val & known[scope]:
743                warn(clazz, f, "C1", "Found overlapping flag constant value")
744            known[scope] |= val
745
746
747def verify_exception(clazz):
748    """Verifies that methods don't throw generic exceptions."""
749    for m in clazz.methods:
750        if "throws java.lang.Exception" in m.raw or "throws java.lang.Throwable" in m.raw or "throws java.lang.Error" in m.raw:
751            error(clazz, m, "S1", "Methods must not throw generic exceptions")
752
753        if "throws android.os.RemoteException" in m.raw:
754            if clazz.name == "android.content.ContentProviderClient": continue
755            if clazz.name == "android.os.Binder": continue
756            if clazz.name == "android.os.IBinder": continue
757
758            error(clazz, m, "FW9", "Methods calling into system server should rethrow RemoteException as RuntimeException")
759
760
761def verify_google(clazz):
762    """Verifies that APIs never reference Google."""
763
764    if re.search("google", clazz.raw, re.IGNORECASE):
765        error(clazz, None, None, "Must never reference Google")
766
767    test = []
768    test.extend(clazz.ctors)
769    test.extend(clazz.fields)
770    test.extend(clazz.methods)
771
772    for t in test:
773        if re.search("google", t.raw, re.IGNORECASE):
774            error(clazz, t, None, "Must never reference Google")
775
776
777def verify_bitset(clazz):
778    """Verifies that we avoid using heavy BitSet."""
779
780    for f in clazz.fields:
781        if f.typ == "java.util.BitSet":
782            error(clazz, f, None, "Field type must not be heavy BitSet")
783
784    for m in clazz.methods:
785        if m.typ == "java.util.BitSet":
786            error(clazz, m, None, "Return type must not be heavy BitSet")
787        for arg in m.args:
788            if arg == "java.util.BitSet":
789                error(clazz, m, None, "Argument type must not be heavy BitSet")
790
791
792def verify_manager(clazz):
793    """Verifies that FooManager is only obtained from Context."""
794
795    if not clazz.name.endswith("Manager"): return
796
797    for c in clazz.ctors:
798        error(clazz, c, None, "Managers must always be obtained from Context; no direct constructors")
799
800    for m in clazz.methods:
801        if m.typ == clazz.fullname:
802            error(clazz, m, None, "Managers must always be obtained from Context")
803
804
805def verify_boxed(clazz):
806    """Verifies that methods avoid boxed primitives."""
807
808    boxed = ["java.lang.Number","java.lang.Byte","java.lang.Double","java.lang.Float","java.lang.Integer","java.lang.Long","java.lang.Short"]
809
810    for c in clazz.ctors:
811        for arg in c.args:
812            if arg in boxed:
813                error(clazz, c, "M11", "Must avoid boxed primitives")
814
815    for f in clazz.fields:
816        if f.typ in boxed:
817            error(clazz, f, "M11", "Must avoid boxed primitives")
818
819    for m in clazz.methods:
820        if m.typ in boxed:
821            error(clazz, m, "M11", "Must avoid boxed primitives")
822        for arg in m.args:
823            if arg in boxed:
824                error(clazz, m, "M11", "Must avoid boxed primitives")
825
826
827def verify_static_utils(clazz):
828    """Verifies that helper classes can't be constructed."""
829    if clazz.fullname.startswith("android.opengl"): return
830    if clazz.fullname.startswith("android.R"): return
831
832    # Only care about classes with default constructors
833    if len(clazz.ctors) == 1 and len(clazz.ctors[0].args) == 0:
834        test = []
835        test.extend(clazz.fields)
836        test.extend(clazz.methods)
837
838        if len(test) == 0: return
839        for t in test:
840            if "static" not in t.split:
841                return
842
843        error(clazz, None, None, "Fully-static utility classes must not have constructor")
844
845
846def verify_overload_args(clazz):
847    """Verifies that method overloads add new arguments at the end."""
848    if clazz.fullname.startswith("android.opengl"): return
849
850    overloads = collections.defaultdict(list)
851    for m in clazz.methods:
852        if "deprecated" in m.split: continue
853        overloads[m.name].append(m)
854
855    for name, methods in overloads.items():
856        if len(methods) <= 1: continue
857
858        # Look for arguments common across all overloads
859        def cluster(args):
860            count = collections.defaultdict(int)
861            res = set()
862            for i in range(len(args)):
863                a = args[i]
864                res.add("%s#%d" % (a, count[a]))
865                count[a] += 1
866            return res
867
868        common_args = cluster(methods[0].args)
869        for m in methods:
870            common_args = common_args & cluster(m.args)
871
872        if len(common_args) == 0: continue
873
874        # Require that all common arguments are present at start of signature
875        locked_sig = None
876        for m in methods:
877            sig = m.args[0:len(common_args)]
878            if not common_args.issubset(cluster(sig)):
879                warn(clazz, m, "M2", "Expected common arguments [%s] at beginning of overloaded method" % (", ".join(common_args)))
880            elif not locked_sig:
881                locked_sig = sig
882            elif locked_sig != sig:
883                error(clazz, m, "M2", "Expected consistent argument ordering between overloads: %s..." % (", ".join(locked_sig)))
884
885
886def verify_callback_handlers(clazz):
887    """Verifies that methods adding listener/callback have overload
888    for specifying delivery thread."""
889
890    # Ignore UI packages which assume main thread
891    skip = [
892        "animation",
893        "view",
894        "graphics",
895        "transition",
896        "widget",
897        "webkit",
898    ]
899    for s in skip:
900        if s in clazz.pkg.name_path: return
901        if s in clazz.extends_path: return
902
903    # Ignore UI classes which assume main thread
904    if "app" in clazz.pkg.name_path or "app" in clazz.extends_path:
905        for s in ["ActionBar","Dialog","Application","Activity","Fragment","Loader"]:
906            if s in clazz.fullname: return
907    if "content" in clazz.pkg.name_path or "content" in clazz.extends_path:
908        for s in ["Loader"]:
909            if s in clazz.fullname: return
910
911    found = {}
912    by_name = collections.defaultdict(list)
913    for m in clazz.methods:
914        if m.name.startswith("unregister"): continue
915        if m.name.startswith("remove"): continue
916        if re.match("on[A-Z]+", m.name): continue
917
918        by_name[m.name].append(m)
919
920        for a in m.args:
921            if a.endswith("Listener") or a.endswith("Callback") or a.endswith("Callbacks"):
922                found[m.name] = m
923
924    for f in found.values():
925        takes_handler = False
926        for m in by_name[f.name]:
927            if "android.os.Handler" in m.args:
928                takes_handler = True
929        if not takes_handler:
930            warn(clazz, f, "L1", "Registration methods should have overload that accepts delivery Handler")
931
932
933def verify_context_first(clazz):
934    """Verifies that methods accepting a Context keep it the first argument."""
935    examine = clazz.ctors + clazz.methods
936    for m in examine:
937        if len(m.args) > 1 and m.args[0] != "android.content.Context":
938            if "android.content.Context" in m.args[1:]:
939                error(clazz, m, "M3", "Context is distinct, so it must be the first argument")
940        if len(m.args) > 1 and m.args[0] != "android.content.ContentResolver":
941            if "android.content.ContentResolver" in m.args[1:]:
942                error(clazz, m, "M3", "ContentResolver is distinct, so it must be the first argument")
943
944
945def verify_listener_last(clazz):
946    """Verifies that methods accepting a Listener or Callback keep them as last arguments."""
947    examine = clazz.ctors + clazz.methods
948    for m in examine:
949        if "Listener" in m.name or "Callback" in m.name: continue
950        found = False
951        for a in m.args:
952            if a.endswith("Callback") or a.endswith("Callbacks") or a.endswith("Listener"):
953                found = True
954            elif found and a != "android.os.Handler":
955                warn(clazz, m, "M3", "Listeners should always be at end of argument list")
956
957
958def verify_resource_names(clazz):
959    """Verifies that resource names have consistent case."""
960    if not re.match("android\.R\.[a-z]+", clazz.fullname): return
961
962    # Resources defined by files are foo_bar_baz
963    if clazz.name in ["anim","animator","color","dimen","drawable","interpolator","layout","transition","menu","mipmap","string","plurals","raw","xml"]:
964        for f in clazz.fields:
965            if re.match("[a-z1-9_]+$", f.name): continue
966            error(clazz, f, None, "Expected resource name in this class to be foo_bar_baz style")
967
968    # Resources defined inside files are fooBarBaz
969    if clazz.name in ["array","attr","id","bool","fraction","integer"]:
970        for f in clazz.fields:
971            if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue
972            if re.match("layout_[a-z][a-zA-Z1-9]*$", f.name): continue
973            if re.match("state_[a-z_]*$", f.name): continue
974
975            if re.match("[a-z][a-zA-Z1-9]*$", f.name): continue
976            error(clazz, f, "C7", "Expected resource name in this class to be fooBarBaz style")
977
978    # Styles are FooBar_Baz
979    if clazz.name in ["style"]:
980        for f in clazz.fields:
981            if re.match("[A-Z][A-Za-z1-9]+(_[A-Z][A-Za-z1-9]+?)*$", f.name): continue
982            error(clazz, f, "C7", "Expected resource name in this class to be FooBar_Baz style")
983
984
985def verify_files(clazz):
986    """Verifies that methods accepting File also accept streams."""
987
988    has_file = set()
989    has_stream = set()
990
991    test = []
992    test.extend(clazz.ctors)
993    test.extend(clazz.methods)
994
995    for m in test:
996        if "java.io.File" in m.args:
997            has_file.add(m)
998        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:
999            has_stream.add(m.name)
1000
1001    for m in has_file:
1002        if m.name not in has_stream:
1003            warn(clazz, m, "M10", "Methods accepting File should also accept FileDescriptor or streams")
1004
1005
1006def verify_manager_list(clazz):
1007    """Verifies that managers return List<? extends Parcelable> instead of arrays."""
1008
1009    if not clazz.name.endswith("Manager"): return
1010
1011    for m in clazz.methods:
1012        if m.typ.startswith("android.") and m.typ.endswith("[]"):
1013            warn(clazz, m, None, "Methods should return List<? extends Parcelable> instead of Parcelable[] to support ParceledListSlice under the hood")
1014
1015
1016def verify_abstract_inner(clazz):
1017    """Verifies that abstract inner classes are static."""
1018
1019    if re.match(".+?\.[A-Z][^\.]+\.[A-Z]", clazz.fullname):
1020        if " abstract " in clazz.raw and " static " not in clazz.raw:
1021            warn(clazz, None, None, "Abstract inner classes should be static to improve testability")
1022
1023
1024def verify_runtime_exceptions(clazz):
1025    """Verifies that runtime exceptions aren't listed in throws."""
1026
1027    banned = [
1028        "java.lang.NullPointerException",
1029        "java.lang.ClassCastException",
1030        "java.lang.IndexOutOfBoundsException",
1031        "java.lang.reflect.UndeclaredThrowableException",
1032        "java.lang.reflect.MalformedParametersException",
1033        "java.lang.reflect.MalformedParameterizedTypeException",
1034        "java.lang.invoke.WrongMethodTypeException",
1035        "java.lang.EnumConstantNotPresentException",
1036        "java.lang.IllegalMonitorStateException",
1037        "java.lang.SecurityException",
1038        "java.lang.UnsupportedOperationException",
1039        "java.lang.annotation.AnnotationTypeMismatchException",
1040        "java.lang.annotation.IncompleteAnnotationException",
1041        "java.lang.TypeNotPresentException",
1042        "java.lang.IllegalStateException",
1043        "java.lang.ArithmeticException",
1044        "java.lang.IllegalArgumentException",
1045        "java.lang.ArrayStoreException",
1046        "java.lang.NegativeArraySizeException",
1047        "java.util.MissingResourceException",
1048        "java.util.EmptyStackException",
1049        "java.util.concurrent.CompletionException",
1050        "java.util.concurrent.RejectedExecutionException",
1051        "java.util.IllformedLocaleException",
1052        "java.util.ConcurrentModificationException",
1053        "java.util.NoSuchElementException",
1054        "java.io.UncheckedIOException",
1055        "java.time.DateTimeException",
1056        "java.security.ProviderException",
1057        "java.nio.BufferUnderflowException",
1058        "java.nio.BufferOverflowException",
1059    ]
1060
1061    test = []
1062    test.extend(clazz.ctors)
1063    test.extend(clazz.methods)
1064
1065    for t in test:
1066        if " throws " not in t.raw: continue
1067        throws = t.raw[t.raw.index(" throws "):]
1068        for b in banned:
1069            if b in throws:
1070                error(clazz, t, None, "Methods must not mention RuntimeException subclasses in throws clauses")
1071
1072
1073def verify_error(clazz):
1074    """Verifies that we always use Exception instead of Error."""
1075    if not clazz.extends: return
1076    if clazz.extends.endswith("Error"):
1077        error(clazz, None, None, "Trouble must be reported through an Exception, not Error")
1078    if clazz.extends.endswith("Exception") and not clazz.name.endswith("Exception"):
1079        error(clazz, None, None, "Exceptions must be named FooException")
1080
1081
1082def verify_units(clazz):
1083    """Verifies that we use consistent naming for units."""
1084
1085    # If we find K, recommend replacing with V
1086    bad = {
1087        "Ns": "Nanos",
1088        "Ms": "Millis or Micros",
1089        "Sec": "Seconds", "Secs": "Seconds",
1090        "Hr": "Hours", "Hrs": "Hours",
1091        "Mo": "Months", "Mos": "Months",
1092        "Yr": "Years", "Yrs": "Years",
1093        "Byte": "Bytes", "Space": "Bytes",
1094    }
1095
1096    for m in clazz.methods:
1097        if m.typ not in ["short","int","long"]: continue
1098        for k, v in bad.iteritems():
1099            if m.name.endswith(k):
1100                error(clazz, m, None, "Expected method name units to be " + v)
1101        if m.name.endswith("Nanos") or m.name.endswith("Micros"):
1102            warn(clazz, m, None, "Returned time values are strongly encouraged to be in milliseconds unless you need the extra precision")
1103        if m.name.endswith("Seconds"):
1104            error(clazz, m, None, "Returned time values must be in milliseconds")
1105
1106    for m in clazz.methods:
1107        typ = m.typ
1108        if typ == "void":
1109            if len(m.args) != 1: continue
1110            typ = m.args[0]
1111
1112        if m.name.endswith("Fraction") and typ != "float":
1113            error(clazz, m, None, "Fractions must use floats")
1114        if m.name.endswith("Percentage") and typ != "int":
1115            error(clazz, m, None, "Percentage must use ints")
1116
1117
1118def verify_closable(clazz):
1119    """Verifies that classes are AutoClosable."""
1120    if "implements java.lang.AutoCloseable" in clazz.raw: return
1121    if "implements java.io.Closeable" in clazz.raw: return
1122
1123    for m in clazz.methods:
1124        if len(m.args) > 0: continue
1125        if m.name in ["close","release","destroy","finish","finalize","disconnect","shutdown","stop","free","quit"]:
1126            warn(clazz, m, None, "Classes that release resources should implement AutoClosable and CloseGuard")
1127            return
1128
1129
1130def examine_clazz(clazz):
1131    """Find all style issues in the given class."""
1132    if clazz.pkg.name.startswith("java"): return
1133    if clazz.pkg.name.startswith("junit"): return
1134    if clazz.pkg.name.startswith("org.apache"): return
1135    if clazz.pkg.name.startswith("org.xml"): return
1136    if clazz.pkg.name.startswith("org.json"): return
1137    if clazz.pkg.name.startswith("org.w3c"): return
1138    if clazz.pkg.name.startswith("android.icu."): return
1139
1140    verify_constants(clazz)
1141    verify_enums(clazz)
1142    verify_class_names(clazz)
1143    verify_method_names(clazz)
1144    verify_callbacks(clazz)
1145    verify_listeners(clazz)
1146    verify_actions(clazz)
1147    verify_extras(clazz)
1148    verify_equals(clazz)
1149    verify_parcelable(clazz)
1150    verify_protected(clazz)
1151    verify_fields(clazz)
1152    verify_register(clazz)
1153    verify_sync(clazz)
1154    verify_intent_builder(clazz)
1155    verify_helper_classes(clazz)
1156    verify_builder(clazz)
1157    verify_aidl(clazz)
1158    verify_internal(clazz)
1159    verify_layering(clazz)
1160    verify_boolean(clazz)
1161    verify_collections(clazz)
1162    verify_flags(clazz)
1163    verify_exception(clazz)
1164    if not ALLOW_GOOGLE: verify_google(clazz)
1165    verify_bitset(clazz)
1166    verify_manager(clazz)
1167    verify_boxed(clazz)
1168    verify_static_utils(clazz)
1169    verify_overload_args(clazz)
1170    verify_callback_handlers(clazz)
1171    verify_context_first(clazz)
1172    verify_listener_last(clazz)
1173    verify_resource_names(clazz)
1174    verify_files(clazz)
1175    verify_manager_list(clazz)
1176    verify_abstract_inner(clazz)
1177    verify_runtime_exceptions(clazz)
1178    verify_error(clazz)
1179    verify_units(clazz)
1180    verify_closable(clazz)
1181
1182
1183def examine_stream(stream):
1184    """Find all style issues in the given API stream."""
1185    global failures
1186    failures = {}
1187    _parse_stream(stream, examine_clazz)
1188    return failures
1189
1190
1191def examine_api(api):
1192    """Find all style issues in the given parsed API."""
1193    global failures
1194    failures = {}
1195    for key in sorted(api.keys()):
1196        examine_clazz(api[key])
1197    return failures
1198
1199
1200def verify_compat(cur, prev):
1201    """Find any incompatible API changes between two levels."""
1202    global failures
1203
1204    def class_exists(api, test):
1205        return test.fullname in api
1206
1207    def ctor_exists(api, clazz, test):
1208        for m in clazz.ctors:
1209            if m.ident == test.ident: return True
1210        return False
1211
1212    def all_methods(api, clazz):
1213        methods = list(clazz.methods)
1214        if clazz.extends is not None:
1215            methods.extend(all_methods(api, api[clazz.extends]))
1216        return methods
1217
1218    def method_exists(api, clazz, test):
1219        methods = all_methods(api, clazz)
1220        for m in methods:
1221            if m.ident == test.ident: return True
1222        return False
1223
1224    def field_exists(api, clazz, test):
1225        for f in clazz.fields:
1226            if f.ident == test.ident: return True
1227        return False
1228
1229    failures = {}
1230    for key in sorted(prev.keys()):
1231        prev_clazz = prev[key]
1232
1233        if not class_exists(cur, prev_clazz):
1234            error(prev_clazz, None, None, "Class removed or incompatible change")
1235            continue
1236
1237        cur_clazz = cur[key]
1238
1239        for test in prev_clazz.ctors:
1240            if not ctor_exists(cur, cur_clazz, test):
1241                error(prev_clazz, prev_ctor, None, "Constructor removed or incompatible change")
1242
1243        methods = all_methods(prev, prev_clazz)
1244        for test in methods:
1245            if not method_exists(cur, cur_clazz, test):
1246                error(prev_clazz, test, None, "Method removed or incompatible change")
1247
1248        for test in prev_clazz.fields:
1249            if not field_exists(cur, cur_clazz, test):
1250                error(prev_clazz, test, None, "Field removed or incompatible change")
1251
1252    return failures
1253
1254
1255if __name__ == "__main__":
1256    parser = argparse.ArgumentParser(description="Enforces common Android public API design \
1257            patterns. It ignores lint messages from a previous API level, if provided.")
1258    parser.add_argument("current.txt", type=argparse.FileType('r'), help="current.txt")
1259    parser.add_argument("previous.txt", nargs='?', type=argparse.FileType('r'), default=None,
1260            help="previous.txt")
1261    parser.add_argument("--no-color", action='store_const', const=True,
1262            help="Disable terminal colors")
1263    parser.add_argument("--allow-google", action='store_const', const=True,
1264            help="Allow references to Google")
1265    args = vars(parser.parse_args())
1266
1267    if args['no_color']:
1268        USE_COLOR = False
1269
1270    if args['allow_google']:
1271        ALLOW_GOOGLE = True
1272
1273    current_file = args['current.txt']
1274    previous_file = args['previous.txt']
1275
1276    with current_file as f:
1277        cur_fail = examine_stream(f)
1278    if not previous_file is None:
1279        with previous_file as f:
1280            prev_fail = examine_stream(f)
1281
1282        # ignore errors from previous API level
1283        for p in prev_fail:
1284            if p in cur_fail:
1285                del cur_fail[p]
1286
1287        """
1288        # NOTE: disabled because of memory pressure
1289        # look for compatibility issues
1290        compat_fail = verify_compat(cur, prev)
1291
1292        print "%s API compatibility issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
1293        for f in sorted(compat_fail):
1294            print compat_fail[f]
1295            print
1296        """
1297
1298    print "%s API style issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
1299    for f in sorted(cur_fail):
1300        print cur_fail[f]
1301        print
1302