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, itertools
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, sig_format = 1):
55        self.clazz = clazz
56        self.line = line
57        self.raw = raw.strip(" {;")
58        self.blame = blame
59
60        if sig_format == 2:
61            V2LineParser(raw).parse_into_field(self)
62        elif sig_format == 1:
63            # drop generics for now; may need multiple passes
64            raw = re.sub("<[^<]+?>", "", raw)
65            raw = re.sub("<[^<]+?>", "", raw)
66
67            raw = raw.split()
68            self.split = list(raw)
69
70            for r in ["field", "volatile", "transient", "public", "protected", "static", "final", "deprecated"]:
71                while r in raw: raw.remove(r)
72
73            # ignore annotations for now
74            raw = [ r for r in raw if not r.startswith("@") ]
75
76            self.typ = raw[0]
77            self.name = raw[1].strip(";")
78            if len(raw) >= 4 and raw[2] == "=":
79                self.value = raw[3].strip(';"')
80            else:
81                self.value = None
82            self.annotations = []
83
84        self.ident = "-".join((self.typ, self.name, self.value or ""))
85
86    def __hash__(self):
87        return hash(self.raw)
88
89    def __repr__(self):
90        return self.raw
91
92
93class Argument(object):
94
95    __slots__ = ["type", "annotations", "name", "default"]
96
97    def __init__(self, type):
98        self.type = type
99        self.annotations = []
100        self.name = None
101        self.default = None
102
103
104class Method():
105    def __init__(self, clazz, line, raw, blame, sig_format = 1):
106        self.clazz = clazz
107        self.line = line
108        self.raw = raw.strip(" {;")
109        self.blame = blame
110
111        if sig_format == 2:
112            V2LineParser(raw).parse_into_method(self)
113        elif sig_format == 1:
114            # drop generics for now; may need multiple passes
115            raw = re.sub("<[^<]+?>", "", raw)
116            raw = re.sub("<[^<]+?>", "", raw)
117
118            # handle each clause differently
119            raw_prefix, raw_args, _, raw_throws = re.match(r"(.*?)\((.*?)\)( throws )?(.*?);$", raw).groups()
120
121            # parse prefixes
122            raw = re.split("[\s]+", raw_prefix)
123            for r in ["", ";"]:
124                while r in raw: raw.remove(r)
125            self.split = list(raw)
126
127            for r in ["method", "public", "protected", "static", "final", "deprecated", "abstract", "default", "operator", "synchronized"]:
128                while r in raw: raw.remove(r)
129
130            self.typ = raw[0]
131            self.name = raw[1]
132
133            # parse args
134            self.detailed_args = []
135            for arg in re.split(",\s*", raw_args):
136                arg = re.split("\s", arg)
137                # ignore annotations for now
138                arg = [ a for a in arg if not a.startswith("@") ]
139                if len(arg[0]) > 0:
140                    self.detailed_args.append(Argument(arg[0]))
141
142            # parse throws
143            self.throws = []
144            for throw in re.split(",\s*", raw_throws):
145                self.throws.append(throw)
146
147            self.annotations = []
148        else:
149            raise ValueError("Unknown signature format: " + sig_format)
150
151        self.args = map(lambda a: a.type, self.detailed_args)
152        self.ident = "-".join((self.typ, self.name, "-".join(self.args)))
153
154    def sig_matches(self, typ, name, args):
155        return typ == self.typ and name == self.name and args == self.args
156
157    def __hash__(self):
158        return hash(self.raw)
159
160    def __repr__(self):
161        return self.raw
162
163
164class Class():
165    def __init__(self, pkg, line, raw, blame, sig_format = 1):
166        self.pkg = pkg
167        self.line = line
168        self.raw = raw.strip(" {;")
169        self.blame = blame
170        self.ctors = []
171        self.fields = []
172        self.methods = []
173        self.annotations = []
174
175        if sig_format == 2:
176            V2LineParser(raw).parse_into_class(self)
177        elif sig_format == 1:
178            # drop generics for now; may need multiple passes
179            raw = re.sub("<[^<]+?>", "", raw)
180            raw = re.sub("<[^<]+?>", "", raw)
181
182            raw = raw.split()
183            self.split = list(raw)
184            if "class" in raw:
185                self.fullname = raw[raw.index("class")+1]
186            elif "interface" in raw:
187                self.fullname = raw[raw.index("interface")+1]
188            elif "@interface" in raw:
189                self.fullname = raw[raw.index("@interface")+1]
190            else:
191                raise ValueError("Funky class type %s" % (self.raw))
192
193            if "extends" in raw:
194                self.extends = raw[raw.index("extends")+1]
195            else:
196                self.extends = None
197
198            if "implements" in raw:
199                self.implements = raw[raw.index("implements")+1]
200                self.implements_all = [self.implements]
201            else:
202                self.implements = None
203                self.implements_all = []
204        else:
205            raise ValueError("Unknown signature format: " + sig_format)
206
207        self.fullname = self.pkg.name + "." + self.fullname
208        self.fullname_path = self.fullname.split(".")
209
210        if self.extends is not None:
211            self.extends_path = self.extends.split(".")
212        else:
213            self.extends_path = []
214
215        self.name = self.fullname[self.fullname.rindex(".")+1:]
216
217    def merge_from(self, other):
218        self.ctors.extend(other.ctors)
219        self.fields.extend(other.fields)
220        self.methods.extend(other.methods)
221
222    def __hash__(self):
223        return hash((self.raw, tuple(self.ctors), tuple(self.fields), tuple(self.methods)))
224
225    def __repr__(self):
226        return self.raw
227
228
229class Package():
230    NAME = re.compile("package(?: .*)? ([A-Za-z0-9.]+)")
231
232    def __init__(self, line, raw, blame):
233        self.line = line
234        self.raw = raw.strip(" {;")
235        self.blame = blame
236
237        self.name = Package.NAME.match(raw).group(1)
238        self.name_path = self.name.split(".")
239
240    def __repr__(self):
241        return self.raw
242
243class V2Tokenizer(object):
244    __slots__ = ["raw"]
245
246    SIGNATURE_PREFIX = "// Signature format: "
247    DELIMITER = re.compile(r'\s+|[()@<>;,={}/"!?]|\[\]|\.\.\.')
248    STRING_SPECIAL = re.compile(r'["\\]')
249
250    def __init__(self, raw):
251        self.raw = raw
252
253    def tokenize(self):
254        tokens = []
255        current = 0
256        raw = self.raw
257        length = len(raw)
258
259        while current < length:
260            while current < length:
261                start = current
262                match = V2Tokenizer.DELIMITER.search(raw, start)
263                if match is not None:
264                    match_start = match.start()
265                    if match_start == current:
266                        end = match.end()
267                    else:
268                        end = match_start
269                else:
270                    end = length
271
272                token = raw[start:end]
273                current = end
274
275                if token == "" or token[0] == " ":
276                    continue
277                else:
278                    break
279
280            if token == "@":
281                if raw[start:start+11] == "@interface ":
282                    current = start + 11
283                    tokens.append("@interface")
284                    continue
285            elif token == '/':
286                if raw[start:start+2] == "//":
287                    current = length
288                    continue
289            elif token == '"':
290                current, string_token = self.tokenize_string(raw, length, current)
291                tokens.append(token + string_token)
292                continue
293
294            tokens.append(token)
295
296        return tokens
297
298    def tokenize_string(self, raw, length, current):
299        start = current
300        end = length
301        while start < end:
302            match = V2Tokenizer.STRING_SPECIAL.search(raw, start)
303            if match:
304                if match.group() == '"':
305                    end = match.end()
306                    break
307                elif match.group() == '\\':
308                    # ignore whatever is after the slash
309                    start += 2
310                else:
311                    raise ValueError("Unexpected match: `%s`" % (match.group()))
312            else:
313                raise ValueError("Unexpected EOF tokenizing string: `%s`" % (raw[current - 1:],))
314
315        token = raw[current:end]
316        return end, token
317
318class V2LineParser(object):
319    __slots__ = ["tokenized", "current", "len"]
320
321    FIELD_KINDS = ("field", "property", "enum_constant")
322    MODIFIERS = set("public protected internal private abstract default static final transient volatile synchronized native operator sealed strictfp infix inline suspend vararg".split())
323    JAVA_LANG_TYPES = set("AbstractMethodError AbstractStringBuilder Appendable ArithmeticException ArrayIndexOutOfBoundsException ArrayStoreException AssertionError AutoCloseable Boolean BootstrapMethodError Byte Character CharSequence Class ClassCastException ClassCircularityError ClassFormatError ClassLoader ClassNotFoundException Cloneable CloneNotSupportedException Comparable Compiler Deprecated Double Enum EnumConstantNotPresentException Error Exception ExceptionInInitializerError Float FunctionalInterface IllegalAccessError IllegalAccessException IllegalArgumentException IllegalMonitorStateException IllegalStateException IllegalThreadStateException IncompatibleClassChangeError IndexOutOfBoundsException InheritableThreadLocal InstantiationError InstantiationException Integer InternalError InterruptedException Iterable LinkageError Long Math NegativeArraySizeException NoClassDefFoundError NoSuchFieldError NoSuchFieldException NoSuchMethodError NoSuchMethodException NullPointerException Number NumberFormatException Object OutOfMemoryError Override Package package-info.java Process ProcessBuilder ProcessEnvironment ProcessImpl Readable ReflectiveOperationException Runnable Runtime RuntimeException RuntimePermission SafeVarargs SecurityException SecurityManager Short StackOverflowError StackTraceElement StrictMath String StringBuffer StringBuilder StringIndexOutOfBoundsException SuppressWarnings System Thread ThreadDeath ThreadGroup ThreadLocal Throwable TypeNotPresentException UNIXProcess UnknownError UnsatisfiedLinkError UnsupportedClassVersionError UnsupportedOperationException VerifyError VirtualMachineError Void".split())
324
325    def __init__(self, raw):
326        self.tokenized = V2Tokenizer(raw).tokenize()
327        self.current = 0
328        self.len = len(self.tokenized)
329
330    def parse_into_method(self, method):
331        method.split = []
332        kind = self.parse_one_of("ctor", "method")
333        method.split.append(kind)
334        method.annotations = self.parse_annotations()
335        method.split.extend(self.parse_modifiers())
336        self.parse_matching_paren("<", ">")
337        if "@Deprecated" in method.annotations:
338            method.split.append("deprecated")
339        if kind == "ctor":
340            method.typ = "ctor"
341        else:
342            method.typ = self.parse_type()
343            method.split.append(method.typ)
344        method.name = self.parse_name()
345        method.split.append(method.name)
346        self.parse_token("(")
347        method.detailed_args = self.parse_args()
348        self.parse_token(")")
349        method.throws = self.parse_throws()
350        if "@interface" in method.clazz.split:
351            self.parse_annotation_default()
352        self.parse_token(";")
353        self.parse_eof()
354
355    def parse_into_class(self, clazz):
356        clazz.split = []
357        clazz.annotations = self.parse_annotations()
358        if "@Deprecated" in clazz.annotations:
359            clazz.split.append("deprecated")
360        clazz.split.extend(self.parse_modifiers())
361        kind = self.parse_one_of("class", "interface", "@interface", "enum")
362        if kind == "enum":
363            # enums are implicitly final
364            clazz.split.append("final")
365        clazz.split.append(kind)
366        clazz.fullname = self.parse_name()
367        self.parse_matching_paren("<", ">")
368        extends = self.parse_extends()
369        clazz.extends = extends[0] if extends else None
370        clazz.implements_all = self.parse_implements()
371        # The checks assume that interfaces are always found in implements, which isn't true for
372        # subinterfaces.
373        if not clazz.implements_all and "interface" in clazz.split:
374            clazz.implements_all = [clazz.extends]
375        clazz.implements = clazz.implements_all[0] if clazz.implements_all else None
376        self.parse_token("{")
377        self.parse_eof()
378
379    def parse_into_field(self, field):
380        kind = self.parse_one_of(*V2LineParser.FIELD_KINDS)
381        field.split = [kind]
382        field.annotations = self.parse_annotations()
383        if "@Deprecated" in field.annotations:
384            field.split.append("deprecated")
385        field.split.extend(self.parse_modifiers())
386        field.typ = self.parse_type()
387        field.split.append(field.typ)
388        field.name = self.parse_name()
389        field.split.append(field.name)
390        if self.parse_if("="):
391            field.value = self.parse_value_stripped()
392        else:
393            field.value = None
394
395        self.parse_token(";")
396        self.parse_eof()
397
398    def lookahead(self):
399        return self.tokenized[self.current]
400
401    def parse_one_of(self, *options):
402        found = self.lookahead()
403        if found not in options:
404            raise ValueError("Parsing failed, expected one of `%s` but found `%s` in %s" % (options, found, repr(self.tokenized)))
405        return self.parse_token()
406
407    def parse_token(self, tok = None):
408        found = self.lookahead()
409        if tok is not None and found != tok:
410            raise ValueError("Parsing failed, expected `%s` but found `%s` in %s" % (tok, found, repr(self.tokenized)))
411        self.current += 1
412        return found
413
414    def eof(self):
415        return self.current == self.len
416
417    def parse_eof(self):
418        if not self.eof():
419            raise ValueError("Parsing failed, expected EOF, but %s has not been parsed in %s" % (self.tokenized[self.current:], self.tokenized))
420
421    def parse_if(self, tok):
422        if not self.eof() and self.lookahead() == tok:
423            self.parse_token()
424            return True
425        return False
426
427    def parse_annotations(self):
428        ret = []
429        while self.lookahead() == "@":
430            ret.append(self.parse_annotation())
431        return ret
432
433    def parse_annotation(self):
434        ret = self.parse_token("@") + self.parse_token()
435        self.parse_matching_paren("(", ")")
436        return ret
437
438    def parse_matching_paren(self, open, close):
439        start = self.current
440        if not self.parse_if(open):
441            return
442        length = len(self.tokenized)
443        count = 1
444        while count > 0:
445            if self.current == length:
446                raise ValueError("Unexpected EOF looking for closing paren: `%s`" % (self.tokenized[start:],))
447            t = self.parse_token()
448            if t == open:
449                count += 1
450            elif t == close:
451                count -= 1
452        return self.tokenized[start:self.current]
453
454    def parse_modifiers(self):
455        ret = []
456        while self.lookahead() in V2LineParser.MODIFIERS:
457            ret.append(self.parse_token())
458        return ret
459
460    def parse_kotlin_nullability(self):
461        t = self.lookahead()
462        if t == "?" or t == "!":
463            return self.parse_token()
464        return None
465
466    def parse_type(self):
467        self.parse_annotations()
468        type = self.parse_token()
469        if type[-1] == '.':
470            self.parse_annotations()
471            type += self.parse_token()
472        if type in V2LineParser.JAVA_LANG_TYPES:
473            type = "java.lang." + type
474        self.parse_matching_paren("<", ">")
475        while True:
476            t = self.lookahead()
477            if t == "@":
478                self.parse_annotation()
479            elif t == "[]":
480                type += self.parse_token()
481            elif self.parse_kotlin_nullability() is not None:
482                pass  # discard nullability for now
483            else:
484                break
485        return type
486
487    def parse_arg_type(self):
488        type = self.parse_type()
489        if self.parse_if("..."):
490            type += "..."
491        self.parse_kotlin_nullability() # discard nullability for now
492        return type
493
494    def parse_name(self):
495        return self.parse_token()
496
497    def parse_args(self):
498        args = []
499        if self.lookahead() == ")":
500            return args
501
502        while True:
503            args.append(self.parse_arg())
504            if self.lookahead() == ")":
505                return args
506            self.parse_token(",")
507
508    def parse_arg(self):
509        self.parse_if("vararg")  # kotlin vararg
510        annotations = self.parse_annotations()
511        arg = Argument(self.parse_arg_type())
512        arg.annotations = annotations
513        l = self.lookahead()
514        if l != "," and l != ")":
515            if self.lookahead() != '=':
516                arg.name = self.parse_token()  # kotlin argument name
517            if self.parse_if('='): # kotlin default value
518                arg.default = self.parse_expression()
519        return arg
520
521    def parse_expression(self):
522        while not self.lookahead() in [')', ',', ';']:
523            (self.parse_matching_paren('(', ')') or
524            self.parse_matching_paren('{', '}') or
525            self.parse_token())
526
527    def parse_throws(self):
528        ret = []
529        if self.parse_if("throws"):
530            ret.append(self.parse_type())
531            while self.parse_if(","):
532                ret.append(self.parse_type())
533        return ret
534
535    def parse_extends(self):
536        if self.parse_if("extends"):
537            return self.parse_space_delimited_type_list()
538        return []
539
540    def parse_implements(self):
541        if self.parse_if("implements"):
542            return self.parse_space_delimited_type_list()
543        return []
544
545    def parse_space_delimited_type_list(self, terminals = ["implements", "{"]):
546        types = []
547        while True:
548            types.append(self.parse_type())
549            if self.lookahead() in terminals:
550                return types
551
552    def parse_annotation_default(self):
553        if self.parse_if("default"):
554            self.parse_expression()
555
556    def parse_value(self):
557        if self.lookahead() == "{":
558            return " ".join(self.parse_matching_paren("{", "}"))
559        elif self.lookahead() == "(":
560            return " ".join(self.parse_matching_paren("(", ")"))
561        else:
562            return self.parse_token()
563
564    def parse_value_stripped(self):
565        value = self.parse_value()
566        if value[0] == '"':
567            return value[1:-1]
568        return value
569
570
571def _parse_stream(f, clazz_cb=None, base_f=None, out_classes_with_base=None,
572                  in_classes_with_base=[]):
573    api = {}
574    in_classes_with_base = _retry_iterator(in_classes_with_base)
575
576    if base_f:
577        base_classes = _retry_iterator(_parse_stream_to_generator(base_f))
578    else:
579        base_classes = []
580
581    def handle_class(clazz):
582        if clazz_cb:
583            clazz_cb(clazz)
584        else: # In callback mode, don't keep track of the full API
585            api[clazz.fullname] = clazz
586
587    def handle_missed_classes_with_base(clazz):
588        for c in _yield_until_matching_class(in_classes_with_base, clazz):
589            base_class = _skip_to_matching_class(base_classes, c)
590            if base_class:
591                handle_class(base_class)
592
593    for clazz in _parse_stream_to_generator(f):
594        # Before looking at clazz, let's see if there's some classes that were not present, but
595        # may have an entry in the base stream.
596        handle_missed_classes_with_base(clazz)
597
598        base_class = _skip_to_matching_class(base_classes, clazz)
599        if base_class:
600            clazz.merge_from(base_class)
601            if out_classes_with_base is not None:
602                out_classes_with_base.append(clazz)
603        handle_class(clazz)
604
605    handle_missed_classes_with_base(None)
606
607    return api
608
609def _parse_stream_to_generator(f):
610    line = 0
611    pkg = None
612    clazz = None
613    blame = None
614    sig_format = 1
615
616    re_blame = re.compile(r"^(\^?[a-z0-9]{7,}) \(<([^>]+)>.+?\) (.+?)$")
617
618    field_prefixes = map(lambda kind: "    %s" % (kind,), V2LineParser.FIELD_KINDS)
619    def startsWithFieldPrefix(raw):
620        for prefix in field_prefixes:
621            if raw.startswith(prefix):
622                return True
623        return False
624
625    for raw in f:
626        line += 1
627        raw = raw.rstrip()
628        match = re_blame.match(raw)
629        if match is not None:
630            blame = match.groups()[0:2]
631            if blame[0].startswith("^"):  # Outside of blame range
632              blame = None
633            raw = match.groups()[2]
634        else:
635            blame = None
636
637        if line == 1 and V2Tokenizer.SIGNATURE_PREFIX in raw:
638            sig_format_string = raw[len(V2Tokenizer.SIGNATURE_PREFIX):]
639            if sig_format_string in ["2.0", "3.0"]:
640                sig_format = 2
641            else:
642                raise ValueError("Unknown format: %s" % (sig_format_string,))
643        elif raw.startswith("package"):
644            pkg = Package(line, raw, blame)
645        elif raw.startswith("  ") and raw.endswith("{"):
646            clazz = Class(pkg, line, raw, blame, sig_format=sig_format)
647        elif raw.startswith("    ctor"):
648            clazz.ctors.append(Method(clazz, line, raw, blame, sig_format=sig_format))
649        elif raw.startswith("    method"):
650            clazz.methods.append(Method(clazz, line, raw, blame, sig_format=sig_format))
651        elif startsWithFieldPrefix(raw):
652            clazz.fields.append(Field(clazz, line, raw, blame, sig_format=sig_format))
653        elif raw.startswith("  }") and clazz:
654            yield clazz
655
656def _retry_iterator(it):
657    """Wraps an iterator, such that calling send(True) on it will redeliver the same element"""
658    for e in it:
659        while True:
660            retry = yield e
661            if not retry:
662                break
663            # send() was called, asking us to redeliver clazz on next(). Still need to yield
664            # a dummy value to the send() first though.
665            if (yield "Returning clazz on next()"):
666                raise TypeError("send() must be followed by next(), not send()")
667
668def _skip_to_matching_class(classes, needle):
669    """Takes a classes iterator and consumes entries until it returns the class we're looking for
670
671    This relies on classes being sorted by package and class name."""
672
673    for clazz in classes:
674        if clazz.pkg.name < needle.pkg.name:
675            # We haven't reached the right package yet
676            continue
677        if clazz.pkg.name == needle.pkg.name and clazz.fullname < needle.fullname:
678            # We're in the right package, but not the right class yet
679            continue
680        if clazz.fullname == needle.fullname:
681            return clazz
682        # We ran past the right class. Send it back into the generator, then report failure.
683        classes.send(clazz)
684        return None
685
686def _yield_until_matching_class(classes, needle):
687    """Takes a class iterator and yields entries it until it reaches the class we're looking for.
688
689    This relies on classes being sorted by package and class name."""
690
691    for clazz in classes:
692        if needle is None:
693            yield clazz
694        elif clazz.pkg.name < needle.pkg.name:
695            # We haven't reached the right package yet
696            yield clazz
697        elif clazz.pkg.name == needle.pkg.name and clazz.fullname < needle.fullname:
698            # We're in the right package, but not the right class yet
699            yield clazz
700        elif clazz.fullname == needle.fullname:
701            # Class found, abort.
702            return
703        else:
704            # We ran past the right class. Send it back into the iterator, then abort.
705            classes.send(clazz)
706            return
707
708class Failure():
709    def __init__(self, sig, clazz, detail, error, rule, msg):
710        self.clazz = clazz
711        self.sig = sig
712        self.error = error
713        self.rule = rule
714        self.msg = msg
715
716        if error:
717            self.head = "Error %s" % (rule) if rule else "Error"
718            dump = "%s%s:%s %s" % (format(fg=RED, bg=BLACK, bold=True), self.head, format(reset=True), msg)
719        else:
720            self.head = "Warning %s" % (rule) if rule else "Warning"
721            dump = "%s%s:%s %s" % (format(fg=YELLOW, bg=BLACK, bold=True), self.head, format(reset=True), msg)
722
723        self.line = clazz.line
724        blame = clazz.blame
725        if detail is not None:
726            dump += "\n    in " + repr(detail)
727            self.line = detail.line
728            blame = detail.blame
729        dump += "\n    in " + repr(clazz)
730        dump += "\n    in " + repr(clazz.pkg)
731        dump += "\n    at line " + repr(self.line)
732        if blame is not None:
733            dump += "\n    last modified by %s in %s" % (blame[1], blame[0])
734
735        self.dump = dump
736
737    def __repr__(self):
738        return self.dump
739
740
741failures = {}
742
743def _fail(clazz, detail, error, rule, msg):
744    """Records an API failure to be processed later."""
745    global failures
746
747    sig = "%s-%s-%s" % (clazz.fullname, detail.ident if detail else None, msg)
748    sig = sig.replace(" deprecated ", " ")
749
750    failures[sig] = Failure(sig, clazz, detail, error, rule, msg)
751
752
753def warn(clazz, detail, rule, msg):
754    _fail(clazz, detail, False, rule, msg)
755
756def error(clazz, detail, rule, msg):
757    _fail(clazz, detail, True, rule, msg)
758
759
760noticed = {}
761
762def notice(clazz):
763    global noticed
764
765    noticed[clazz.fullname] = hash(clazz)
766
767
768verifiers = {}
769
770def verifier(f):
771    verifiers[f.__name__] = f
772    return f
773
774
775@verifier
776def verify_constants(clazz):
777    """All static final constants must be FOO_NAME style."""
778    if re.match("android\.R\.[a-z]+", clazz.fullname): return
779    if clazz.fullname.startswith("android.os.Build"): return
780    if clazz.fullname == "android.system.OsConstants": return
781
782    req = ["java.lang.String","byte","short","int","long","float","double","boolean","char"]
783    for f in clazz.fields:
784        if "static" in f.split and "final" in f.split:
785            if re.match("[A-Z0-9_]+", f.name) is None:
786                error(clazz, f, "C2", "Constant field names must be FOO_NAME")
787            if f.typ != "java.lang.String":
788                if f.name.startswith("MIN_") or f.name.startswith("MAX_"):
789                    warn(clazz, f, "C8", "If min/max could change in future, make them dynamic methods")
790            if f.typ in req and f.value is None:
791                error(clazz, f, None, "All constants must be defined at compile time")
792
793@verifier
794def verify_enums(clazz):
795    """Enums are bad, mmkay?"""
796    if clazz.extends == "java.lang.Enum" or "enum" in clazz.split:
797        error(clazz, None, "F5", "Enums are not allowed")
798
799@verifier
800def verify_class_names(clazz):
801    """Try catching malformed class names like myMtp or MTPUser."""
802    if clazz.fullname.startswith("android.opengl"): return
803    if clazz.fullname.startswith("android.renderscript"): return
804    if re.match("android\.R\.[a-z]+", clazz.fullname): return
805
806    if re.search("[A-Z]{2,}", clazz.name) is not None:
807        warn(clazz, None, "S1", "Class names with acronyms should be Mtp not MTP")
808    if re.match("[^A-Z]", clazz.name):
809        error(clazz, None, "S1", "Class must start with uppercase char")
810    if clazz.name.endswith("Impl"):
811        error(clazz, None, None, "Don't expose your implementation details")
812
813
814@verifier
815def verify_method_names(clazz):
816    """Try catching malformed method names, like Foo() or getMTU()."""
817    if clazz.fullname.startswith("android.opengl"): return
818    if clazz.fullname.startswith("android.renderscript"): return
819    if clazz.fullname == "android.system.OsConstants": return
820
821    for m in clazz.methods:
822        if re.search("[A-Z]{2,}", m.name) is not None:
823            warn(clazz, m, "S1", "Method names with acronyms should be getMtu() instead of getMTU()")
824        if re.match("[^a-z]", m.name):
825            error(clazz, m, "S1", "Method name must start with lowercase char")
826
827
828@verifier
829def verify_callbacks(clazz):
830    """Verify Callback classes.
831    All methods must follow onFoo() naming style."""
832    if clazz.fullname == "android.speech.tts.SynthesisCallback": return
833
834    if clazz.name.endswith("Callbacks"):
835        error(clazz, None, "L1", "Callback class names should be singular")
836    if clazz.name.endswith("Observer"):
837        warn(clazz, None, "L1", "Class should be named FooCallback")
838
839    if clazz.name.endswith("Callback"):
840        for m in clazz.methods:
841            if not re.match("on[A-Z][a-z]*", m.name):
842                error(clazz, m, "L1", "Callback method names must be onFoo() style")
843
844
845@verifier
846def verify_listeners(clazz):
847    """Verify Listener classes.
848    All Listener classes must be interface.
849    All methods must follow onFoo() naming style.
850    If only a single method, it must match class name:
851        interface OnFooListener { void onFoo() }"""
852
853    if clazz.name.endswith("Listener"):
854        if "abstract" in clazz.split and "class" in clazz.split:
855            error(clazz, None, "L1", "Listeners should be an interface, or otherwise renamed Callback")
856
857        for m in clazz.methods:
858            if not re.match("on[A-Z][a-z]*", m.name):
859                error(clazz, m, "L1", "Listener method names must be onFoo() style")
860
861        if len(clazz.methods) == 1 and clazz.name.startswith("On"):
862            m = clazz.methods[0]
863            if (m.name + "Listener").lower() != clazz.name.lower():
864                error(clazz, m, "L1", "Single listener method name must match class name")
865
866
867@verifier
868def verify_actions(clazz):
869    """Verify intent actions.
870    All action names must be named ACTION_FOO.
871    All action values must be scoped by package and match name:
872        package android.foo {
873            String ACTION_BAR = "android.foo.action.BAR";
874        }"""
875    for f in clazz.fields:
876        if f.value is None: continue
877        if f.name.startswith("EXTRA_"): continue
878        if f.name == "SERVICE_INTERFACE" or f.name == "PROVIDER_INTERFACE": continue
879        if "INTERACTION" in f.name: continue
880
881        if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
882            if "_ACTION" in f.name or "ACTION_" in f.name or ".action." in f.value.lower():
883                if not f.name.startswith("ACTION_"):
884                    error(clazz, f, "C3", "Intent action constant name must be ACTION_FOO")
885                else:
886                    if clazz.fullname == "android.content.Intent":
887                        prefix = "android.intent.action"
888                    elif clazz.fullname == "android.provider.Settings":
889                        prefix = "android.settings"
890                    elif clazz.fullname == "android.app.admin.DevicePolicyManager" or clazz.fullname == "android.app.admin.DeviceAdminReceiver":
891                        prefix = "android.app.action"
892                    else:
893                        prefix = clazz.pkg.name + ".action"
894                    expected = prefix + "." + f.name[7:]
895                    if f.value != expected:
896                        error(clazz, f, "C4", "Inconsistent action value; expected '%s'" % (expected))
897
898
899@verifier
900def verify_extras(clazz):
901    """Verify intent extras.
902    All extra names must be named EXTRA_FOO.
903    All extra values must be scoped by package and match name:
904        package android.foo {
905            String EXTRA_BAR = "android.foo.extra.BAR";
906        }"""
907    if clazz.fullname == "android.app.Notification": return
908    if clazz.fullname == "android.appwidget.AppWidgetManager": return
909
910    for f in clazz.fields:
911        if f.value is None: continue
912        if f.name.startswith("ACTION_"): continue
913
914        if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
915            if "_EXTRA" in f.name or "EXTRA_" in f.name or ".extra" in f.value.lower():
916                if not f.name.startswith("EXTRA_"):
917                    error(clazz, f, "C3", "Intent extra must be EXTRA_FOO")
918                else:
919                    if clazz.pkg.name == "android.content" and clazz.name == "Intent":
920                        prefix = "android.intent.extra"
921                    elif clazz.pkg.name == "android.app.admin":
922                        prefix = "android.app.extra"
923                    else:
924                        prefix = clazz.pkg.name + ".extra"
925                    expected = prefix + "." + f.name[6:]
926                    if f.value != expected:
927                        error(clazz, f, "C4", "Inconsistent extra value; expected '%s'" % (expected))
928
929
930@verifier
931def verify_equals(clazz):
932    """Verify that equals() and hashCode() must be overridden together."""
933    eq = False
934    hc = False
935    for m in clazz.methods:
936        if "static" in m.split: continue
937        if m.sig_matches("boolean", "equals", ["java.lang.Object"]): eq = True
938        if m.sig_matches("int", "hashCode", []): hc = True
939    if eq != hc:
940        error(clazz, None, "M8", "Must override both equals and hashCode; missing one")
941
942
943@verifier
944def verify_parcelable(clazz):
945    """Verify that Parcelable objects aren't hiding required bits."""
946    if clazz.implements == "android.os.Parcelable":
947        creator = [ i for i in clazz.fields if i.name == "CREATOR" ]
948        write = [ i for i in clazz.methods if i.name == "writeToParcel" ]
949        describe = [ i for i in clazz.methods if i.name == "describeContents" ]
950
951        if len(creator) == 0 or len(write) == 0 or len(describe) == 0:
952            error(clazz, None, "FW3", "Parcelable requires CREATOR, writeToParcel, and describeContents; missing one")
953
954        if "final" not in clazz.split:
955            error(clazz, None, "FW8", "Parcelable classes must be final")
956
957        for c in clazz.ctors:
958            if c.args == ["android.os.Parcel"]:
959                error(clazz, c, "FW3", "Parcelable inflation is exposed through CREATOR, not raw constructors")
960
961
962@verifier
963def verify_protected(clazz):
964    """Verify that no protected methods or fields are allowed."""
965    for m in clazz.methods:
966        if m.name == "finalize": continue
967        if "protected" in m.split:
968            error(clazz, m, "M7", "Protected methods not allowed; must be public")
969    for f in clazz.fields:
970        if "protected" in f.split:
971            error(clazz, f, "M7", "Protected fields not allowed; must be public")
972
973
974@verifier
975def verify_fields(clazz):
976    """Verify that all exposed fields are final.
977    Exposed fields must follow myName style.
978    Catch internal mFoo objects being exposed."""
979
980    IGNORE_BARE_FIELDS = [
981        "android.app.ActivityManager.RecentTaskInfo",
982        "android.app.Notification",
983        "android.content.pm.ActivityInfo",
984        "android.content.pm.ApplicationInfo",
985        "android.content.pm.ComponentInfo",
986        "android.content.pm.ResolveInfo",
987        "android.content.pm.FeatureGroupInfo",
988        "android.content.pm.InstrumentationInfo",
989        "android.content.pm.PackageInfo",
990        "android.content.pm.PackageItemInfo",
991        "android.content.res.Configuration",
992        "android.graphics.BitmapFactory.Options",
993        "android.os.Message",
994        "android.system.StructPollfd",
995    ]
996
997    for f in clazz.fields:
998        if not "final" in f.split:
999            if clazz.fullname in IGNORE_BARE_FIELDS:
1000                pass
1001            elif clazz.fullname.endswith("LayoutParams"):
1002                pass
1003            elif clazz.fullname.startswith("android.util.Mutable"):
1004                pass
1005            else:
1006                error(clazz, f, "F2", "Bare fields must be marked final, or add accessors if mutable")
1007
1008        if "static" not in f.split and "property" not in f.split:
1009            if not re.match("[a-z]([a-zA-Z]+)?", f.name):
1010                error(clazz, f, "S1", "Non-static fields must be named using myField style")
1011
1012        if re.match("[ms][A-Z]", f.name):
1013            error(clazz, f, "F1", "Internal objects must not be exposed")
1014
1015        if re.match("[A-Z_]+", f.name):
1016            if "static" not in f.split or "final" not in f.split:
1017                error(clazz, f, "C2", "Constants must be marked static final")
1018
1019
1020@verifier
1021def verify_register(clazz):
1022    """Verify parity of registration methods.
1023    Callback objects use register/unregister methods.
1024    Listener objects use add/remove methods."""
1025    methods = [ m.name for m in clazz.methods ]
1026    for m in clazz.methods:
1027        if "Callback" in m.raw:
1028            if m.name.startswith("register"):
1029                other = "unregister" + m.name[8:]
1030                if other not in methods:
1031                    error(clazz, m, "L2", "Missing unregister method")
1032            if m.name.startswith("unregister"):
1033                other = "register" + m.name[10:]
1034                if other not in methods:
1035                    error(clazz, m, "L2", "Missing register method")
1036
1037            if m.name.startswith("add") or m.name.startswith("remove"):
1038                error(clazz, m, "L3", "Callback methods should be named register/unregister")
1039
1040        if "Listener" in m.raw:
1041            if m.name.startswith("add"):
1042                other = "remove" + m.name[3:]
1043                if other not in methods:
1044                    error(clazz, m, "L2", "Missing remove method")
1045            if m.name.startswith("remove") and not m.name.startswith("removeAll"):
1046                other = "add" + m.name[6:]
1047                if other not in methods:
1048                    error(clazz, m, "L2", "Missing add method")
1049
1050            if m.name.startswith("register") or m.name.startswith("unregister"):
1051                error(clazz, m, "L3", "Listener methods should be named add/remove")
1052
1053
1054@verifier
1055def verify_sync(clazz):
1056    """Verify synchronized methods aren't exposed."""
1057    for m in clazz.methods:
1058        if "synchronized" in m.split:
1059            error(clazz, m, "M5", "Internal locks must not be exposed")
1060
1061
1062@verifier
1063def verify_intent_builder(clazz):
1064    """Verify that Intent builders are createFooIntent() style."""
1065    if clazz.name == "Intent": return
1066
1067    for m in clazz.methods:
1068        if m.typ == "android.content.Intent":
1069            if m.name.startswith("create") and m.name.endswith("Intent"):
1070                pass
1071            else:
1072                warn(clazz, m, "FW1", "Methods creating an Intent should be named createFooIntent()")
1073
1074
1075@verifier
1076def verify_helper_classes(clazz):
1077    """Verify that helper classes are named consistently with what they extend.
1078    All developer extendable methods should be named onFoo()."""
1079    test_methods = False
1080    if clazz.extends == "android.app.Service":
1081        test_methods = True
1082        if not clazz.name.endswith("Service"):
1083            error(clazz, None, "CL4", "Inconsistent class name; should be FooService")
1084
1085        found = False
1086        for f in clazz.fields:
1087            if f.name == "SERVICE_INTERFACE":
1088                found = True
1089                if f.value != clazz.fullname:
1090                    error(clazz, f, "C4", "Inconsistent interface constant; expected '%s'" % (clazz.fullname))
1091
1092    if clazz.extends == "android.content.ContentProvider":
1093        test_methods = True
1094        if not clazz.name.endswith("Provider"):
1095            error(clazz, None, "CL4", "Inconsistent class name; should be FooProvider")
1096
1097        found = False
1098        for f in clazz.fields:
1099            if f.name == "PROVIDER_INTERFACE":
1100                found = True
1101                if f.value != clazz.fullname:
1102                    error(clazz, f, "C4", "Inconsistent interface constant; expected '%s'" % (clazz.fullname))
1103
1104    if clazz.extends == "android.content.BroadcastReceiver":
1105        test_methods = True
1106        if not clazz.name.endswith("Receiver"):
1107            error(clazz, None, "CL4", "Inconsistent class name; should be FooReceiver")
1108
1109    if clazz.extends == "android.app.Activity":
1110        test_methods = True
1111        if not clazz.name.endswith("Activity"):
1112            error(clazz, None, "CL4", "Inconsistent class name; should be FooActivity")
1113
1114    if test_methods:
1115        for m in clazz.methods:
1116            if "final" in m.split: continue
1117            if not re.match("on[A-Z]", m.name):
1118                if "abstract" in m.split:
1119                    warn(clazz, m, None, "Methods implemented by developers should be named onFoo()")
1120                else:
1121                    warn(clazz, m, None, "If implemented by developer, should be named onFoo(); otherwise consider marking final")
1122
1123
1124@verifier
1125def verify_builder(clazz):
1126    """Verify builder classes.
1127    Methods should return the builder to enable chaining."""
1128    if clazz.extends: return
1129    if not clazz.name.endswith("Builder"): return
1130
1131    if clazz.name != "Builder":
1132        warn(clazz, None, None, "Builder should be defined as inner class")
1133
1134    has_build = False
1135    for m in clazz.methods:
1136        if m.name == "build":
1137            has_build = True
1138            continue
1139
1140        if m.name.startswith("get"): continue
1141        if m.name.startswith("clear"): continue
1142
1143        if m.name.startswith("with"):
1144            warn(clazz, m, None, "Builder methods names should use setFoo() style")
1145
1146        if m.name.startswith("set"):
1147            if not m.typ.endswith(clazz.fullname):
1148                warn(clazz, m, "M4", "Methods must return the builder object")
1149
1150    if not has_build:
1151        warn(clazz, None, None, "Missing build() method")
1152
1153    if "final" not in clazz.split:
1154        error(clazz, None, None, "Builder should be final")
1155
1156
1157@verifier
1158def verify_aidl(clazz):
1159    """Catch people exposing raw AIDL."""
1160    if clazz.extends == "android.os.Binder" or clazz.implements == "android.os.IInterface":
1161        error(clazz, None, None, "Raw AIDL interfaces must not be exposed")
1162
1163
1164@verifier
1165def verify_internal(clazz):
1166    """Catch people exposing internal classes."""
1167    if clazz.pkg.name.startswith("com.android"):
1168        error(clazz, None, None, "Internal classes must not be exposed")
1169
1170def layering_build_ranking(ranking_list):
1171    r = {}
1172    for rank, ps in enumerate(ranking_list):
1173        if not isinstance(ps, list):
1174            ps = [ps]
1175        for p in ps:
1176            rs = r
1177            for n in p.split('.'):
1178                if n not in rs:
1179                    rs[n] = {}
1180                rs = rs[n]
1181            rs['-rank'] = rank
1182    return r
1183
1184LAYERING_PACKAGE_RANKING = layering_build_ranking([
1185    ["android.service","android.accessibilityservice","android.inputmethodservice","android.printservice","android.appwidget","android.webkit","android.preference","android.gesture","android.print"],
1186    "android.app",
1187    "android.widget",
1188    "android.view",
1189    "android.animation",
1190    "android.provider",
1191    ["android.content","android.graphics.drawable"],
1192    "android.database",
1193    "android.text",
1194    "android.graphics",
1195    "android.os",
1196    "android.util"
1197])
1198
1199@verifier
1200def verify_layering(clazz):
1201    """Catch package layering violations.
1202    For example, something in android.os depending on android.app."""
1203
1204    def rank(p):
1205        r = None
1206        l = LAYERING_PACKAGE_RANKING
1207        for n in p.split('.'):
1208            if n in l:
1209                l = l[n]
1210                if '-rank' in l:
1211                    r = l['-rank']
1212            else:
1213                break
1214        return r
1215
1216    cr = rank(clazz.pkg.name)
1217    if cr is None: return
1218
1219    for f in clazz.fields:
1220        ir = rank(f.typ)
1221        if ir is not None and ir < cr:
1222            warn(clazz, f, "FW6", "Field type violates package layering")
1223
1224    for m in itertools.chain(clazz.methods, clazz.ctors):
1225        ir = rank(m.typ)
1226        if ir is not None and ir < cr:
1227            warn(clazz, m, "FW6", "Method return type violates package layering")
1228        for arg in m.args:
1229            ir = rank(arg)
1230            if ir is not None and ir < cr:
1231                warn(clazz, m, "FW6", "Method argument type violates package layering")
1232
1233
1234@verifier
1235def verify_boolean(clazz):
1236    """Verifies that boolean accessors are named correctly.
1237    For example, hasFoo() and setHasFoo()."""
1238
1239    def is_get(m): return len(m.args) == 0 and m.typ == "boolean"
1240    def is_set(m): return len(m.args) == 1 and m.args[0] == "boolean"
1241
1242    gets = [ m for m in clazz.methods if is_get(m) ]
1243    sets = [ m for m in clazz.methods if is_set(m) ]
1244
1245    def error_if_exists(methods, trigger, expected, actual):
1246        for m in methods:
1247            if m.name == actual:
1248                error(clazz, m, "M6", "Symmetric method for %s must be named %s" % (trigger, expected))
1249
1250    for m in clazz.methods:
1251        if is_get(m):
1252            if re.match("is[A-Z]", m.name):
1253                target = m.name[2:]
1254                expected = "setIs" + target
1255                error_if_exists(sets, m.name, expected, "setHas" + target)
1256            elif re.match("has[A-Z]", m.name):
1257                target = m.name[3:]
1258                expected = "setHas" + target
1259                error_if_exists(sets, m.name, expected, "setIs" + target)
1260                error_if_exists(sets, m.name, expected, "set" + target)
1261            elif re.match("get[A-Z]", m.name):
1262                target = m.name[3:]
1263                expected = "set" + target
1264                error_if_exists(sets, m.name, expected, "setIs" + target)
1265                error_if_exists(sets, m.name, expected, "setHas" + target)
1266
1267        if is_set(m):
1268            if re.match("set[A-Z]", m.name):
1269                target = m.name[3:]
1270                expected = "get" + target
1271                error_if_exists(sets, m.name, expected, "is" + target)
1272                error_if_exists(sets, m.name, expected, "has" + target)
1273
1274
1275@verifier
1276def verify_collections(clazz):
1277    """Verifies that collection types are interfaces."""
1278    if clazz.fullname == "android.os.Bundle": return
1279    if clazz.fullname == "android.os.Parcel": return
1280
1281    bad = ["java.util.Vector", "java.util.LinkedList", "java.util.ArrayList", "java.util.Stack",
1282           "java.util.HashMap", "java.util.HashSet", "android.util.ArraySet", "android.util.ArrayMap"]
1283    for m in clazz.methods:
1284        if m.typ in bad:
1285            error(clazz, m, "CL2", "Return type is concrete collection; must be higher-level interface")
1286        for arg in m.args:
1287            if arg in bad:
1288                error(clazz, m, "CL2", "Argument is concrete collection; must be higher-level interface")
1289
1290
1291@verifier
1292def verify_uris(clazz):
1293    bad = ["java.net.URL", "java.net.URI", "android.net.URL"]
1294
1295    for f in clazz.fields:
1296        if f.typ in bad:
1297            error(clazz, f, None, "Field must be android.net.Uri instead of " + f.typ)
1298
1299    for m in clazz.methods + clazz.ctors:
1300        if m.typ in bad:
1301            error(clazz, m, None, "Must return android.net.Uri instead of " + m.typ)
1302        for arg in m.args:
1303            if arg in bad:
1304                error(clazz, m, None, "Argument must take android.net.Uri instead of " + arg)
1305
1306
1307@verifier
1308def verify_flags(clazz):
1309    """Verifies that flags are non-overlapping."""
1310    known = collections.defaultdict(int)
1311    for f in clazz.fields:
1312        if "FLAG_" in f.name:
1313            try:
1314                val = int(f.value)
1315            except:
1316                continue
1317
1318            scope = f.name[0:f.name.index("FLAG_")]
1319            if val & known[scope]:
1320                warn(clazz, f, "C1", "Found overlapping flag constant value")
1321            known[scope] |= val
1322
1323
1324@verifier
1325def verify_exception(clazz):
1326    """Verifies that methods don't throw generic exceptions."""
1327    for m in clazz.methods:
1328        for t in m.throws:
1329            if t in ["java.lang.Exception", "java.lang.Throwable", "java.lang.Error"]:
1330                error(clazz, m, "S1", "Methods must not throw generic exceptions")
1331
1332            if t in ["android.os.RemoteException"]:
1333                if clazz.fullname == "android.content.ContentProviderClient": continue
1334                if clazz.fullname == "android.os.Binder": continue
1335                if clazz.fullname == "android.os.IBinder": continue
1336
1337                error(clazz, m, "FW9", "Methods calling into system server should rethrow RemoteException as RuntimeException")
1338
1339            if len(m.args) == 0 and t in ["java.lang.IllegalArgumentException", "java.lang.NullPointerException"]:
1340                warn(clazz, m, "S1", "Methods taking no arguments should throw IllegalStateException")
1341
1342
1343GOOGLE_IGNORECASE = re.compile("google", re.IGNORECASE)
1344
1345# Not marked as @verifier, because it is only conditionally applied.
1346def verify_google(clazz):
1347    """Verifies that APIs never reference Google."""
1348
1349    if GOOGLE_IGNORECASE.search(clazz.raw) is not None:
1350        error(clazz, None, None, "Must never reference Google")
1351
1352    for test in clazz.ctors, clazz.fields, clazz.methods:
1353        for t in test:
1354            if GOOGLE_IGNORECASE.search(t.raw) is not None:
1355                error(clazz, t, None, "Must never reference Google")
1356
1357
1358@verifier
1359def verify_bitset(clazz):
1360    """Verifies that we avoid using heavy BitSet."""
1361
1362    for f in clazz.fields:
1363        if f.typ == "java.util.BitSet":
1364            error(clazz, f, None, "Field type must not be heavy BitSet")
1365
1366    for m in clazz.methods:
1367        if m.typ == "java.util.BitSet":
1368            error(clazz, m, None, "Return type must not be heavy BitSet")
1369        for arg in m.args:
1370            if arg == "java.util.BitSet":
1371                error(clazz, m, None, "Argument type must not be heavy BitSet")
1372
1373
1374@verifier
1375def verify_manager(clazz):
1376    """Verifies that FooManager is only obtained from Context."""
1377
1378    if not clazz.name.endswith("Manager"): return
1379
1380    for c in clazz.ctors:
1381        error(clazz, c, None, "Managers must always be obtained from Context; no direct constructors")
1382
1383    for m in clazz.methods:
1384        if m.typ == clazz.fullname:
1385            error(clazz, m, None, "Managers must always be obtained from Context")
1386
1387
1388@verifier
1389def verify_boxed(clazz):
1390    """Verifies that methods avoid boxed primitives."""
1391
1392    boxed = ["java.lang.Number","java.lang.Byte","java.lang.Double","java.lang.Float","java.lang.Integer","java.lang.Long","java.lang.Short"]
1393
1394    for c in clazz.ctors:
1395        for arg in c.args:
1396            if arg in boxed:
1397                error(clazz, c, "M11", "Must avoid boxed primitives")
1398
1399    for f in clazz.fields:
1400        if f.typ in boxed:
1401            error(clazz, f, "M11", "Must avoid boxed primitives")
1402
1403    for m in clazz.methods:
1404        if m.typ in boxed:
1405            error(clazz, m, "M11", "Must avoid boxed primitives")
1406        for arg in m.args:
1407            if arg in boxed:
1408                error(clazz, m, "M11", "Must avoid boxed primitives")
1409
1410
1411@verifier
1412def verify_static_utils(clazz):
1413    """Verifies that helper classes can't be constructed."""
1414    if clazz.fullname.startswith("android.opengl"): return
1415    if clazz.fullname.startswith("android.R"): return
1416
1417    # Only care about classes with default constructors
1418    if len(clazz.ctors) == 1 and len(clazz.ctors[0].args) == 0:
1419        test = []
1420        test.extend(clazz.fields)
1421        test.extend(clazz.methods)
1422
1423        if len(test) == 0: return
1424        for t in test:
1425            if "static" not in t.split:
1426                return
1427
1428        error(clazz, None, None, "Fully-static utility classes must not have constructor")
1429
1430
1431# @verifier  # Disabled for now
1432def verify_overload_args(clazz):
1433    """Verifies that method overloads add new arguments at the end."""
1434    if clazz.fullname.startswith("android.opengl"): return
1435
1436    overloads = collections.defaultdict(list)
1437    for m in clazz.methods:
1438        if "deprecated" in m.split: continue
1439        overloads[m.name].append(m)
1440
1441    for name, methods in overloads.items():
1442        if len(methods) <= 1: continue
1443
1444        # Look for arguments common across all overloads
1445        def cluster(args):
1446            count = collections.defaultdict(int)
1447            res = set()
1448            for i in range(len(args)):
1449                a = args[i]
1450                res.add("%s#%d" % (a, count[a]))
1451                count[a] += 1
1452            return res
1453
1454        common_args = cluster(methods[0].args)
1455        for m in methods:
1456            common_args = common_args & cluster(m.args)
1457
1458        if len(common_args) == 0: continue
1459
1460        # Require that all common arguments are present at start of signature
1461        locked_sig = None
1462        for m in methods:
1463            sig = m.args[0:len(common_args)]
1464            if not common_args.issubset(cluster(sig)):
1465                warn(clazz, m, "M2", "Expected common arguments [%s] at beginning of overloaded method" % (", ".join(common_args)))
1466            elif not locked_sig:
1467                locked_sig = sig
1468            elif locked_sig != sig:
1469                error(clazz, m, "M2", "Expected consistent argument ordering between overloads: %s..." % (", ".join(locked_sig)))
1470
1471
1472@verifier
1473def verify_callback_handlers(clazz):
1474    """Verifies that methods adding listener/callback have overload
1475    for specifying delivery thread."""
1476
1477    # Ignore UI packages which assume main thread
1478    skip = [
1479        "animation",
1480        "view",
1481        "graphics",
1482        "transition",
1483        "widget",
1484        "webkit",
1485    ]
1486    for s in skip:
1487        if s in clazz.pkg.name_path: return
1488        if s in clazz.extends_path: return
1489
1490    # Ignore UI classes which assume main thread
1491    if "app" in clazz.pkg.name_path or "app" in clazz.extends_path:
1492        for s in ["ActionBar","Dialog","Application","Activity","Fragment","Loader"]:
1493            if s in clazz.fullname: return
1494    if "content" in clazz.pkg.name_path or "content" in clazz.extends_path:
1495        for s in ["Loader"]:
1496            if s in clazz.fullname: return
1497
1498    found = {}
1499    by_name = collections.defaultdict(list)
1500    examine = clazz.ctors + clazz.methods
1501    for m in examine:
1502        if m.name.startswith("unregister"): continue
1503        if m.name.startswith("remove"): continue
1504        if re.match("on[A-Z]+", m.name): continue
1505
1506        by_name[m.name].append(m)
1507
1508        for a in m.args:
1509            if a.endswith("Listener") or a.endswith("Callback") or a.endswith("Callbacks"):
1510                found[m.name] = m
1511
1512    for f in found.values():
1513        takes_handler = False
1514        takes_exec = False
1515        for m in by_name[f.name]:
1516            if "android.os.Handler" in m.args:
1517                takes_handler = True
1518            if "java.util.concurrent.Executor" in m.args:
1519                takes_exec = True
1520        if not takes_exec:
1521            warn(clazz, f, "L1", "Registration methods should have overload that accepts delivery Executor")
1522
1523
1524@verifier
1525def verify_context_first(clazz):
1526    """Verifies that methods accepting a Context keep it the first argument."""
1527    examine = clazz.ctors + clazz.methods
1528    for m in examine:
1529        if len(m.args) > 1 and m.args[0] != "android.content.Context":
1530            if "android.content.Context" in m.args[1:]:
1531                error(clazz, m, "M3", "Context is distinct, so it must be the first argument")
1532        if len(m.args) > 1 and m.args[0] != "android.content.ContentResolver":
1533            if "android.content.ContentResolver" in m.args[1:]:
1534                error(clazz, m, "M3", "ContentResolver is distinct, so it must be the first argument")
1535
1536
1537@verifier
1538def verify_listener_last(clazz):
1539    """Verifies that methods accepting a Listener or Callback keep them as last arguments."""
1540    examine = clazz.ctors + clazz.methods
1541    for m in examine:
1542        if "Listener" in m.name or "Callback" in m.name: continue
1543        found = False
1544        for a in m.args:
1545            if a.endswith("Callback") or a.endswith("Callbacks") or a.endswith("Listener"):
1546                found = True
1547            elif found:
1548                warn(clazz, m, "M3", "Listeners should always be at end of argument list")
1549
1550
1551@verifier
1552def verify_resource_names(clazz):
1553    """Verifies that resource names have consistent case."""
1554    if not re.match("android\.R\.[a-z]+", clazz.fullname): return
1555
1556    # Resources defined by files are foo_bar_baz
1557    if clazz.name in ["anim","animator","color","dimen","drawable","interpolator","layout","transition","menu","mipmap","string","plurals","raw","xml"]:
1558        for f in clazz.fields:
1559            if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue
1560            if f.name.startswith("config_"):
1561                error(clazz, f, None, "Expected config name to be config_fooBarBaz style")
1562
1563            if re.match("[a-z1-9_]+$", f.name): continue
1564            error(clazz, f, None, "Expected resource name in this class to be foo_bar_baz style")
1565
1566    # Resources defined inside files are fooBarBaz
1567    if clazz.name in ["array","attr","id","bool","fraction","integer"]:
1568        for f in clazz.fields:
1569            if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue
1570            if re.match("layout_[a-z][a-zA-Z1-9]*$", f.name): continue
1571            if re.match("state_[a-z_]*$", f.name): continue
1572
1573            if re.match("[a-z][a-zA-Z1-9]*$", f.name): continue
1574            error(clazz, f, "C7", "Expected resource name in this class to be fooBarBaz style")
1575
1576    # Styles are FooBar_Baz
1577    if clazz.name in ["style"]:
1578        for f in clazz.fields:
1579            if re.match("[A-Z][A-Za-z1-9]+(_[A-Z][A-Za-z1-9]+?)*$", f.name): continue
1580            error(clazz, f, "C7", "Expected resource name in this class to be FooBar_Baz style")
1581
1582
1583@verifier
1584def verify_files(clazz):
1585    """Verifies that methods accepting File also accept streams."""
1586
1587    has_file = set()
1588    has_stream = set()
1589
1590    test = []
1591    test.extend(clazz.ctors)
1592    test.extend(clazz.methods)
1593
1594    for m in test:
1595        if "java.io.File" in m.args:
1596            has_file.add(m)
1597        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:
1598            has_stream.add(m.name)
1599
1600    for m in has_file:
1601        if m.name not in has_stream:
1602            warn(clazz, m, "M10", "Methods accepting File should also accept FileDescriptor or streams")
1603
1604
1605@verifier
1606def verify_manager_list(clazz):
1607    """Verifies that managers return List<? extends Parcelable> instead of arrays."""
1608
1609    if not clazz.name.endswith("Manager"): return
1610
1611    for m in clazz.methods:
1612        if m.typ.startswith("android.") and m.typ.endswith("[]"):
1613            warn(clazz, m, None, "Methods should return List<? extends Parcelable> instead of Parcelable[] to support ParceledListSlice under the hood")
1614
1615
1616@verifier
1617def verify_abstract_inner(clazz):
1618    """Verifies that abstract inner classes are static."""
1619
1620    if re.match(".+?\.[A-Z][^\.]+\.[A-Z]", clazz.fullname):
1621        if "abstract" in clazz.split and "static" not in clazz.split:
1622            warn(clazz, None, None, "Abstract inner classes should be static to improve testability")
1623
1624
1625@verifier
1626def verify_runtime_exceptions(clazz):
1627    """Verifies that runtime exceptions aren't listed in throws."""
1628
1629    banned = [
1630        "java.lang.NullPointerException",
1631        "java.lang.ClassCastException",
1632        "java.lang.IndexOutOfBoundsException",
1633        "java.lang.reflect.UndeclaredThrowableException",
1634        "java.lang.reflect.MalformedParametersException",
1635        "java.lang.reflect.MalformedParameterizedTypeException",
1636        "java.lang.invoke.WrongMethodTypeException",
1637        "java.lang.EnumConstantNotPresentException",
1638        "java.lang.IllegalMonitorStateException",
1639        "java.lang.SecurityException",
1640        "java.lang.UnsupportedOperationException",
1641        "java.lang.annotation.AnnotationTypeMismatchException",
1642        "java.lang.annotation.IncompleteAnnotationException",
1643        "java.lang.TypeNotPresentException",
1644        "java.lang.IllegalStateException",
1645        "java.lang.ArithmeticException",
1646        "java.lang.IllegalArgumentException",
1647        "java.lang.ArrayStoreException",
1648        "java.lang.NegativeArraySizeException",
1649        "java.util.MissingResourceException",
1650        "java.util.EmptyStackException",
1651        "java.util.concurrent.CompletionException",
1652        "java.util.concurrent.RejectedExecutionException",
1653        "java.util.IllformedLocaleException",
1654        "java.util.ConcurrentModificationException",
1655        "java.util.NoSuchElementException",
1656        "java.io.UncheckedIOException",
1657        "java.time.DateTimeException",
1658        "java.security.ProviderException",
1659        "java.nio.BufferUnderflowException",
1660        "java.nio.BufferOverflowException",
1661    ]
1662
1663    examine = clazz.ctors + clazz.methods
1664    for m in examine:
1665        for t in m.throws:
1666            if t in banned:
1667                error(clazz, m, None, "Methods must not mention RuntimeException subclasses in throws clauses")
1668
1669
1670@verifier
1671def verify_error(clazz):
1672    """Verifies that we always use Exception instead of Error."""
1673    if not clazz.extends: return
1674    if clazz.extends.endswith("Error"):
1675        error(clazz, None, None, "Trouble must be reported through an Exception, not Error")
1676    if clazz.extends.endswith("Exception") and not clazz.name.endswith("Exception"):
1677        error(clazz, None, None, "Exceptions must be named FooException")
1678
1679
1680@verifier
1681def verify_units(clazz):
1682    """Verifies that we use consistent naming for units."""
1683
1684    # If we find K, recommend replacing with V
1685    bad = {
1686        "Ns": "Nanos",
1687        "Ms": "Millis or Micros",
1688        "Sec": "Seconds", "Secs": "Seconds",
1689        "Hr": "Hours", "Hrs": "Hours",
1690        "Mo": "Months", "Mos": "Months",
1691        "Yr": "Years", "Yrs": "Years",
1692        "Byte": "Bytes", "Space": "Bytes",
1693    }
1694
1695    for m in clazz.methods:
1696        if m.typ not in ["short","int","long"]: continue
1697        for k, v in bad.iteritems():
1698            if m.name.endswith(k):
1699                error(clazz, m, None, "Expected method name units to be " + v)
1700        if m.name.endswith("Nanos") or m.name.endswith("Micros"):
1701            warn(clazz, m, None, "Returned time values are strongly encouraged to be in milliseconds unless you need the extra precision")
1702        if m.name.endswith("Seconds"):
1703            error(clazz, m, None, "Returned time values must be in milliseconds")
1704
1705    for m in clazz.methods:
1706        typ = m.typ
1707        if typ == "void":
1708            if len(m.args) != 1: continue
1709            typ = m.args[0]
1710
1711        if m.name.endswith("Fraction") and typ != "float":
1712            error(clazz, m, None, "Fractions must use floats")
1713        if m.name.endswith("Percentage") and typ != "int":
1714            error(clazz, m, None, "Percentage must use ints")
1715
1716
1717@verifier
1718def verify_closable(clazz):
1719    """Verifies that classes are AutoClosable."""
1720    if "java.lang.AutoCloseable" in clazz.implements_all: return
1721    if "java.io.Closeable" in clazz.implements_all: return
1722
1723    for m in clazz.methods:
1724        if len(m.args) > 0: continue
1725        if m.name in ["close","release","destroy","finish","finalize","disconnect","shutdown","stop","free","quit"]:
1726            warn(clazz, m, None, "Classes that release resources should implement AutoClosable and CloseGuard")
1727            return
1728
1729
1730@verifier
1731def verify_member_name_not_kotlin_keyword(clazz):
1732    """Prevent method names which are keywords in Kotlin."""
1733
1734    # https://kotlinlang.org/docs/reference/keyword-reference.html#hard-keywords
1735    # This list does not include Java keywords as those are already impossible to use.
1736    keywords = [
1737        'as',
1738        'fun',
1739        'in',
1740        'is',
1741        'object',
1742        'typealias',
1743        'val',
1744        'var',
1745        'when',
1746    ]
1747
1748    for m in clazz.methods:
1749        if m.name in keywords:
1750            error(clazz, m, None, "Method name must not be a Kotlin keyword")
1751    for f in clazz.fields:
1752        if f.name in keywords:
1753            error(clazz, f, None, "Field name must not be a Kotlin keyword")
1754
1755
1756@verifier
1757def verify_method_name_not_kotlin_operator(clazz):
1758    """Warn about method names which become operators in Kotlin."""
1759
1760    binary = set()
1761
1762    def unique_binary_op(m, op):
1763        if op in binary:
1764            error(clazz, m, None, "Only one of '{0}' and '{0}Assign' methods should be present for Kotlin".format(op))
1765        binary.add(op)
1766
1767    for m in clazz.methods:
1768        if 'static' in m.split or 'operator' in m.split:
1769            continue
1770
1771        # https://kotlinlang.org/docs/reference/operator-overloading.html#unary-prefix-operators
1772        if m.name in ['unaryPlus', 'unaryMinus', 'not'] and len(m.args) == 0:
1773            warn(clazz, m, None, "Method can be invoked as a unary operator from Kotlin")
1774
1775        # https://kotlinlang.org/docs/reference/operator-overloading.html#increments-and-decrements
1776        if m.name in ['inc', 'dec'] and len(m.args) == 0 and m.typ != 'void':
1777            # This only applies if the return type is the same or a subtype of the enclosing class, but we have no
1778            # practical way of checking that relationship here.
1779            warn(clazz, m, None, "Method can be invoked as a pre/postfix inc/decrement operator from Kotlin")
1780
1781        # https://kotlinlang.org/docs/reference/operator-overloading.html#arithmetic
1782        if m.name in ['plus', 'minus', 'times', 'div', 'rem', 'mod', 'rangeTo'] and len(m.args) == 1:
1783            warn(clazz, m, None, "Method can be invoked as a binary operator from Kotlin")
1784            unique_binary_op(m, m.name)
1785
1786        # https://kotlinlang.org/docs/reference/operator-overloading.html#in
1787        if m.name == 'contains' and len(m.args) == 1 and m.typ == 'boolean':
1788            warn(clazz, m, None, "Method can be invoked as a 'in' operator from Kotlin")
1789
1790        # https://kotlinlang.org/docs/reference/operator-overloading.html#indexed
1791        if (m.name == 'get' and len(m.args) > 0) or (m.name == 'set' and len(m.args) > 1):
1792            warn(clazz, m, None, "Method can be invoked with an indexing operator from Kotlin")
1793
1794        # https://kotlinlang.org/docs/reference/operator-overloading.html#invoke
1795        if m.name == 'invoke':
1796            warn(clazz, m, None, "Method can be invoked with function call syntax from Kotlin")
1797
1798        # https://kotlinlang.org/docs/reference/operator-overloading.html#assignments
1799        if m.name in ['plusAssign', 'minusAssign', 'timesAssign', 'divAssign', 'remAssign', 'modAssign'] \
1800                and len(m.args) == 1 \
1801                and m.typ == 'void':
1802            warn(clazz, m, None, "Method can be invoked as a compound assignment operator from Kotlin")
1803            unique_binary_op(m, m.name[:-6])  # Remove 'Assign' suffix
1804
1805
1806@verifier
1807def verify_collections_over_arrays(clazz):
1808    """Warn that [] should be Collections."""
1809
1810    if "@interface" in clazz.split:
1811        return
1812
1813    safe = ["java.lang.String[]","byte[]","short[]","int[]","long[]","float[]","double[]","boolean[]","char[]"]
1814    for m in clazz.methods:
1815        if m.typ.endswith("[]") and m.typ not in safe:
1816            warn(clazz, m, None, "Method should return Collection<> (or subclass) instead of raw array")
1817        for arg in m.args:
1818            if arg.endswith("[]") and arg not in safe:
1819                warn(clazz, m, None, "Method argument should be Collection<> (or subclass) instead of raw array")
1820
1821
1822@verifier
1823def verify_user_handle(clazz):
1824    """Methods taking UserHandle should be ForUser or AsUser."""
1825    if clazz.name.endswith("Listener") or clazz.name.endswith("Callback") or clazz.name.endswith("Callbacks"): return
1826    if clazz.fullname == "android.app.admin.DeviceAdminReceiver": return
1827    if clazz.fullname == "android.content.pm.LauncherApps": return
1828    if clazz.fullname == "android.os.UserHandle": return
1829    if clazz.fullname == "android.os.UserManager": return
1830
1831    for m in clazz.methods:
1832        if re.match("on[A-Z]+", m.name): continue
1833
1834        has_arg = "android.os.UserHandle" in m.args
1835        has_name = m.name.endswith("AsUser") or m.name.endswith("ForUser")
1836
1837        if clazz.fullname.endswith("Manager") and has_arg:
1838            warn(clazz, m, None, "When a method overload is needed to target a specific "
1839                 "UserHandle, callers should be directed to use "
1840                 "Context.createPackageContextAsUser() and re-obtain the relevant "
1841                 "Manager, and no new API should be added")
1842        elif has_arg and not has_name:
1843            warn(clazz, m, None, "Method taking UserHandle should be named 'doFooAsUser' "
1844                 "or 'queryFooForUser'")
1845
1846
1847@verifier
1848def verify_params(clazz):
1849    """Parameter classes should be 'Params'."""
1850    if clazz.name.endswith("Params"): return
1851    if clazz.fullname == "android.app.ActivityOptions": return
1852    if clazz.fullname == "android.app.BroadcastOptions": return
1853    if clazz.fullname == "android.os.Bundle": return
1854    if clazz.fullname == "android.os.BaseBundle": return
1855    if clazz.fullname == "android.os.PersistableBundle": return
1856
1857    bad = ["Param","Parameter","Parameters","Args","Arg","Argument","Arguments","Options","Bundle"]
1858    for b in bad:
1859        if clazz.name.endswith(b):
1860            error(clazz, None, None, "Classes holding a set of parameters should be called 'FooParams'")
1861
1862
1863@verifier
1864def verify_services(clazz):
1865    """Service name should be FOO_BAR_SERVICE = 'foo_bar'."""
1866    if clazz.fullname != "android.content.Context": return
1867
1868    for f in clazz.fields:
1869        if f.typ != "java.lang.String": continue
1870        found = re.match(r"([A-Z_]+)_SERVICE", f.name)
1871        if found:
1872            expected = found.group(1).lower()
1873            if f.value != expected:
1874                error(clazz, f, "C4", "Inconsistent service value; expected '%s'" % (expected))
1875
1876
1877@verifier
1878def verify_tense(clazz):
1879    """Verify tenses of method names."""
1880    if clazz.fullname.startswith("android.opengl"): return
1881
1882    for m in clazz.methods:
1883        if m.name.endswith("Enable"):
1884            warn(clazz, m, None, "Unexpected tense; probably meant 'enabled'")
1885
1886
1887@verifier
1888def verify_icu(clazz):
1889    """Verifies that richer ICU replacements are used."""
1890    better = {
1891        "java.util.TimeZone": "android.icu.util.TimeZone",
1892        "java.util.Calendar": "android.icu.util.Calendar",
1893        "java.util.Locale": "android.icu.util.ULocale",
1894        "java.util.ResourceBundle": "android.icu.util.UResourceBundle",
1895        "java.util.SimpleTimeZone": "android.icu.util.SimpleTimeZone",
1896        "java.util.StringTokenizer": "android.icu.util.StringTokenizer",
1897        "java.util.GregorianCalendar": "android.icu.util.GregorianCalendar",
1898        "java.lang.Character": "android.icu.lang.UCharacter",
1899        "java.text.BreakIterator": "android.icu.text.BreakIterator",
1900        "java.text.Collator": "android.icu.text.Collator",
1901        "java.text.DecimalFormatSymbols": "android.icu.text.DecimalFormatSymbols",
1902        "java.text.NumberFormat": "android.icu.text.NumberFormat",
1903        "java.text.DateFormatSymbols": "android.icu.text.DateFormatSymbols",
1904        "java.text.DateFormat": "android.icu.text.DateFormat",
1905        "java.text.SimpleDateFormat": "android.icu.text.SimpleDateFormat",
1906        "java.text.MessageFormat": "android.icu.text.MessageFormat",
1907        "java.text.DecimalFormat": "android.icu.text.DecimalFormat",
1908    }
1909
1910    for m in clazz.ctors + clazz.methods:
1911        types = []
1912        types.extend(m.typ)
1913        types.extend(m.args)
1914        for arg in types:
1915            if arg in better:
1916                warn(clazz, m, None, "Type %s should be replaced with richer ICU type %s" % (arg, better[arg]))
1917
1918
1919@verifier
1920def verify_clone(clazz):
1921    """Verify that clone() isn't implemented; see EJ page 61."""
1922    for m in clazz.methods:
1923        if m.name == "clone":
1924            error(clazz, m, None, "Provide an explicit copy constructor instead of implementing clone()")
1925
1926
1927@verifier
1928def verify_pfd(clazz):
1929    """Verify that android APIs use PFD over FD."""
1930    if clazz.fullname == "android.os.FileUtils" or clazz.fullname == "android.system.Os":
1931        return
1932
1933    examine = clazz.ctors + clazz.methods
1934    for m in examine:
1935        if m.typ == "java.io.FileDescriptor":
1936            error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
1937        if m.typ == "int":
1938            if "Fd" in m.name or "FD" in m.name or "FileDescriptor" in m.name:
1939                error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
1940        for arg in m.args:
1941            if arg == "java.io.FileDescriptor":
1942                error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
1943
1944    for f in clazz.fields:
1945        if f.typ == "java.io.FileDescriptor":
1946            error(clazz, f, "FW11", "Must use ParcelFileDescriptor")
1947
1948
1949@verifier
1950def verify_numbers(clazz):
1951    """Discourage small numbers types like short and byte."""
1952
1953    discouraged = ["short","byte"]
1954
1955    for c in clazz.ctors:
1956        for arg in c.args:
1957            if arg in discouraged:
1958                warn(clazz, c, "FW12", "Should avoid odd sized primitives; use int instead")
1959
1960    for f in clazz.fields:
1961        if f.typ in discouraged:
1962            warn(clazz, f, "FW12", "Should avoid odd sized primitives; use int instead")
1963
1964    for m in clazz.methods:
1965        if m.typ in discouraged:
1966            warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead")
1967        for arg in m.args:
1968            if arg in discouraged:
1969                warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead")
1970
1971
1972PRIMITIVES = {"void", "int", "float", "boolean", "short", "char", "byte", "long", "double"}
1973
1974@verifier
1975def verify_nullability(clazz):
1976    """Catches missing nullability annotations"""
1977
1978    for f in clazz.fields:
1979        if f.value is not None and 'static' in f.split and 'final' in f.split:
1980            continue  # Nullability of constants can be inferred.
1981        if f.typ not in PRIMITIVES and not has_nullability(f.annotations):
1982            error(clazz, f, "M12", "Field must be marked either @NonNull or @Nullable")
1983
1984    for c in clazz.ctors:
1985        verify_nullability_args(clazz, c)
1986
1987    for m in clazz.methods:
1988        if m.name == "writeToParcel" or m.name == "onReceive":
1989            continue  # Parcelable.writeToParcel() and BroadcastReceiver.onReceive() are not yet annotated
1990
1991        if m.typ not in PRIMITIVES and not has_nullability(m.annotations):
1992            error(clazz, m, "M12", "Return value must be marked either @NonNull or @Nullable")
1993        verify_nullability_args(clazz, m)
1994
1995def verify_nullability_args(clazz, m):
1996    for i, arg in enumerate(m.detailed_args):
1997        if arg.type not in PRIMITIVES and not has_nullability(arg.annotations):
1998            error(clazz, m, "M12", "Argument %d must be marked either @NonNull or @Nullable" % (i+1,))
1999
2000def has_nullability(annotations):
2001    return "@NonNull" in annotations or "@Nullable" in annotations
2002
2003
2004@verifier
2005def verify_intdef(clazz):
2006    """intdefs must be @hide, because the constant names cannot be stored in
2007       the stubs (only the values are, which is not useful)"""
2008    if "@interface" not in clazz.split:
2009        return
2010    if "@IntDef" in clazz.annotations or "@LongDef" in clazz.annotations:
2011        error(clazz, None, None, "@IntDef and @LongDef annotations must be @hide")
2012
2013
2014@verifier
2015def verify_singleton(clazz):
2016    """Catch singleton objects with constructors."""
2017
2018    singleton = False
2019    for m in clazz.methods:
2020        if m.name.startswith("get") and m.name.endswith("Instance") and " static " in m.raw:
2021            singleton = True
2022
2023    if singleton:
2024        for c in clazz.ctors:
2025            error(clazz, c, None, "Singleton classes should use getInstance() methods")
2026
2027
2028
2029def is_interesting(clazz):
2030    """Test if given class is interesting from an Android PoV."""
2031
2032    if clazz.pkg.name.startswith("java"): return False
2033    if clazz.pkg.name.startswith("junit"): return False
2034    if clazz.pkg.name.startswith("org.apache"): return False
2035    if clazz.pkg.name.startswith("org.xml"): return False
2036    if clazz.pkg.name.startswith("org.json"): return False
2037    if clazz.pkg.name.startswith("org.w3c"): return False
2038    if clazz.pkg.name.startswith("android.icu."): return False
2039    return True
2040
2041
2042def examine_clazz(clazz):
2043    """Find all style issues in the given class."""
2044
2045    notice(clazz)
2046
2047    if not is_interesting(clazz): return
2048
2049    for v in verifiers.itervalues():
2050        v(clazz)
2051
2052    if not ALLOW_GOOGLE: verify_google(clazz)
2053
2054
2055def examine_stream(stream, base_stream=None, in_classes_with_base=[], out_classes_with_base=None):
2056    """Find all style issues in the given API stream."""
2057    global failures, noticed
2058    failures = {}
2059    noticed = {}
2060    _parse_stream(stream, examine_clazz, base_f=base_stream,
2061                  in_classes_with_base=in_classes_with_base,
2062                  out_classes_with_base=out_classes_with_base)
2063    return (failures, noticed)
2064
2065
2066def examine_api(api):
2067    """Find all style issues in the given parsed API."""
2068    global failures
2069    failures = {}
2070    for key in sorted(api.keys()):
2071        examine_clazz(api[key])
2072    return failures
2073
2074
2075def verify_compat(cur, prev):
2076    """Find any incompatible API changes between two levels."""
2077    global failures
2078
2079    def class_exists(api, test):
2080        return test.fullname in api
2081
2082    def ctor_exists(api, clazz, test):
2083        for m in clazz.ctors:
2084            if m.ident == test.ident: return True
2085        return False
2086
2087    def all_methods(api, clazz):
2088        methods = list(clazz.methods)
2089        if clazz.extends is not None:
2090            methods.extend(all_methods(api, api[clazz.extends]))
2091        return methods
2092
2093    def method_exists(api, clazz, test):
2094        methods = all_methods(api, clazz)
2095        for m in methods:
2096            if m.ident == test.ident: return True
2097        return False
2098
2099    def field_exists(api, clazz, test):
2100        for f in clazz.fields:
2101            if f.ident == test.ident: return True
2102        return False
2103
2104    failures = {}
2105    for key in sorted(prev.keys()):
2106        prev_clazz = prev[key]
2107
2108        if not class_exists(cur, prev_clazz):
2109            error(prev_clazz, None, None, "Class removed or incompatible change")
2110            continue
2111
2112        cur_clazz = cur[key]
2113
2114        for test in prev_clazz.ctors:
2115            if not ctor_exists(cur, cur_clazz, test):
2116                error(prev_clazz, prev_ctor, None, "Constructor removed or incompatible change")
2117
2118        methods = all_methods(prev, prev_clazz)
2119        for test in methods:
2120            if not method_exists(cur, cur_clazz, test):
2121                error(prev_clazz, test, None, "Method removed or incompatible change")
2122
2123        for test in prev_clazz.fields:
2124            if not field_exists(cur, cur_clazz, test):
2125                error(prev_clazz, test, None, "Field removed or incompatible change")
2126
2127    return failures
2128
2129
2130def match_filter(filters, fullname):
2131    for f in filters:
2132        if fullname == f:
2133            return True
2134        if fullname.startswith(f + '.'):
2135            return True
2136    return False
2137
2138
2139def show_deprecations_at_birth(cur, prev):
2140    """Show API deprecations at birth."""
2141    global failures
2142
2143    # Remove all existing things so we're left with new
2144    for prev_clazz in prev.values():
2145        if prev_clazz.fullname not in cur:
2146            # The class was removed this release; we can safely ignore it.
2147            continue
2148
2149        cur_clazz = cur[prev_clazz.fullname]
2150        if not is_interesting(cur_clazz): continue
2151
2152        sigs = { i.ident: i for i in prev_clazz.ctors }
2153        cur_clazz.ctors = [ i for i in cur_clazz.ctors if i.ident not in sigs ]
2154        sigs = { i.ident: i for i in prev_clazz.methods }
2155        cur_clazz.methods = [ i for i in cur_clazz.methods if i.ident not in sigs ]
2156        sigs = { i.ident: i for i in prev_clazz.fields }
2157        cur_clazz.fields = [ i for i in cur_clazz.fields if i.ident not in sigs ]
2158
2159        # Forget about class entirely when nothing new
2160        if len(cur_clazz.ctors) == 0 and len(cur_clazz.methods) == 0 and len(cur_clazz.fields) == 0:
2161            del cur[prev_clazz.fullname]
2162
2163    for clazz in cur.values():
2164        if not is_interesting(clazz): continue
2165
2166        if "deprecated" in clazz.split and not clazz.fullname in prev:
2167            error(clazz, None, None, "Found API deprecation at birth")
2168
2169        for i in clazz.ctors + clazz.methods + clazz.fields:
2170            if "deprecated" in i.split:
2171                error(clazz, i, None, "Found API deprecation at birth")
2172
2173    print "%s Deprecated at birth %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True),
2174                                            format(reset=True)))
2175    for f in sorted(failures):
2176        print failures[f]
2177        print
2178
2179
2180def show_stats(cur, prev):
2181    """Show API stats."""
2182
2183    stats = collections.defaultdict(int)
2184    for cur_clazz in cur.values():
2185        if not is_interesting(cur_clazz): continue
2186
2187        if cur_clazz.fullname not in prev:
2188            stats['new_classes'] += 1
2189            stats['new_ctors'] += len(cur_clazz.ctors)
2190            stats['new_methods'] += len(cur_clazz.methods)
2191            stats['new_fields'] += len(cur_clazz.fields)
2192        else:
2193            prev_clazz = prev[cur_clazz.fullname]
2194
2195            sigs = { i.ident: i for i in prev_clazz.ctors }
2196            ctors = len([ i for i in cur_clazz.ctors if i.ident not in sigs ])
2197            sigs = { i.ident: i for i in prev_clazz.methods }
2198            methods = len([ i for i in cur_clazz.methods if i.ident not in sigs ])
2199            sigs = { i.ident: i for i in prev_clazz.fields }
2200            fields = len([ i for i in cur_clazz.fields if i.ident not in sigs ])
2201
2202            if ctors + methods + fields > 0:
2203                stats['extend_classes'] += 1
2204                stats['extend_ctors'] += ctors
2205                stats['extend_methods'] += methods
2206                stats['extend_fields'] += fields
2207
2208    print "#", "".join([ k.ljust(20) for k in sorted(stats.keys()) ])
2209    print " ", "".join([ str(stats[k]).ljust(20) for k in sorted(stats.keys()) ])
2210
2211
2212def main():
2213    parser = argparse.ArgumentParser(description="Enforces common Android public API design \
2214            patterns. It ignores lint messages from a previous API level, if provided.")
2215    parser.add_argument("--base-current", nargs='?', type=argparse.FileType('r'), default=None,
2216            help="The base current.txt to use when examining system-current.txt or"
2217                 " test-current.txt")
2218    parser.add_argument("--base-previous", nargs='?', type=argparse.FileType('r'), default=None,
2219            help="The base previous.txt to use when examining system-previous.txt or"
2220                 " test-previous.txt")
2221    parser.add_argument("--no-color", action='store_const', const=True,
2222            help="Disable terminal colors")
2223    parser.add_argument("--color", action='store_const', const=True,
2224            help="Use terminal colors")
2225    parser.add_argument("--allow-google", action='store_const', const=True,
2226            help="Allow references to Google")
2227    parser.add_argument("--show-noticed", action='store_const', const=True,
2228            help="Show API changes noticed")
2229    parser.add_argument("--show-deprecations-at-birth", action='store_const', const=True,
2230            help="Show API deprecations at birth")
2231    parser.add_argument("--show-stats", action='store_const', const=True,
2232            help="Show API stats")
2233    parser.add_argument("--title", action='store', default=None,
2234            help="Title to put in for display purposes")
2235    parser.add_argument("--filter", action="append",
2236            help="If provided, only show lint for the given packages or classes.")
2237    parser.add_argument("current.txt", type=argparse.FileType('r'), help="current.txt")
2238    parser.add_argument("previous.txt", nargs='?', type=argparse.FileType('r'), default=None,
2239            help="previous.txt")
2240    args = vars(parser.parse_args())
2241
2242    if args['no_color']:
2243        USE_COLOR = False
2244    elif args['color']:
2245        USE_COLOR = True
2246    else:
2247        USE_COLOR = sys.stdout.isatty()
2248
2249    if args['allow_google']:
2250        ALLOW_GOOGLE = True
2251
2252    current_file = args['current.txt']
2253    base_current_file = args['base_current']
2254    previous_file = args['previous.txt']
2255    base_previous_file = args['base_previous']
2256    filters = args['filter']
2257    if not filters:
2258        filters = []
2259    title = args['title']
2260    if not title:
2261        title = current_file.name
2262
2263    if args['show_deprecations_at_birth']:
2264        with current_file as f:
2265            cur = _parse_stream(f)
2266        with previous_file as f:
2267            prev = _parse_stream(f)
2268        show_deprecations_at_birth(cur, prev)
2269        sys.exit()
2270
2271    if args['show_stats']:
2272        with current_file as f:
2273            cur = _parse_stream(f)
2274        with previous_file as f:
2275            prev = _parse_stream(f)
2276        show_stats(cur, prev)
2277        sys.exit()
2278
2279    classes_with_base = []
2280
2281    with current_file as f:
2282        if base_current_file:
2283            with base_current_file as base_f:
2284                cur_fail, cur_noticed = examine_stream(f, base_f,
2285                                                       out_classes_with_base=classes_with_base)
2286        else:
2287            cur_fail, cur_noticed = examine_stream(f, out_classes_with_base=classes_with_base)
2288
2289    if not previous_file is None:
2290        with previous_file as f:
2291            if base_previous_file:
2292                with base_previous_file as base_f:
2293                    prev_fail, prev_noticed = examine_stream(f, base_f,
2294                                                             in_classes_with_base=classes_with_base)
2295            else:
2296                prev_fail, prev_noticed = examine_stream(f, in_classes_with_base=classes_with_base)
2297
2298        # ignore errors from previous API level
2299        for p in prev_fail:
2300            if p in cur_fail:
2301                del cur_fail[p]
2302
2303        # ignore classes unchanged from previous API level
2304        for k, v in prev_noticed.iteritems():
2305            if k in cur_noticed and v == cur_noticed[k]:
2306                del cur_noticed[k]
2307
2308        """
2309        # NOTE: disabled because of memory pressure
2310        # look for compatibility issues
2311        compat_fail = verify_compat(cur, prev)
2312
2313        print "%s API compatibility issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
2314        for f in sorted(compat_fail):
2315            print compat_fail[f]
2316            print
2317        """
2318
2319    # ignore everything but the given filters, if provided
2320    if filters:
2321        cur_fail = dict([(key, failure) for key, failure in cur_fail.iteritems()
2322                if match_filter(filters, failure.clazz.fullname)])
2323
2324    if args['show_noticed'] and len(cur_noticed) != 0:
2325        print "%s API changes noticed %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
2326        for f in sorted(cur_noticed.keys()):
2327            print f
2328        print
2329
2330    if len(cur_fail) != 0:
2331        print "%s API style issues: %s %s" % ((format(fg=WHITE, bg=BLUE, bold=True),
2332                    title, format(reset=True)))
2333        for f in filters:
2334            print "%s   filter: %s %s" % ((format(fg=WHITE, bg=BLUE, bold=True),
2335                        f, format(reset=True)))
2336        print
2337        for f in sorted(cur_fail):
2338            print cur_fail[f]
2339            print
2340        print "%d errors" % len(cur_fail)
2341        sys.exit(77)
2342
2343if __name__ == "__main__":
2344    try:
2345        main()
2346    except KeyboardInterrupt:
2347        sys.exit(1)
2348