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