1#!/usr/bin/env python 2 3# Copyright (C) 2014 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the 'License'); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an 'AS IS' BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17""" 18Enforces common Android public API design patterns. It ignores lint messages from 19a previous API level, if provided. 20 21Usage: apilint.py current.txt 22Usage: apilint.py current.txt previous.txt 23 24You can also splice in blame details like this: 25$ git blame api/current.txt -t -e > /tmp/currentblame.txt 26$ apilint.py /tmp/currentblame.txt previous.txt --no-color 27""" 28 29import re, sys, collections, traceback, argparse 30 31 32BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) 33 34ALLOW_GOOGLE = False 35USE_COLOR = True 36 37def format(fg=None, bg=None, bright=False, bold=False, dim=False, reset=False): 38 # manually derived from http://en.wikipedia.org/wiki/ANSI_escape_code#Codes 39 if not USE_COLOR: return "" 40 codes = [] 41 if reset: codes.append("0") 42 else: 43 if not fg is None: codes.append("3%d" % (fg)) 44 if not bg is None: 45 if not bright: codes.append("4%d" % (bg)) 46 else: codes.append("10%d" % (bg)) 47 if bold: codes.append("1") 48 elif dim: codes.append("2") 49 else: codes.append("22") 50 return "\033[%sm" % (";".join(codes)) 51 52 53class Field(): 54 def __init__(self, clazz, line, raw, blame): 55 self.clazz = clazz 56 self.line = line 57 self.raw = raw.strip(" {;") 58 self.blame = blame 59 60 raw = raw.split() 61 self.split = list(raw) 62 63 for r in ["field", "volatile", "transient", "public", "protected", "static", "final", "deprecated"]: 64 while r in raw: raw.remove(r) 65 66 self.typ = raw[0] 67 self.name = raw[1].strip(";") 68 if len(raw) >= 4 and raw[2] == "=": 69 self.value = raw[3].strip(';"') 70 else: 71 self.value = None 72 73 self.ident = self.raw.replace(" deprecated ", " ") 74 75 def __repr__(self): 76 return self.raw 77 78 79class Method(): 80 def __init__(self, clazz, line, raw, blame): 81 self.clazz = clazz 82 self.line = line 83 self.raw = raw.strip(" {;") 84 self.blame = blame 85 86 # drop generics for now 87 raw = re.sub("<.+?>", "", raw) 88 89 raw = re.split("[\s(),;]+", raw) 90 for r in ["", ";"]: 91 while r in raw: raw.remove(r) 92 self.split = list(raw) 93 94 for r in ["method", "public", "protected", "static", "final", "deprecated", "abstract", "default"]: 95 while r in raw: raw.remove(r) 96 97 self.typ = raw[0] 98 self.name = raw[1] 99 self.args = [] 100 for r in raw[2:]: 101 if r == "throws": break 102 self.args.append(r) 103 104 # identity for compat purposes 105 ident = self.raw 106 ident = ident.replace(" deprecated ", " ") 107 ident = ident.replace(" synchronized ", " ") 108 ident = re.sub("<.+?>", "", ident) 109 if " throws " in ident: 110 ident = ident[:ident.index(" throws ")] 111 self.ident = ident 112 113 def __repr__(self): 114 return self.raw 115 116 117class Class(): 118 def __init__(self, pkg, line, raw, blame): 119 self.pkg = pkg 120 self.line = line 121 self.raw = raw.strip(" {;") 122 self.blame = blame 123 self.ctors = [] 124 self.fields = [] 125 self.methods = [] 126 127 raw = raw.split() 128 self.split = list(raw) 129 if "class" in raw: 130 self.fullname = raw[raw.index("class")+1] 131 elif "interface" in raw: 132 self.fullname = raw[raw.index("interface")+1] 133 else: 134 raise ValueError("Funky class type %s" % (self.raw)) 135 136 if "extends" in raw: 137 self.extends = raw[raw.index("extends")+1] 138 self.extends_path = self.extends.split(".") 139 else: 140 self.extends = None 141 self.extends_path = [] 142 143 self.fullname = self.pkg.name + "." + self.fullname 144 self.fullname_path = self.fullname.split(".") 145 146 self.name = self.fullname[self.fullname.rindex(".")+1:] 147 148 def __repr__(self): 149 return self.raw 150 151 152class Package(): 153 def __init__(self, line, raw, blame): 154 self.line = line 155 self.raw = raw.strip(" {;") 156 self.blame = blame 157 158 raw = raw.split() 159 self.name = raw[raw.index("package")+1] 160 self.name_path = self.name.split(".") 161 162 def __repr__(self): 163 return self.raw 164 165 166def _parse_stream(f, clazz_cb=None): 167 line = 0 168 api = {} 169 pkg = None 170 clazz = None 171 blame = None 172 173 re_blame = re.compile("^([a-z0-9]{7,}) \(<([^>]+)>.+?\) (.+?)$") 174 for raw in f: 175 line += 1 176 raw = raw.rstrip() 177 match = re_blame.match(raw) 178 if match is not None: 179 blame = match.groups()[0:2] 180 raw = match.groups()[2] 181 else: 182 blame = None 183 184 if raw.startswith("package"): 185 pkg = Package(line, raw, blame) 186 elif raw.startswith(" ") and raw.endswith("{"): 187 # When provided with class callback, we treat as incremental 188 # parse and don't build up entire API 189 if clazz and clazz_cb: 190 clazz_cb(clazz) 191 clazz = Class(pkg, line, raw, blame) 192 if not clazz_cb: 193 api[clazz.fullname] = clazz 194 elif raw.startswith(" ctor"): 195 clazz.ctors.append(Method(clazz, line, raw, blame)) 196 elif raw.startswith(" method"): 197 clazz.methods.append(Method(clazz, line, raw, blame)) 198 elif raw.startswith(" field"): 199 clazz.fields.append(Field(clazz, line, raw, blame)) 200 201 # Handle last trailing class 202 if clazz and clazz_cb: 203 clazz_cb(clazz) 204 205 return api 206 207 208class Failure(): 209 def __init__(self, sig, clazz, detail, error, rule, msg): 210 self.sig = sig 211 self.error = error 212 self.rule = rule 213 self.msg = msg 214 215 if error: 216 self.head = "Error %s" % (rule) if rule else "Error" 217 dump = "%s%s:%s %s" % (format(fg=RED, bg=BLACK, bold=True), self.head, format(reset=True), msg) 218 else: 219 self.head = "Warning %s" % (rule) if rule else "Warning" 220 dump = "%s%s:%s %s" % (format(fg=YELLOW, bg=BLACK, bold=True), self.head, format(reset=True), msg) 221 222 self.line = clazz.line 223 blame = clazz.blame 224 if detail is not None: 225 dump += "\n in " + repr(detail) 226 self.line = detail.line 227 blame = detail.blame 228 dump += "\n in " + repr(clazz) 229 dump += "\n in " + repr(clazz.pkg) 230 dump += "\n at line " + repr(self.line) 231 if blame is not None: 232 dump += "\n last modified by %s in %s" % (blame[1], blame[0]) 233 234 self.dump = dump 235 236 def __repr__(self): 237 return self.dump 238 239 240failures = {} 241 242def _fail(clazz, detail, error, rule, msg): 243 """Records an API failure to be processed later.""" 244 global failures 245 246 sig = "%s-%s-%s" % (clazz.fullname, repr(detail), msg) 247 sig = sig.replace(" deprecated ", " ") 248 249 failures[sig] = Failure(sig, clazz, detail, error, rule, msg) 250 251 252def warn(clazz, detail, rule, msg): 253 _fail(clazz, detail, False, rule, msg) 254 255def error(clazz, detail, rule, msg): 256 _fail(clazz, detail, True, rule, msg) 257 258 259def verify_constants(clazz): 260 """All static final constants must be FOO_NAME style.""" 261 if re.match("android\.R\.[a-z]+", clazz.fullname): return 262 if clazz.fullname.startswith("android.os.Build"): return 263 if clazz.fullname == "android.system.OsConstants": return 264 265 req = ["java.lang.String","byte","short","int","long","float","double","boolean","char"] 266 for f in clazz.fields: 267 if "static" in f.split and "final" in f.split: 268 if re.match("[A-Z0-9_]+", f.name) is None: 269 error(clazz, f, "C2", "Constant field names must be FOO_NAME") 270 if f.typ != "java.lang.String": 271 if f.name.startswith("MIN_") or f.name.startswith("MAX_"): 272 warn(clazz, f, "C8", "If min/max could change in future, make them dynamic methods") 273 if f.typ in req and f.value is None: 274 error(clazz, f, None, "All constants must be defined at compile time") 275 276 277def verify_enums(clazz): 278 """Enums are bad, mmkay?""" 279 if "extends java.lang.Enum" in clazz.raw: 280 error(clazz, None, "F5", "Enums are not allowed") 281 282 283def verify_class_names(clazz): 284 """Try catching malformed class names like myMtp or MTPUser.""" 285 if clazz.fullname.startswith("android.opengl"): return 286 if clazz.fullname.startswith("android.renderscript"): return 287 if re.match("android\.R\.[a-z]+", clazz.fullname): return 288 289 if re.search("[A-Z]{2,}", clazz.name) is not None: 290 warn(clazz, None, "S1", "Class names with acronyms should be Mtp not MTP") 291 if re.match("[^A-Z]", clazz.name): 292 error(clazz, None, "S1", "Class must start with uppercase char") 293 294 295def verify_method_names(clazz): 296 """Try catching malformed method names, like Foo() or getMTU().""" 297 if clazz.fullname.startswith("android.opengl"): return 298 if clazz.fullname.startswith("android.renderscript"): return 299 if clazz.fullname == "android.system.OsConstants": return 300 301 for m in clazz.methods: 302 if re.search("[A-Z]{2,}", m.name) is not None: 303 warn(clazz, m, "S1", "Method names with acronyms should be getMtu() instead of getMTU()") 304 if re.match("[^a-z]", m.name): 305 error(clazz, m, "S1", "Method name must start with lowercase char") 306 307 308def verify_callbacks(clazz): 309 """Verify Callback classes. 310 All callback classes must be abstract. 311 All methods must follow onFoo() naming style.""" 312 if clazz.fullname == "android.speech.tts.SynthesisCallback": return 313 314 if clazz.name.endswith("Callbacks"): 315 error(clazz, None, "L1", "Callback class names should be singular") 316 if clazz.name.endswith("Observer"): 317 warn(clazz, None, "L1", "Class should be named FooCallback") 318 319 if clazz.name.endswith("Callback"): 320 if "interface" in clazz.split: 321 error(clazz, None, "CL3", "Callbacks must be abstract class to enable extension in future API levels") 322 323 for m in clazz.methods: 324 if not re.match("on[A-Z][a-z]*", m.name): 325 error(clazz, m, "L1", "Callback method names must be onFoo() style") 326 327 328def verify_listeners(clazz): 329 """Verify Listener classes. 330 All Listener classes must be interface. 331 All methods must follow onFoo() naming style. 332 If only a single method, it must match class name: 333 interface OnFooListener { void onFoo() }""" 334 335 if clazz.name.endswith("Listener"): 336 if " abstract class " in clazz.raw: 337 error(clazz, None, "L1", "Listeners should be an interface, or otherwise renamed Callback") 338 339 for m in clazz.methods: 340 if not re.match("on[A-Z][a-z]*", m.name): 341 error(clazz, m, "L1", "Listener method names must be onFoo() style") 342 343 if len(clazz.methods) == 1 and clazz.name.startswith("On"): 344 m = clazz.methods[0] 345 if (m.name + "Listener").lower() != clazz.name.lower(): 346 error(clazz, m, "L1", "Single listener method name must match class name") 347 348 349def verify_actions(clazz): 350 """Verify intent actions. 351 All action names must be named ACTION_FOO. 352 All action values must be scoped by package and match name: 353 package android.foo { 354 String ACTION_BAR = "android.foo.action.BAR"; 355 }""" 356 for f in clazz.fields: 357 if f.value is None: continue 358 if f.name.startswith("EXTRA_"): continue 359 if f.name == "SERVICE_INTERFACE" or f.name == "PROVIDER_INTERFACE": continue 360 if "INTERACTION" in f.name: continue 361 362 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String": 363 if "_ACTION" in f.name or "ACTION_" in f.name or ".action." in f.value.lower(): 364 if not f.name.startswith("ACTION_"): 365 error(clazz, f, "C3", "Intent action constant name must be ACTION_FOO") 366 else: 367 if clazz.fullname == "android.content.Intent": 368 prefix = "android.intent.action" 369 elif clazz.fullname == "android.provider.Settings": 370 prefix = "android.settings" 371 elif clazz.fullname == "android.app.admin.DevicePolicyManager" or clazz.fullname == "android.app.admin.DeviceAdminReceiver": 372 prefix = "android.app.action" 373 else: 374 prefix = clazz.pkg.name + ".action" 375 expected = prefix + "." + f.name[7:] 376 if f.value != expected: 377 error(clazz, f, "C4", "Inconsistent action value; expected %s" % (expected)) 378 379 380def verify_extras(clazz): 381 """Verify intent extras. 382 All extra names must be named EXTRA_FOO. 383 All extra values must be scoped by package and match name: 384 package android.foo { 385 String EXTRA_BAR = "android.foo.extra.BAR"; 386 }""" 387 if clazz.fullname == "android.app.Notification": return 388 if clazz.fullname == "android.appwidget.AppWidgetManager": return 389 390 for f in clazz.fields: 391 if f.value is None: continue 392 if f.name.startswith("ACTION_"): continue 393 394 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String": 395 if "_EXTRA" in f.name or "EXTRA_" in f.name or ".extra" in f.value.lower(): 396 if not f.name.startswith("EXTRA_"): 397 error(clazz, f, "C3", "Intent extra must be EXTRA_FOO") 398 else: 399 if clazz.pkg.name == "android.content" and clazz.name == "Intent": 400 prefix = "android.intent.extra" 401 elif clazz.pkg.name == "android.app.admin": 402 prefix = "android.app.extra" 403 else: 404 prefix = clazz.pkg.name + ".extra" 405 expected = prefix + "." + f.name[6:] 406 if f.value != expected: 407 error(clazz, f, "C4", "Inconsistent extra value; expected %s" % (expected)) 408 409 410def verify_equals(clazz): 411 """Verify that equals() and hashCode() must be overridden together.""" 412 eq = False 413 hc = False 414 for m in clazz.methods: 415 if " static " in m.raw: continue 416 if "boolean equals(java.lang.Object)" in m.raw: eq = True 417 if "int hashCode()" in m.raw: hc = True 418 if eq != hc: 419 error(clazz, None, "M8", "Must override both equals and hashCode; missing one") 420 421 422def verify_parcelable(clazz): 423 """Verify that Parcelable objects aren't hiding required bits.""" 424 if "implements android.os.Parcelable" in clazz.raw: 425 creator = [ i for i in clazz.fields if i.name == "CREATOR" ] 426 write = [ i for i in clazz.methods if i.name == "writeToParcel" ] 427 describe = [ i for i in clazz.methods if i.name == "describeContents" ] 428 429 if len(creator) == 0 or len(write) == 0 or len(describe) == 0: 430 error(clazz, None, "FW3", "Parcelable requires CREATOR, writeToParcel, and describeContents; missing one") 431 432 if " final class " not in clazz.raw: 433 error(clazz, None, "FW8", "Parcelable classes must be final") 434 435 436def verify_protected(clazz): 437 """Verify that no protected methods or fields are allowed.""" 438 for m in clazz.methods: 439 if "protected" in m.split: 440 error(clazz, m, "M7", "Protected methods not allowed; must be public") 441 for f in clazz.fields: 442 if "protected" in f.split: 443 error(clazz, f, "M7", "Protected fields not allowed; must be public") 444 445 446def verify_fields(clazz): 447 """Verify that all exposed fields are final. 448 Exposed fields must follow myName style. 449 Catch internal mFoo objects being exposed.""" 450 451 IGNORE_BARE_FIELDS = [ 452 "android.app.ActivityManager.RecentTaskInfo", 453 "android.app.Notification", 454 "android.content.pm.ActivityInfo", 455 "android.content.pm.ApplicationInfo", 456 "android.content.pm.ComponentInfo", 457 "android.content.pm.ResolveInfo", 458 "android.content.pm.FeatureGroupInfo", 459 "android.content.pm.InstrumentationInfo", 460 "android.content.pm.PackageInfo", 461 "android.content.pm.PackageItemInfo", 462 "android.content.res.Configuration", 463 "android.graphics.BitmapFactory.Options", 464 "android.os.Message", 465 "android.system.StructPollfd", 466 ] 467 468 for f in clazz.fields: 469 if not "final" in f.split: 470 if clazz.fullname in IGNORE_BARE_FIELDS: 471 pass 472 elif clazz.fullname.endswith("LayoutParams"): 473 pass 474 elif clazz.fullname.startswith("android.util.Mutable"): 475 pass 476 else: 477 error(clazz, f, "F2", "Bare fields must be marked final, or add accessors if mutable") 478 479 if not "static" in f.split: 480 if not re.match("[a-z]([a-zA-Z]+)?", f.name): 481 error(clazz, f, "S1", "Non-static fields must be named using myField style") 482 483 if re.match("[ms][A-Z]", f.name): 484 error(clazz, f, "F1", "Internal objects must not be exposed") 485 486 if re.match("[A-Z_]+", f.name): 487 if "static" not in f.split or "final" not in f.split: 488 error(clazz, f, "C2", "Constants must be marked static final") 489 490 491def verify_register(clazz): 492 """Verify parity of registration methods. 493 Callback objects use register/unregister methods. 494 Listener objects use add/remove methods.""" 495 methods = [ m.name for m in clazz.methods ] 496 for m in clazz.methods: 497 if "Callback" in m.raw: 498 if m.name.startswith("register"): 499 other = "unregister" + m.name[8:] 500 if other not in methods: 501 error(clazz, m, "L2", "Missing unregister method") 502 if m.name.startswith("unregister"): 503 other = "register" + m.name[10:] 504 if other not in methods: 505 error(clazz, m, "L2", "Missing register method") 506 507 if m.name.startswith("add") or m.name.startswith("remove"): 508 error(clazz, m, "L3", "Callback methods should be named register/unregister") 509 510 if "Listener" in m.raw: 511 if m.name.startswith("add"): 512 other = "remove" + m.name[3:] 513 if other not in methods: 514 error(clazz, m, "L2", "Missing remove method") 515 if m.name.startswith("remove") and not m.name.startswith("removeAll"): 516 other = "add" + m.name[6:] 517 if other not in methods: 518 error(clazz, m, "L2", "Missing add method") 519 520 if m.name.startswith("register") or m.name.startswith("unregister"): 521 error(clazz, m, "L3", "Listener methods should be named add/remove") 522 523 524def verify_sync(clazz): 525 """Verify synchronized methods aren't exposed.""" 526 for m in clazz.methods: 527 if "synchronized" in m.split: 528 error(clazz, m, "M5", "Internal locks must not be exposed") 529 530 531def verify_intent_builder(clazz): 532 """Verify that Intent builders are createFooIntent() style.""" 533 if clazz.name == "Intent": return 534 535 for m in clazz.methods: 536 if m.typ == "android.content.Intent": 537 if m.name.startswith("create") and m.name.endswith("Intent"): 538 pass 539 else: 540 warn(clazz, m, "FW1", "Methods creating an Intent should be named createFooIntent()") 541 542 543def verify_helper_classes(clazz): 544 """Verify that helper classes are named consistently with what they extend. 545 All developer extendable methods should be named onFoo().""" 546 test_methods = False 547 if "extends android.app.Service" in clazz.raw: 548 test_methods = True 549 if not clazz.name.endswith("Service"): 550 error(clazz, None, "CL4", "Inconsistent class name; should be FooService") 551 552 found = False 553 for f in clazz.fields: 554 if f.name == "SERVICE_INTERFACE": 555 found = True 556 if f.value != clazz.fullname: 557 error(clazz, f, "C4", "Inconsistent interface constant; expected %s" % (clazz.fullname)) 558 559 if "extends android.content.ContentProvider" in clazz.raw: 560 test_methods = True 561 if not clazz.name.endswith("Provider"): 562 error(clazz, None, "CL4", "Inconsistent class name; should be FooProvider") 563 564 found = False 565 for f in clazz.fields: 566 if f.name == "PROVIDER_INTERFACE": 567 found = True 568 if f.value != clazz.fullname: 569 error(clazz, f, "C4", "Inconsistent interface constant; expected %s" % (clazz.fullname)) 570 571 if "extends android.content.BroadcastReceiver" in clazz.raw: 572 test_methods = True 573 if not clazz.name.endswith("Receiver"): 574 error(clazz, None, "CL4", "Inconsistent class name; should be FooReceiver") 575 576 if "extends android.app.Activity" in clazz.raw: 577 test_methods = True 578 if not clazz.name.endswith("Activity"): 579 error(clazz, None, "CL4", "Inconsistent class name; should be FooActivity") 580 581 if test_methods: 582 for m in clazz.methods: 583 if "final" in m.split: continue 584 if not re.match("on[A-Z]", m.name): 585 if "abstract" in m.split: 586 warn(clazz, m, None, "Methods implemented by developers should be named onFoo()") 587 else: 588 warn(clazz, m, None, "If implemented by developer, should be named onFoo(); otherwise consider marking final") 589 590 591def verify_builder(clazz): 592 """Verify builder classes. 593 Methods should return the builder to enable chaining.""" 594 if " extends " in clazz.raw: return 595 if not clazz.name.endswith("Builder"): return 596 597 if clazz.name != "Builder": 598 warn(clazz, None, None, "Builder should be defined as inner class") 599 600 has_build = False 601 for m in clazz.methods: 602 if m.name == "build": 603 has_build = True 604 continue 605 606 if m.name.startswith("get"): continue 607 if m.name.startswith("clear"): continue 608 609 if m.name.startswith("with"): 610 warn(clazz, m, None, "Builder methods names should use setFoo() style") 611 612 if m.name.startswith("set"): 613 if not m.typ.endswith(clazz.fullname): 614 warn(clazz, m, "M4", "Methods must return the builder object") 615 616 if not has_build: 617 warn(clazz, None, None, "Missing build() method") 618 619 620def verify_aidl(clazz): 621 """Catch people exposing raw AIDL.""" 622 if "extends android.os.Binder" in clazz.raw or "implements android.os.IInterface" in clazz.raw: 623 error(clazz, None, None, "Raw AIDL interfaces must not be exposed") 624 625 626def verify_internal(clazz): 627 """Catch people exposing internal classes.""" 628 if clazz.pkg.name.startswith("com.android"): 629 error(clazz, None, None, "Internal classes must not be exposed") 630 631 632def verify_layering(clazz): 633 """Catch package layering violations. 634 For example, something in android.os depending on android.app.""" 635 ranking = [ 636 ["android.service","android.accessibilityservice","android.inputmethodservice","android.printservice","android.appwidget","android.webkit","android.preference","android.gesture","android.print"], 637 "android.app", 638 "android.widget", 639 "android.view", 640 "android.animation", 641 "android.provider", 642 ["android.content","android.graphics.drawable"], 643 "android.database", 644 "android.graphics", 645 "android.text", 646 "android.os", 647 "android.util" 648 ] 649 650 def rank(p): 651 for i in range(len(ranking)): 652 if isinstance(ranking[i], list): 653 for j in ranking[i]: 654 if p.startswith(j): return i 655 else: 656 if p.startswith(ranking[i]): return i 657 658 cr = rank(clazz.pkg.name) 659 if cr is None: return 660 661 for f in clazz.fields: 662 ir = rank(f.typ) 663 if ir and ir < cr: 664 warn(clazz, f, "FW6", "Field type violates package layering") 665 666 for m in clazz.methods: 667 ir = rank(m.typ) 668 if ir and ir < cr: 669 warn(clazz, m, "FW6", "Method return type violates package layering") 670 for arg in m.args: 671 ir = rank(arg) 672 if ir and ir < cr: 673 warn(clazz, m, "FW6", "Method argument type violates package layering") 674 675 676def verify_boolean(clazz): 677 """Verifies that boolean accessors are named correctly. 678 For example, hasFoo() and setHasFoo().""" 679 680 def is_get(m): return len(m.args) == 0 and m.typ == "boolean" 681 def is_set(m): return len(m.args) == 1 and m.args[0] == "boolean" 682 683 gets = [ m for m in clazz.methods if is_get(m) ] 684 sets = [ m for m in clazz.methods if is_set(m) ] 685 686 def error_if_exists(methods, trigger, expected, actual): 687 for m in methods: 688 if m.name == actual: 689 error(clazz, m, "M6", "Symmetric method for %s must be named %s" % (trigger, expected)) 690 691 for m in clazz.methods: 692 if is_get(m): 693 if re.match("is[A-Z]", m.name): 694 target = m.name[2:] 695 expected = "setIs" + target 696 error_if_exists(sets, m.name, expected, "setHas" + target) 697 elif re.match("has[A-Z]", m.name): 698 target = m.name[3:] 699 expected = "setHas" + target 700 error_if_exists(sets, m.name, expected, "setIs" + target) 701 error_if_exists(sets, m.name, expected, "set" + target) 702 elif re.match("get[A-Z]", m.name): 703 target = m.name[3:] 704 expected = "set" + target 705 error_if_exists(sets, m.name, expected, "setIs" + target) 706 error_if_exists(sets, m.name, expected, "setHas" + target) 707 708 if is_set(m): 709 if re.match("set[A-Z]", m.name): 710 target = m.name[3:] 711 expected = "get" + target 712 error_if_exists(sets, m.name, expected, "is" + target) 713 error_if_exists(sets, m.name, expected, "has" + target) 714 715 716def verify_collections(clazz): 717 """Verifies that collection types are interfaces.""" 718 if clazz.fullname == "android.os.Bundle": return 719 720 bad = ["java.util.Vector", "java.util.LinkedList", "java.util.ArrayList", "java.util.Stack", 721 "java.util.HashMap", "java.util.HashSet", "android.util.ArraySet", "android.util.ArrayMap"] 722 for m in clazz.methods: 723 if m.typ in bad: 724 error(clazz, m, "CL2", "Return type is concrete collection; must be higher-level interface") 725 for arg in m.args: 726 if arg in bad: 727 error(clazz, m, "CL2", "Argument is concrete collection; must be higher-level interface") 728 729 730def verify_flags(clazz): 731 """Verifies that flags are non-overlapping.""" 732 known = collections.defaultdict(int) 733 for f in clazz.fields: 734 if "FLAG_" in f.name: 735 try: 736 val = int(f.value) 737 except: 738 continue 739 740 scope = f.name[0:f.name.index("FLAG_")] 741 if val & known[scope]: 742 warn(clazz, f, "C1", "Found overlapping flag constant value") 743 known[scope] |= val 744 745 746def verify_exception(clazz): 747 """Verifies that methods don't throw generic exceptions.""" 748 for m in clazz.methods: 749 if "throws java.lang.Exception" in m.raw or "throws java.lang.Throwable" in m.raw or "throws java.lang.Error" in m.raw: 750 error(clazz, m, "S1", "Methods must not throw generic exceptions") 751 752 if "throws android.os.RemoteException" in m.raw: 753 if clazz.name == "android.content.ContentProviderClient": continue 754 if clazz.name == "android.os.Binder": continue 755 if clazz.name == "android.os.IBinder": continue 756 757 error(clazz, m, "FW9", "Methods calling into system server should rethrow RemoteException as RuntimeException") 758 759 760def verify_google(clazz): 761 """Verifies that APIs never reference Google.""" 762 763 if re.search("google", clazz.raw, re.IGNORECASE): 764 error(clazz, None, None, "Must never reference Google") 765 766 test = [] 767 test.extend(clazz.ctors) 768 test.extend(clazz.fields) 769 test.extend(clazz.methods) 770 771 for t in test: 772 if re.search("google", t.raw, re.IGNORECASE): 773 error(clazz, t, None, "Must never reference Google") 774 775 776def verify_bitset(clazz): 777 """Verifies that we avoid using heavy BitSet.""" 778 779 for f in clazz.fields: 780 if f.typ == "java.util.BitSet": 781 error(clazz, f, None, "Field type must not be heavy BitSet") 782 783 for m in clazz.methods: 784 if m.typ == "java.util.BitSet": 785 error(clazz, m, None, "Return type must not be heavy BitSet") 786 for arg in m.args: 787 if arg == "java.util.BitSet": 788 error(clazz, m, None, "Argument type must not be heavy BitSet") 789 790 791def verify_manager(clazz): 792 """Verifies that FooManager is only obtained from Context.""" 793 794 if not clazz.name.endswith("Manager"): return 795 796 for c in clazz.ctors: 797 error(clazz, c, None, "Managers must always be obtained from Context; no direct constructors") 798 799 for m in clazz.methods: 800 if m.typ == clazz.fullname: 801 error(clazz, m, None, "Managers must always be obtained from Context") 802 803 804def verify_boxed(clazz): 805 """Verifies that methods avoid boxed primitives.""" 806 807 boxed = ["java.lang.Number","java.lang.Byte","java.lang.Double","java.lang.Float","java.lang.Integer","java.lang.Long","java.lang.Short"] 808 809 for c in clazz.ctors: 810 for arg in c.args: 811 if arg in boxed: 812 error(clazz, c, "M11", "Must avoid boxed primitives") 813 814 for f in clazz.fields: 815 if f.typ in boxed: 816 error(clazz, f, "M11", "Must avoid boxed primitives") 817 818 for m in clazz.methods: 819 if m.typ in boxed: 820 error(clazz, m, "M11", "Must avoid boxed primitives") 821 for arg in m.args: 822 if arg in boxed: 823 error(clazz, m, "M11", "Must avoid boxed primitives") 824 825 826def verify_static_utils(clazz): 827 """Verifies that helper classes can't be constructed.""" 828 if clazz.fullname.startswith("android.opengl"): return 829 if clazz.fullname.startswith("android.R"): return 830 831 # Only care about classes with default constructors 832 if len(clazz.ctors) == 1 and len(clazz.ctors[0].args) == 0: 833 test = [] 834 test.extend(clazz.fields) 835 test.extend(clazz.methods) 836 837 if len(test) == 0: return 838 for t in test: 839 if "static" not in t.split: 840 return 841 842 error(clazz, None, None, "Fully-static utility classes must not have constructor") 843 844 845def verify_overload_args(clazz): 846 """Verifies that method overloads add new arguments at the end.""" 847 if clazz.fullname.startswith("android.opengl"): return 848 849 overloads = collections.defaultdict(list) 850 for m in clazz.methods: 851 if "deprecated" in m.split: continue 852 overloads[m.name].append(m) 853 854 for name, methods in overloads.items(): 855 if len(methods) <= 1: continue 856 857 # Look for arguments common across all overloads 858 def cluster(args): 859 count = collections.defaultdict(int) 860 res = set() 861 for i in range(len(args)): 862 a = args[i] 863 res.add("%s#%d" % (a, count[a])) 864 count[a] += 1 865 return res 866 867 common_args = cluster(methods[0].args) 868 for m in methods: 869 common_args = common_args & cluster(m.args) 870 871 if len(common_args) == 0: continue 872 873 # Require that all common arguments are present at start of signature 874 locked_sig = None 875 for m in methods: 876 sig = m.args[0:len(common_args)] 877 if not common_args.issubset(cluster(sig)): 878 warn(clazz, m, "M2", "Expected common arguments [%s] at beginning of overloaded method" % (", ".join(common_args))) 879 elif not locked_sig: 880 locked_sig = sig 881 elif locked_sig != sig: 882 error(clazz, m, "M2", "Expected consistent argument ordering between overloads: %s..." % (", ".join(locked_sig))) 883 884 885def verify_callback_handlers(clazz): 886 """Verifies that methods adding listener/callback have overload 887 for specifying delivery thread.""" 888 889 # Ignore UI packages which assume main thread 890 skip = [ 891 "animation", 892 "view", 893 "graphics", 894 "transition", 895 "widget", 896 "webkit", 897 ] 898 for s in skip: 899 if s in clazz.pkg.name_path: return 900 if s in clazz.extends_path: return 901 902 # Ignore UI classes which assume main thread 903 if "app" in clazz.pkg.name_path or "app" in clazz.extends_path: 904 for s in ["ActionBar","Dialog","Application","Activity","Fragment","Loader"]: 905 if s in clazz.fullname: return 906 if "content" in clazz.pkg.name_path or "content" in clazz.extends_path: 907 for s in ["Loader"]: 908 if s in clazz.fullname: return 909 910 found = {} 911 by_name = collections.defaultdict(list) 912 for m in clazz.methods: 913 if m.name.startswith("unregister"): continue 914 if m.name.startswith("remove"): continue 915 if re.match("on[A-Z]+", m.name): continue 916 917 by_name[m.name].append(m) 918 919 for a in m.args: 920 if a.endswith("Listener") or a.endswith("Callback") or a.endswith("Callbacks"): 921 found[m.name] = m 922 923 for f in found.values(): 924 takes_handler = False 925 for m in by_name[f.name]: 926 if "android.os.Handler" in m.args: 927 takes_handler = True 928 if not takes_handler: 929 warn(clazz, f, "L1", "Registration methods should have overload that accepts delivery Handler") 930 931 932def verify_context_first(clazz): 933 """Verifies that methods accepting a Context keep it the first argument.""" 934 examine = clazz.ctors + clazz.methods 935 for m in examine: 936 if len(m.args) > 1 and m.args[0] != "android.content.Context": 937 if "android.content.Context" in m.args[1:]: 938 error(clazz, m, "M3", "Context is distinct, so it must be the first argument") 939 if len(m.args) > 1 and m.args[0] != "android.content.ContentResolver": 940 if "android.content.ContentResolver" in m.args[1:]: 941 error(clazz, m, "M3", "ContentResolver is distinct, so it must be the first argument") 942 943 944def verify_listener_last(clazz): 945 """Verifies that methods accepting a Listener or Callback keep them as last arguments.""" 946 examine = clazz.ctors + clazz.methods 947 for m in examine: 948 if "Listener" in m.name or "Callback" in m.name: continue 949 found = False 950 for a in m.args: 951 if a.endswith("Callback") or a.endswith("Callbacks") or a.endswith("Listener"): 952 found = True 953 elif found and a != "android.os.Handler": 954 warn(clazz, m, "M3", "Listeners should always be at end of argument list") 955 956 957def verify_resource_names(clazz): 958 """Verifies that resource names have consistent case.""" 959 if not re.match("android\.R\.[a-z]+", clazz.fullname): return 960 961 # Resources defined by files are foo_bar_baz 962 if clazz.name in ["anim","animator","color","dimen","drawable","interpolator","layout","transition","menu","mipmap","string","plurals","raw","xml"]: 963 for f in clazz.fields: 964 if re.match("[a-z1-9_]+$", f.name): continue 965 error(clazz, f, None, "Expected resource name in this class to be foo_bar_baz style") 966 967 # Resources defined inside files are fooBarBaz 968 if clazz.name in ["array","attr","id","bool","fraction","integer"]: 969 for f in clazz.fields: 970 if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue 971 if re.match("layout_[a-z][a-zA-Z1-9]*$", f.name): continue 972 if re.match("state_[a-z_]*$", f.name): continue 973 974 if re.match("[a-z][a-zA-Z1-9]*$", f.name): continue 975 error(clazz, f, "C7", "Expected resource name in this class to be fooBarBaz style") 976 977 # Styles are FooBar_Baz 978 if clazz.name in ["style"]: 979 for f in clazz.fields: 980 if re.match("[A-Z][A-Za-z1-9]+(_[A-Z][A-Za-z1-9]+?)*$", f.name): continue 981 error(clazz, f, "C7", "Expected resource name in this class to be FooBar_Baz style") 982 983 984def verify_files(clazz): 985 """Verifies that methods accepting File also accept streams.""" 986 987 has_file = set() 988 has_stream = set() 989 990 test = [] 991 test.extend(clazz.ctors) 992 test.extend(clazz.methods) 993 994 for m in test: 995 if "java.io.File" in m.args: 996 has_file.add(m) 997 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: 998 has_stream.add(m.name) 999 1000 for m in has_file: 1001 if m.name not in has_stream: 1002 warn(clazz, m, "M10", "Methods accepting File should also accept FileDescriptor or streams") 1003 1004 1005def verify_manager_list(clazz): 1006 """Verifies that managers return List<? extends Parcelable> instead of arrays.""" 1007 1008 if not clazz.name.endswith("Manager"): return 1009 1010 for m in clazz.methods: 1011 if m.typ.startswith("android.") and m.typ.endswith("[]"): 1012 warn(clazz, m, None, "Methods should return List<? extends Parcelable> instead of Parcelable[] to support ParceledListSlice under the hood") 1013 1014 1015def verify_abstract_inner(clazz): 1016 """Verifies that abstract inner classes are static.""" 1017 1018 if re.match(".+?\.[A-Z][^\.]+\.[A-Z]", clazz.fullname): 1019 if " abstract " in clazz.raw and " static " not in clazz.raw: 1020 warn(clazz, None, None, "Abstract inner classes should be static to improve testability") 1021 1022 1023def verify_runtime_exceptions(clazz): 1024 """Verifies that runtime exceptions aren't listed in throws.""" 1025 1026 banned = [ 1027 "java.lang.NullPointerException", 1028 "java.lang.ClassCastException", 1029 "java.lang.IndexOutOfBoundsException", 1030 "java.lang.reflect.UndeclaredThrowableException", 1031 "java.lang.reflect.MalformedParametersException", 1032 "java.lang.reflect.MalformedParameterizedTypeException", 1033 "java.lang.invoke.WrongMethodTypeException", 1034 "java.lang.EnumConstantNotPresentException", 1035 "java.lang.IllegalMonitorStateException", 1036 "java.lang.SecurityException", 1037 "java.lang.UnsupportedOperationException", 1038 "java.lang.annotation.AnnotationTypeMismatchException", 1039 "java.lang.annotation.IncompleteAnnotationException", 1040 "java.lang.TypeNotPresentException", 1041 "java.lang.IllegalStateException", 1042 "java.lang.ArithmeticException", 1043 "java.lang.IllegalArgumentException", 1044 "java.lang.ArrayStoreException", 1045 "java.lang.NegativeArraySizeException", 1046 "java.util.MissingResourceException", 1047 "java.util.EmptyStackException", 1048 "java.util.concurrent.CompletionException", 1049 "java.util.concurrent.RejectedExecutionException", 1050 "java.util.IllformedLocaleException", 1051 "java.util.ConcurrentModificationException", 1052 "java.util.NoSuchElementException", 1053 "java.io.UncheckedIOException", 1054 "java.time.DateTimeException", 1055 "java.security.ProviderException", 1056 "java.nio.BufferUnderflowException", 1057 "java.nio.BufferOverflowException", 1058 ] 1059 1060 test = [] 1061 test.extend(clazz.ctors) 1062 test.extend(clazz.methods) 1063 1064 for t in test: 1065 if " throws " not in t.raw: continue 1066 throws = t.raw[t.raw.index(" throws "):] 1067 for b in banned: 1068 if b in throws: 1069 error(clazz, t, None, "Methods must not mention RuntimeException subclasses in throws clauses") 1070 1071 1072def verify_error(clazz): 1073 """Verifies that we always use Exception instead of Error.""" 1074 if not clazz.extends: return 1075 if clazz.extends.endswith("Error"): 1076 error(clazz, None, None, "Trouble must be reported through an Exception, not Error") 1077 if clazz.extends.endswith("Exception") and not clazz.name.endswith("Exception"): 1078 error(clazz, None, None, "Exceptions must be named FooException") 1079 1080 1081def verify_units(clazz): 1082 """Verifies that we use consistent naming for units.""" 1083 1084 # If we find K, recommend replacing with V 1085 bad = { 1086 "Ns": "Nanos", 1087 "Ms": "Millis or Micros", 1088 "Sec": "Seconds", "Secs": "Seconds", 1089 "Hr": "Hours", "Hrs": "Hours", 1090 "Mo": "Months", "Mos": "Months", 1091 "Yr": "Years", "Yrs": "Years", 1092 "Byte": "Bytes", "Space": "Bytes", 1093 } 1094 1095 for m in clazz.methods: 1096 if m.typ not in ["short","int","long"]: continue 1097 for k, v in bad.iteritems(): 1098 if m.name.endswith(k): 1099 error(clazz, m, None, "Expected method name units to be " + v) 1100 if m.name.endswith("Nanos") or m.name.endswith("Micros"): 1101 warn(clazz, m, None, "Returned time values are strongly encouraged to be in milliseconds unless you need the extra precision") 1102 if m.name.endswith("Seconds"): 1103 error(clazz, m, None, "Returned time values must be in milliseconds") 1104 1105 for m in clazz.methods: 1106 typ = m.typ 1107 if typ == "void": 1108 if len(m.args) != 1: continue 1109 typ = m.args[0] 1110 1111 if m.name.endswith("Fraction") and typ != "float": 1112 error(clazz, m, None, "Fractions must use floats") 1113 if m.name.endswith("Percentage") and typ != "int": 1114 error(clazz, m, None, "Percentage must use ints") 1115 1116 1117def verify_closable(clazz): 1118 """Verifies that classes are AutoClosable.""" 1119 if "implements java.lang.AutoCloseable" in clazz.raw: return 1120 if "implements java.io.Closeable" in clazz.raw: return 1121 1122 for m in clazz.methods: 1123 if len(m.args) > 0: continue 1124 if m.name in ["close","release","destroy","finish","finalize","disconnect","shutdown","stop","free","quit"]: 1125 warn(clazz, m, None, "Classes that release resources should implement AutoClosable and CloseGuard") 1126 return 1127 1128 1129def examine_clazz(clazz): 1130 """Find all style issues in the given class.""" 1131 if clazz.pkg.name.startswith("java"): return 1132 if clazz.pkg.name.startswith("junit"): return 1133 if clazz.pkg.name.startswith("org.apache"): return 1134 if clazz.pkg.name.startswith("org.xml"): return 1135 if clazz.pkg.name.startswith("org.json"): return 1136 if clazz.pkg.name.startswith("org.w3c"): return 1137 if clazz.pkg.name.startswith("android.icu."): return 1138 1139 verify_constants(clazz) 1140 verify_enums(clazz) 1141 verify_class_names(clazz) 1142 verify_method_names(clazz) 1143 verify_callbacks(clazz) 1144 verify_listeners(clazz) 1145 verify_actions(clazz) 1146 verify_extras(clazz) 1147 verify_equals(clazz) 1148 verify_parcelable(clazz) 1149 verify_protected(clazz) 1150 verify_fields(clazz) 1151 verify_register(clazz) 1152 verify_sync(clazz) 1153 verify_intent_builder(clazz) 1154 verify_helper_classes(clazz) 1155 verify_builder(clazz) 1156 verify_aidl(clazz) 1157 verify_internal(clazz) 1158 verify_layering(clazz) 1159 verify_boolean(clazz) 1160 verify_collections(clazz) 1161 verify_flags(clazz) 1162 verify_exception(clazz) 1163 if not ALLOW_GOOGLE: verify_google(clazz) 1164 verify_bitset(clazz) 1165 verify_manager(clazz) 1166 verify_boxed(clazz) 1167 verify_static_utils(clazz) 1168 verify_overload_args(clazz) 1169 verify_callback_handlers(clazz) 1170 verify_context_first(clazz) 1171 verify_listener_last(clazz) 1172 verify_resource_names(clazz) 1173 verify_files(clazz) 1174 verify_manager_list(clazz) 1175 verify_abstract_inner(clazz) 1176 verify_runtime_exceptions(clazz) 1177 verify_error(clazz) 1178 verify_units(clazz) 1179 verify_closable(clazz) 1180 1181 1182def examine_stream(stream): 1183 """Find all style issues in the given API stream.""" 1184 global failures 1185 failures = {} 1186 _parse_stream(stream, examine_clazz) 1187 return failures 1188 1189 1190def examine_api(api): 1191 """Find all style issues in the given parsed API.""" 1192 global failures 1193 failures = {} 1194 for key in sorted(api.keys()): 1195 examine_clazz(api[key]) 1196 return failures 1197 1198 1199def verify_compat(cur, prev): 1200 """Find any incompatible API changes between two levels.""" 1201 global failures 1202 1203 def class_exists(api, test): 1204 return test.fullname in api 1205 1206 def ctor_exists(api, clazz, test): 1207 for m in clazz.ctors: 1208 if m.ident == test.ident: return True 1209 return False 1210 1211 def all_methods(api, clazz): 1212 methods = list(clazz.methods) 1213 if clazz.extends is not None: 1214 methods.extend(all_methods(api, api[clazz.extends])) 1215 return methods 1216 1217 def method_exists(api, clazz, test): 1218 methods = all_methods(api, clazz) 1219 for m in methods: 1220 if m.ident == test.ident: return True 1221 return False 1222 1223 def field_exists(api, clazz, test): 1224 for f in clazz.fields: 1225 if f.ident == test.ident: return True 1226 return False 1227 1228 failures = {} 1229 for key in sorted(prev.keys()): 1230 prev_clazz = prev[key] 1231 1232 if not class_exists(cur, prev_clazz): 1233 error(prev_clazz, None, None, "Class removed or incompatible change") 1234 continue 1235 1236 cur_clazz = cur[key] 1237 1238 for test in prev_clazz.ctors: 1239 if not ctor_exists(cur, cur_clazz, test): 1240 error(prev_clazz, prev_ctor, None, "Constructor removed or incompatible change") 1241 1242 methods = all_methods(prev, prev_clazz) 1243 for test in methods: 1244 if not method_exists(cur, cur_clazz, test): 1245 error(prev_clazz, test, None, "Method removed or incompatible change") 1246 1247 for test in prev_clazz.fields: 1248 if not field_exists(cur, cur_clazz, test): 1249 error(prev_clazz, test, None, "Field removed or incompatible change") 1250 1251 return failures 1252 1253 1254if __name__ == "__main__": 1255 parser = argparse.ArgumentParser(description="Enforces common Android public API design \ 1256 patterns. It ignores lint messages from a previous API level, if provided.") 1257 parser.add_argument("current.txt", type=argparse.FileType('r'), help="current.txt") 1258 parser.add_argument("previous.txt", nargs='?', type=argparse.FileType('r'), default=None, 1259 help="previous.txt") 1260 parser.add_argument("--no-color", action='store_const', const=True, 1261 help="Disable terminal colors") 1262 parser.add_argument("--allow-google", action='store_const', const=True, 1263 help="Allow references to Google") 1264 args = vars(parser.parse_args()) 1265 1266 if args['no_color']: 1267 USE_COLOR = False 1268 1269 if args['allow_google']: 1270 ALLOW_GOOGLE = True 1271 1272 current_file = args['current.txt'] 1273 previous_file = args['previous.txt'] 1274 1275 with current_file as f: 1276 cur_fail = examine_stream(f) 1277 if not previous_file is None: 1278 with previous_file as f: 1279 prev_fail = examine_stream(f) 1280 1281 # ignore errors from previous API level 1282 for p in prev_fail: 1283 if p in cur_fail: 1284 del cur_fail[p] 1285 1286 """ 1287 # NOTE: disabled because of memory pressure 1288 # look for compatibility issues 1289 compat_fail = verify_compat(cur, prev) 1290 1291 print "%s API compatibility issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True))) 1292 for f in sorted(compat_fail): 1293 print compat_fail[f] 1294 print 1295 """ 1296 1297 print "%s API style issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True))) 1298 for f in sorted(cur_fail): 1299 print cur_fail[f] 1300 print 1301