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