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