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) and 433 (" final deprecated class " not in clazz.raw)): 434 error(clazz, None, "FW8", "Parcelable classes must be final") 435 436 437def verify_protected(clazz): 438 """Verify that no protected methods or fields are allowed.""" 439 for m in clazz.methods: 440 if "protected" in m.split: 441 error(clazz, m, "M7", "Protected methods not allowed; must be public") 442 for f in clazz.fields: 443 if "protected" in f.split: 444 error(clazz, f, "M7", "Protected fields not allowed; must be public") 445 446 447def verify_fields(clazz): 448 """Verify that all exposed fields are final. 449 Exposed fields must follow myName style. 450 Catch internal mFoo objects being exposed.""" 451 452 IGNORE_BARE_FIELDS = [ 453 "android.app.ActivityManager.RecentTaskInfo", 454 "android.app.Notification", 455 "android.content.pm.ActivityInfo", 456 "android.content.pm.ApplicationInfo", 457 "android.content.pm.ComponentInfo", 458 "android.content.pm.ResolveInfo", 459 "android.content.pm.FeatureGroupInfo", 460 "android.content.pm.InstrumentationInfo", 461 "android.content.pm.PackageInfo", 462 "android.content.pm.PackageItemInfo", 463 "android.content.res.Configuration", 464 "android.graphics.BitmapFactory.Options", 465 "android.os.Message", 466 "android.system.StructPollfd", 467 ] 468 469 for f in clazz.fields: 470 if not "final" in f.split: 471 if clazz.fullname in IGNORE_BARE_FIELDS: 472 pass 473 elif clazz.fullname.endswith("LayoutParams"): 474 pass 475 elif clazz.fullname.startswith("android.util.Mutable"): 476 pass 477 else: 478 error(clazz, f, "F2", "Bare fields must be marked final, or add accessors if mutable") 479 480 if not "static" in f.split: 481 if not re.match("[a-z]([a-zA-Z]+)?", f.name): 482 error(clazz, f, "S1", "Non-static fields must be named using myField style") 483 484 if re.match("[ms][A-Z]", f.name): 485 error(clazz, f, "F1", "Internal objects must not be exposed") 486 487 if re.match("[A-Z_]+", f.name): 488 if "static" not in f.split or "final" not in f.split: 489 error(clazz, f, "C2", "Constants must be marked static final") 490 491 492def verify_register(clazz): 493 """Verify parity of registration methods. 494 Callback objects use register/unregister methods. 495 Listener objects use add/remove methods.""" 496 methods = [ m.name for m in clazz.methods ] 497 for m in clazz.methods: 498 if "Callback" in m.raw: 499 if m.name.startswith("register"): 500 other = "unregister" + m.name[8:] 501 if other not in methods: 502 error(clazz, m, "L2", "Missing unregister method") 503 if m.name.startswith("unregister"): 504 other = "register" + m.name[10:] 505 if other not in methods: 506 error(clazz, m, "L2", "Missing register method") 507 508 if m.name.startswith("add") or m.name.startswith("remove"): 509 error(clazz, m, "L3", "Callback methods should be named register/unregister") 510 511 if "Listener" in m.raw: 512 if m.name.startswith("add"): 513 other = "remove" + m.name[3:] 514 if other not in methods: 515 error(clazz, m, "L2", "Missing remove method") 516 if m.name.startswith("remove") and not m.name.startswith("removeAll"): 517 other = "add" + m.name[6:] 518 if other not in methods: 519 error(clazz, m, "L2", "Missing add method") 520 521 if m.name.startswith("register") or m.name.startswith("unregister"): 522 error(clazz, m, "L3", "Listener methods should be named add/remove") 523 524 525def verify_sync(clazz): 526 """Verify synchronized methods aren't exposed.""" 527 for m in clazz.methods: 528 if "synchronized" in m.split: 529 error(clazz, m, "M5", "Internal locks must not be exposed") 530 531 532def verify_intent_builder(clazz): 533 """Verify that Intent builders are createFooIntent() style.""" 534 if clazz.name == "Intent": return 535 536 for m in clazz.methods: 537 if m.typ == "android.content.Intent": 538 if m.name.startswith("create") and m.name.endswith("Intent"): 539 pass 540 else: 541 warn(clazz, m, "FW1", "Methods creating an Intent should be named createFooIntent()") 542 543 544def verify_helper_classes(clazz): 545 """Verify that helper classes are named consistently with what they extend. 546 All developer extendable methods should be named onFoo().""" 547 test_methods = False 548 if "extends android.app.Service" in clazz.raw: 549 test_methods = True 550 if not clazz.name.endswith("Service"): 551 error(clazz, None, "CL4", "Inconsistent class name; should be FooService") 552 553 found = False 554 for f in clazz.fields: 555 if f.name == "SERVICE_INTERFACE": 556 found = True 557 if f.value != clazz.fullname: 558 error(clazz, f, "C4", "Inconsistent interface constant; expected %s" % (clazz.fullname)) 559 560 if "extends android.content.ContentProvider" in clazz.raw: 561 test_methods = True 562 if not clazz.name.endswith("Provider"): 563 error(clazz, None, "CL4", "Inconsistent class name; should be FooProvider") 564 565 found = False 566 for f in clazz.fields: 567 if f.name == "PROVIDER_INTERFACE": 568 found = True 569 if f.value != clazz.fullname: 570 error(clazz, f, "C4", "Inconsistent interface constant; expected %s" % (clazz.fullname)) 571 572 if "extends android.content.BroadcastReceiver" in clazz.raw: 573 test_methods = True 574 if not clazz.name.endswith("Receiver"): 575 error(clazz, None, "CL4", "Inconsistent class name; should be FooReceiver") 576 577 if "extends android.app.Activity" in clazz.raw: 578 test_methods = True 579 if not clazz.name.endswith("Activity"): 580 error(clazz, None, "CL4", "Inconsistent class name; should be FooActivity") 581 582 if test_methods: 583 for m in clazz.methods: 584 if "final" in m.split: continue 585 if not re.match("on[A-Z]", m.name): 586 if "abstract" in m.split: 587 warn(clazz, m, None, "Methods implemented by developers should be named onFoo()") 588 else: 589 warn(clazz, m, None, "If implemented by developer, should be named onFoo(); otherwise consider marking final") 590 591 592def verify_builder(clazz): 593 """Verify builder classes. 594 Methods should return the builder to enable chaining.""" 595 if " extends " in clazz.raw: return 596 if not clazz.name.endswith("Builder"): return 597 598 if clazz.name != "Builder": 599 warn(clazz, None, None, "Builder should be defined as inner class") 600 601 has_build = False 602 for m in clazz.methods: 603 if m.name == "build": 604 has_build = True 605 continue 606 607 if m.name.startswith("get"): continue 608 if m.name.startswith("clear"): continue 609 610 if m.name.startswith("with"): 611 warn(clazz, m, None, "Builder methods names should use setFoo() style") 612 613 if m.name.startswith("set"): 614 if not m.typ.endswith(clazz.fullname): 615 warn(clazz, m, "M4", "Methods must return the builder object") 616 617 if not has_build: 618 warn(clazz, None, None, "Missing build() method") 619 620 621def verify_aidl(clazz): 622 """Catch people exposing raw AIDL.""" 623 if "extends android.os.Binder" in clazz.raw or "implements android.os.IInterface" in clazz.raw: 624 error(clazz, None, None, "Raw AIDL interfaces must not be exposed") 625 626 627def verify_internal(clazz): 628 """Catch people exposing internal classes.""" 629 if clazz.pkg.name.startswith("com.android"): 630 error(clazz, None, None, "Internal classes must not be exposed") 631 632 633def verify_layering(clazz): 634 """Catch package layering violations. 635 For example, something in android.os depending on android.app.""" 636 ranking = [ 637 ["android.service","android.accessibilityservice","android.inputmethodservice","android.printservice","android.appwidget","android.webkit","android.preference","android.gesture","android.print"], 638 "android.app", 639 "android.widget", 640 "android.view", 641 "android.animation", 642 "android.provider", 643 ["android.content","android.graphics.drawable"], 644 "android.database", 645 "android.graphics", 646 "android.text", 647 "android.os", 648 "android.util" 649 ] 650 651 def rank(p): 652 for i in range(len(ranking)): 653 if isinstance(ranking[i], list): 654 for j in ranking[i]: 655 if p.startswith(j): return i 656 else: 657 if p.startswith(ranking[i]): return i 658 659 cr = rank(clazz.pkg.name) 660 if cr is None: return 661 662 for f in clazz.fields: 663 ir = rank(f.typ) 664 if ir and ir < cr: 665 warn(clazz, f, "FW6", "Field type violates package layering") 666 667 for m in clazz.methods: 668 ir = rank(m.typ) 669 if ir and ir < cr: 670 warn(clazz, m, "FW6", "Method return type violates package layering") 671 for arg in m.args: 672 ir = rank(arg) 673 if ir and ir < cr: 674 warn(clazz, m, "FW6", "Method argument type violates package layering") 675 676 677def verify_boolean(clazz): 678 """Verifies that boolean accessors are named correctly. 679 For example, hasFoo() and setHasFoo().""" 680 681 def is_get(m): return len(m.args) == 0 and m.typ == "boolean" 682 def is_set(m): return len(m.args) == 1 and m.args[0] == "boolean" 683 684 gets = [ m for m in clazz.methods if is_get(m) ] 685 sets = [ m for m in clazz.methods if is_set(m) ] 686 687 def error_if_exists(methods, trigger, expected, actual): 688 for m in methods: 689 if m.name == actual: 690 error(clazz, m, "M6", "Symmetric method for %s must be named %s" % (trigger, expected)) 691 692 for m in clazz.methods: 693 if is_get(m): 694 if re.match("is[A-Z]", m.name): 695 target = m.name[2:] 696 expected = "setIs" + target 697 error_if_exists(sets, m.name, expected, "setHas" + target) 698 elif re.match("has[A-Z]", m.name): 699 target = m.name[3:] 700 expected = "setHas" + target 701 error_if_exists(sets, m.name, expected, "setIs" + target) 702 error_if_exists(sets, m.name, expected, "set" + target) 703 elif re.match("get[A-Z]", m.name): 704 target = m.name[3:] 705 expected = "set" + target 706 error_if_exists(sets, m.name, expected, "setIs" + target) 707 error_if_exists(sets, m.name, expected, "setHas" + target) 708 709 if is_set(m): 710 if re.match("set[A-Z]", m.name): 711 target = m.name[3:] 712 expected = "get" + target 713 error_if_exists(sets, m.name, expected, "is" + target) 714 error_if_exists(sets, m.name, expected, "has" + target) 715 716 717def verify_collections(clazz): 718 """Verifies that collection types are interfaces.""" 719 if clazz.fullname == "android.os.Bundle": return 720 721 bad = ["java.util.Vector", "java.util.LinkedList", "java.util.ArrayList", "java.util.Stack", 722 "java.util.HashMap", "java.util.HashSet", "android.util.ArraySet", "android.util.ArrayMap"] 723 for m in clazz.methods: 724 if m.typ in bad: 725 error(clazz, m, "CL2", "Return type is concrete collection; must be higher-level interface") 726 for arg in m.args: 727 if arg in bad: 728 error(clazz, m, "CL2", "Argument is concrete collection; must be higher-level interface") 729 730 731def verify_flags(clazz): 732 """Verifies that flags are non-overlapping.""" 733 known = collections.defaultdict(int) 734 for f in clazz.fields: 735 if "FLAG_" in f.name: 736 try: 737 val = int(f.value) 738 except: 739 continue 740 741 scope = f.name[0:f.name.index("FLAG_")] 742 if val & known[scope]: 743 warn(clazz, f, "C1", "Found overlapping flag constant value") 744 known[scope] |= val 745 746 747def verify_exception(clazz): 748 """Verifies that methods don't throw generic exceptions.""" 749 for m in clazz.methods: 750 if "throws java.lang.Exception" in m.raw or "throws java.lang.Throwable" in m.raw or "throws java.lang.Error" in m.raw: 751 error(clazz, m, "S1", "Methods must not throw generic exceptions") 752 753 if "throws android.os.RemoteException" in m.raw: 754 if clazz.name == "android.content.ContentProviderClient": continue 755 if clazz.name == "android.os.Binder": continue 756 if clazz.name == "android.os.IBinder": continue 757 758 error(clazz, m, "FW9", "Methods calling into system server should rethrow RemoteException as RuntimeException") 759 760 761def verify_google(clazz): 762 """Verifies that APIs never reference Google.""" 763 764 if re.search("google", clazz.raw, re.IGNORECASE): 765 error(clazz, None, None, "Must never reference Google") 766 767 test = [] 768 test.extend(clazz.ctors) 769 test.extend(clazz.fields) 770 test.extend(clazz.methods) 771 772 for t in test: 773 if re.search("google", t.raw, re.IGNORECASE): 774 error(clazz, t, None, "Must never reference Google") 775 776 777def verify_bitset(clazz): 778 """Verifies that we avoid using heavy BitSet.""" 779 780 for f in clazz.fields: 781 if f.typ == "java.util.BitSet": 782 error(clazz, f, None, "Field type must not be heavy BitSet") 783 784 for m in clazz.methods: 785 if m.typ == "java.util.BitSet": 786 error(clazz, m, None, "Return type must not be heavy BitSet") 787 for arg in m.args: 788 if arg == "java.util.BitSet": 789 error(clazz, m, None, "Argument type must not be heavy BitSet") 790 791 792def verify_manager(clazz): 793 """Verifies that FooManager is only obtained from Context.""" 794 795 if not clazz.name.endswith("Manager"): return 796 797 for c in clazz.ctors: 798 error(clazz, c, None, "Managers must always be obtained from Context; no direct constructors") 799 800 for m in clazz.methods: 801 if m.typ == clazz.fullname: 802 error(clazz, m, None, "Managers must always be obtained from Context") 803 804 805def verify_boxed(clazz): 806 """Verifies that methods avoid boxed primitives.""" 807 808 boxed = ["java.lang.Number","java.lang.Byte","java.lang.Double","java.lang.Float","java.lang.Integer","java.lang.Long","java.lang.Short"] 809 810 for c in clazz.ctors: 811 for arg in c.args: 812 if arg in boxed: 813 error(clazz, c, "M11", "Must avoid boxed primitives") 814 815 for f in clazz.fields: 816 if f.typ in boxed: 817 error(clazz, f, "M11", "Must avoid boxed primitives") 818 819 for m in clazz.methods: 820 if m.typ in boxed: 821 error(clazz, m, "M11", "Must avoid boxed primitives") 822 for arg in m.args: 823 if arg in boxed: 824 error(clazz, m, "M11", "Must avoid boxed primitives") 825 826 827def verify_static_utils(clazz): 828 """Verifies that helper classes can't be constructed.""" 829 if clazz.fullname.startswith("android.opengl"): return 830 if clazz.fullname.startswith("android.R"): return 831 832 # Only care about classes with default constructors 833 if len(clazz.ctors) == 1 and len(clazz.ctors[0].args) == 0: 834 test = [] 835 test.extend(clazz.fields) 836 test.extend(clazz.methods) 837 838 if len(test) == 0: return 839 for t in test: 840 if "static" not in t.split: 841 return 842 843 error(clazz, None, None, "Fully-static utility classes must not have constructor") 844 845 846def verify_overload_args(clazz): 847 """Verifies that method overloads add new arguments at the end.""" 848 if clazz.fullname.startswith("android.opengl"): return 849 850 overloads = collections.defaultdict(list) 851 for m in clazz.methods: 852 if "deprecated" in m.split: continue 853 overloads[m.name].append(m) 854 855 for name, methods in overloads.items(): 856 if len(methods) <= 1: continue 857 858 # Look for arguments common across all overloads 859 def cluster(args): 860 count = collections.defaultdict(int) 861 res = set() 862 for i in range(len(args)): 863 a = args[i] 864 res.add("%s#%d" % (a, count[a])) 865 count[a] += 1 866 return res 867 868 common_args = cluster(methods[0].args) 869 for m in methods: 870 common_args = common_args & cluster(m.args) 871 872 if len(common_args) == 0: continue 873 874 # Require that all common arguments are present at start of signature 875 locked_sig = None 876 for m in methods: 877 sig = m.args[0:len(common_args)] 878 if not common_args.issubset(cluster(sig)): 879 warn(clazz, m, "M2", "Expected common arguments [%s] at beginning of overloaded method" % (", ".join(common_args))) 880 elif not locked_sig: 881 locked_sig = sig 882 elif locked_sig != sig: 883 error(clazz, m, "M2", "Expected consistent argument ordering between overloads: %s..." % (", ".join(locked_sig))) 884 885 886def verify_callback_handlers(clazz): 887 """Verifies that methods adding listener/callback have overload 888 for specifying delivery thread.""" 889 890 # Ignore UI packages which assume main thread 891 skip = [ 892 "animation", 893 "view", 894 "graphics", 895 "transition", 896 "widget", 897 "webkit", 898 ] 899 for s in skip: 900 if s in clazz.pkg.name_path: return 901 if s in clazz.extends_path: return 902 903 # Ignore UI classes which assume main thread 904 if "app" in clazz.pkg.name_path or "app" in clazz.extends_path: 905 for s in ["ActionBar","Dialog","Application","Activity","Fragment","Loader"]: 906 if s in clazz.fullname: return 907 if "content" in clazz.pkg.name_path or "content" in clazz.extends_path: 908 for s in ["Loader"]: 909 if s in clazz.fullname: return 910 911 found = {} 912 by_name = collections.defaultdict(list) 913 for m in clazz.methods: 914 if m.name.startswith("unregister"): continue 915 if m.name.startswith("remove"): continue 916 if re.match("on[A-Z]+", m.name): continue 917 918 by_name[m.name].append(m) 919 920 for a in m.args: 921 if a.endswith("Listener") or a.endswith("Callback") or a.endswith("Callbacks"): 922 found[m.name] = m 923 924 for f in found.values(): 925 takes_handler = False 926 for m in by_name[f.name]: 927 if "android.os.Handler" in m.args: 928 takes_handler = True 929 if not takes_handler: 930 warn(clazz, f, "L1", "Registration methods should have overload that accepts delivery Handler") 931 932 933def verify_context_first(clazz): 934 """Verifies that methods accepting a Context keep it the first argument.""" 935 examine = clazz.ctors + clazz.methods 936 for m in examine: 937 if len(m.args) > 1 and m.args[0] != "android.content.Context": 938 if "android.content.Context" in m.args[1:]: 939 error(clazz, m, "M3", "Context is distinct, so it must be the first argument") 940 if len(m.args) > 1 and m.args[0] != "android.content.ContentResolver": 941 if "android.content.ContentResolver" in m.args[1:]: 942 error(clazz, m, "M3", "ContentResolver is distinct, so it must be the first argument") 943 944 945def verify_listener_last(clazz): 946 """Verifies that methods accepting a Listener or Callback keep them as last arguments.""" 947 examine = clazz.ctors + clazz.methods 948 for m in examine: 949 if "Listener" in m.name or "Callback" in m.name: continue 950 found = False 951 for a in m.args: 952 if a.endswith("Callback") or a.endswith("Callbacks") or a.endswith("Listener"): 953 found = True 954 elif found and a != "android.os.Handler": 955 warn(clazz, m, "M3", "Listeners should always be at end of argument list") 956 957 958def verify_resource_names(clazz): 959 """Verifies that resource names have consistent case.""" 960 if not re.match("android\.R\.[a-z]+", clazz.fullname): return 961 962 # Resources defined by files are foo_bar_baz 963 if clazz.name in ["anim","animator","color","dimen","drawable","interpolator","layout","transition","menu","mipmap","string","plurals","raw","xml"]: 964 for f in clazz.fields: 965 if re.match("[a-z1-9_]+$", f.name): continue 966 error(clazz, f, None, "Expected resource name in this class to be foo_bar_baz style") 967 968 # Resources defined inside files are fooBarBaz 969 if clazz.name in ["array","attr","id","bool","fraction","integer"]: 970 for f in clazz.fields: 971 if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue 972 if re.match("layout_[a-z][a-zA-Z1-9]*$", f.name): continue 973 if re.match("state_[a-z_]*$", f.name): continue 974 975 if re.match("[a-z][a-zA-Z1-9]*$", f.name): continue 976 error(clazz, f, "C7", "Expected resource name in this class to be fooBarBaz style") 977 978 # Styles are FooBar_Baz 979 if clazz.name in ["style"]: 980 for f in clazz.fields: 981 if re.match("[A-Z][A-Za-z1-9]+(_[A-Z][A-Za-z1-9]+?)*$", f.name): continue 982 error(clazz, f, "C7", "Expected resource name in this class to be FooBar_Baz style") 983 984 985def verify_files(clazz): 986 """Verifies that methods accepting File also accept streams.""" 987 988 has_file = set() 989 has_stream = set() 990 991 test = [] 992 test.extend(clazz.ctors) 993 test.extend(clazz.methods) 994 995 for m in test: 996 if "java.io.File" in m.args: 997 has_file.add(m) 998 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: 999 has_stream.add(m.name) 1000 1001 for m in has_file: 1002 if m.name not in has_stream: 1003 warn(clazz, m, "M10", "Methods accepting File should also accept FileDescriptor or streams") 1004 1005 1006def verify_manager_list(clazz): 1007 """Verifies that managers return List<? extends Parcelable> instead of arrays.""" 1008 1009 if not clazz.name.endswith("Manager"): return 1010 1011 for m in clazz.methods: 1012 if m.typ.startswith("android.") and m.typ.endswith("[]"): 1013 warn(clazz, m, None, "Methods should return List<? extends Parcelable> instead of Parcelable[] to support ParceledListSlice under the hood") 1014 1015 1016def verify_abstract_inner(clazz): 1017 """Verifies that abstract inner classes are static.""" 1018 1019 if re.match(".+?\.[A-Z][^\.]+\.[A-Z]", clazz.fullname): 1020 if " abstract " in clazz.raw and " static " not in clazz.raw: 1021 warn(clazz, None, None, "Abstract inner classes should be static to improve testability") 1022 1023 1024def verify_runtime_exceptions(clazz): 1025 """Verifies that runtime exceptions aren't listed in throws.""" 1026 1027 banned = [ 1028 "java.lang.NullPointerException", 1029 "java.lang.ClassCastException", 1030 "java.lang.IndexOutOfBoundsException", 1031 "java.lang.reflect.UndeclaredThrowableException", 1032 "java.lang.reflect.MalformedParametersException", 1033 "java.lang.reflect.MalformedParameterizedTypeException", 1034 "java.lang.invoke.WrongMethodTypeException", 1035 "java.lang.EnumConstantNotPresentException", 1036 "java.lang.IllegalMonitorStateException", 1037 "java.lang.SecurityException", 1038 "java.lang.UnsupportedOperationException", 1039 "java.lang.annotation.AnnotationTypeMismatchException", 1040 "java.lang.annotation.IncompleteAnnotationException", 1041 "java.lang.TypeNotPresentException", 1042 "java.lang.IllegalStateException", 1043 "java.lang.ArithmeticException", 1044 "java.lang.IllegalArgumentException", 1045 "java.lang.ArrayStoreException", 1046 "java.lang.NegativeArraySizeException", 1047 "java.util.MissingResourceException", 1048 "java.util.EmptyStackException", 1049 "java.util.concurrent.CompletionException", 1050 "java.util.concurrent.RejectedExecutionException", 1051 "java.util.IllformedLocaleException", 1052 "java.util.ConcurrentModificationException", 1053 "java.util.NoSuchElementException", 1054 "java.io.UncheckedIOException", 1055 "java.time.DateTimeException", 1056 "java.security.ProviderException", 1057 "java.nio.BufferUnderflowException", 1058 "java.nio.BufferOverflowException", 1059 ] 1060 1061 test = [] 1062 test.extend(clazz.ctors) 1063 test.extend(clazz.methods) 1064 1065 for t in test: 1066 if " throws " not in t.raw: continue 1067 throws = t.raw[t.raw.index(" throws "):] 1068 for b in banned: 1069 if b in throws: 1070 error(clazz, t, None, "Methods must not mention RuntimeException subclasses in throws clauses") 1071 1072 1073def verify_error(clazz): 1074 """Verifies that we always use Exception instead of Error.""" 1075 if not clazz.extends: return 1076 if clazz.extends.endswith("Error"): 1077 error(clazz, None, None, "Trouble must be reported through an Exception, not Error") 1078 if clazz.extends.endswith("Exception") and not clazz.name.endswith("Exception"): 1079 error(clazz, None, None, "Exceptions must be named FooException") 1080 1081 1082def verify_units(clazz): 1083 """Verifies that we use consistent naming for units.""" 1084 1085 # If we find K, recommend replacing with V 1086 bad = { 1087 "Ns": "Nanos", 1088 "Ms": "Millis or Micros", 1089 "Sec": "Seconds", "Secs": "Seconds", 1090 "Hr": "Hours", "Hrs": "Hours", 1091 "Mo": "Months", "Mos": "Months", 1092 "Yr": "Years", "Yrs": "Years", 1093 "Byte": "Bytes", "Space": "Bytes", 1094 } 1095 1096 for m in clazz.methods: 1097 if m.typ not in ["short","int","long"]: continue 1098 for k, v in bad.iteritems(): 1099 if m.name.endswith(k): 1100 error(clazz, m, None, "Expected method name units to be " + v) 1101 if m.name.endswith("Nanos") or m.name.endswith("Micros"): 1102 warn(clazz, m, None, "Returned time values are strongly encouraged to be in milliseconds unless you need the extra precision") 1103 if m.name.endswith("Seconds"): 1104 error(clazz, m, None, "Returned time values must be in milliseconds") 1105 1106 for m in clazz.methods: 1107 typ = m.typ 1108 if typ == "void": 1109 if len(m.args) != 1: continue 1110 typ = m.args[0] 1111 1112 if m.name.endswith("Fraction") and typ != "float": 1113 error(clazz, m, None, "Fractions must use floats") 1114 if m.name.endswith("Percentage") and typ != "int": 1115 error(clazz, m, None, "Percentage must use ints") 1116 1117 1118def verify_closable(clazz): 1119 """Verifies that classes are AutoClosable.""" 1120 if "implements java.lang.AutoCloseable" in clazz.raw: return 1121 if "implements java.io.Closeable" in clazz.raw: return 1122 1123 for m in clazz.methods: 1124 if len(m.args) > 0: continue 1125 if m.name in ["close","release","destroy","finish","finalize","disconnect","shutdown","stop","free","quit"]: 1126 warn(clazz, m, None, "Classes that release resources should implement AutoClosable and CloseGuard") 1127 return 1128 1129 1130def examine_clazz(clazz): 1131 """Find all style issues in the given class.""" 1132 if clazz.pkg.name.startswith("java"): return 1133 if clazz.pkg.name.startswith("junit"): return 1134 if clazz.pkg.name.startswith("org.apache"): return 1135 if clazz.pkg.name.startswith("org.xml"): return 1136 if clazz.pkg.name.startswith("org.json"): return 1137 if clazz.pkg.name.startswith("org.w3c"): return 1138 if clazz.pkg.name.startswith("android.icu."): return 1139 1140 verify_constants(clazz) 1141 verify_enums(clazz) 1142 verify_class_names(clazz) 1143 verify_method_names(clazz) 1144 verify_callbacks(clazz) 1145 verify_listeners(clazz) 1146 verify_actions(clazz) 1147 verify_extras(clazz) 1148 verify_equals(clazz) 1149 verify_parcelable(clazz) 1150 verify_protected(clazz) 1151 verify_fields(clazz) 1152 verify_register(clazz) 1153 verify_sync(clazz) 1154 verify_intent_builder(clazz) 1155 verify_helper_classes(clazz) 1156 verify_builder(clazz) 1157 verify_aidl(clazz) 1158 verify_internal(clazz) 1159 verify_layering(clazz) 1160 verify_boolean(clazz) 1161 verify_collections(clazz) 1162 verify_flags(clazz) 1163 verify_exception(clazz) 1164 if not ALLOW_GOOGLE: verify_google(clazz) 1165 verify_bitset(clazz) 1166 verify_manager(clazz) 1167 verify_boxed(clazz) 1168 verify_static_utils(clazz) 1169 verify_overload_args(clazz) 1170 verify_callback_handlers(clazz) 1171 verify_context_first(clazz) 1172 verify_listener_last(clazz) 1173 verify_resource_names(clazz) 1174 verify_files(clazz) 1175 verify_manager_list(clazz) 1176 verify_abstract_inner(clazz) 1177 verify_runtime_exceptions(clazz) 1178 verify_error(clazz) 1179 verify_units(clazz) 1180 verify_closable(clazz) 1181 1182 1183def examine_stream(stream): 1184 """Find all style issues in the given API stream.""" 1185 global failures 1186 failures = {} 1187 _parse_stream(stream, examine_clazz) 1188 return failures 1189 1190 1191def examine_api(api): 1192 """Find all style issues in the given parsed API.""" 1193 global failures 1194 failures = {} 1195 for key in sorted(api.keys()): 1196 examine_clazz(api[key]) 1197 return failures 1198 1199 1200def verify_compat(cur, prev): 1201 """Find any incompatible API changes between two levels.""" 1202 global failures 1203 1204 def class_exists(api, test): 1205 return test.fullname in api 1206 1207 def ctor_exists(api, clazz, test): 1208 for m in clazz.ctors: 1209 if m.ident == test.ident: return True 1210 return False 1211 1212 def all_methods(api, clazz): 1213 methods = list(clazz.methods) 1214 if clazz.extends is not None: 1215 methods.extend(all_methods(api, api[clazz.extends])) 1216 return methods 1217 1218 def method_exists(api, clazz, test): 1219 methods = all_methods(api, clazz) 1220 for m in methods: 1221 if m.ident == test.ident: return True 1222 return False 1223 1224 def field_exists(api, clazz, test): 1225 for f in clazz.fields: 1226 if f.ident == test.ident: return True 1227 return False 1228 1229 failures = {} 1230 for key in sorted(prev.keys()): 1231 prev_clazz = prev[key] 1232 1233 if not class_exists(cur, prev_clazz): 1234 error(prev_clazz, None, None, "Class removed or incompatible change") 1235 continue 1236 1237 cur_clazz = cur[key] 1238 1239 for test in prev_clazz.ctors: 1240 if not ctor_exists(cur, cur_clazz, test): 1241 error(prev_clazz, prev_ctor, None, "Constructor removed or incompatible change") 1242 1243 methods = all_methods(prev, prev_clazz) 1244 for test in methods: 1245 if not method_exists(cur, cur_clazz, test): 1246 error(prev_clazz, test, None, "Method removed or incompatible change") 1247 1248 for test in prev_clazz.fields: 1249 if not field_exists(cur, cur_clazz, test): 1250 error(prev_clazz, test, None, "Field removed or incompatible change") 1251 1252 return failures 1253 1254 1255if __name__ == "__main__": 1256 parser = argparse.ArgumentParser(description="Enforces common Android public API design \ 1257 patterns. It ignores lint messages from a previous API level, if provided.") 1258 parser.add_argument("current.txt", type=argparse.FileType('r'), help="current.txt") 1259 parser.add_argument("previous.txt", nargs='?', type=argparse.FileType('r'), default=None, 1260 help="previous.txt") 1261 parser.add_argument("--no-color", action='store_const', const=True, 1262 help="Disable terminal colors") 1263 parser.add_argument("--allow-google", action='store_const', const=True, 1264 help="Allow references to Google") 1265 args = vars(parser.parse_args()) 1266 1267 if args['no_color']: 1268 USE_COLOR = False 1269 1270 if args['allow_google']: 1271 ALLOW_GOOGLE = True 1272 1273 current_file = args['current.txt'] 1274 previous_file = args['previous.txt'] 1275 1276 with current_file as f: 1277 cur_fail = examine_stream(f) 1278 if not previous_file is None: 1279 with previous_file as f: 1280 prev_fail = examine_stream(f) 1281 1282 # ignore errors from previous API level 1283 for p in prev_fail: 1284 if p in cur_fail: 1285 del cur_fail[p] 1286 1287 """ 1288 # NOTE: disabled because of memory pressure 1289 # look for compatibility issues 1290 compat_fail = verify_compat(cur, prev) 1291 1292 print "%s API compatibility issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True))) 1293 for f in sorted(compat_fail): 1294 print compat_fail[f] 1295 print 1296 """ 1297 1298 print "%s API style issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True))) 1299 for f in sorted(cur_fail): 1300 print cur_fail[f] 1301 print 1302