blob: fce43235ae5737b7a9e964a735ee67bd0a3be602 [file] [log] [blame]
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001#!/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"""
24
25import re, sys
26
27
28BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
29
30def format(fg=None, bg=None, bright=False, bold=False, dim=False, reset=False):
31 # manually derived from http://en.wikipedia.org/wiki/ANSI_escape_code#Codes
32 codes = []
33 if reset: codes.append("0")
34 else:
35 if not fg is None: codes.append("3%d" % (fg))
36 if not bg is None:
37 if not bright: codes.append("4%d" % (bg))
38 else: codes.append("10%d" % (bg))
39 if bold: codes.append("1")
40 elif dim: codes.append("2")
41 else: codes.append("22")
42 return "\033[%sm" % (";".join(codes))
43
44
45class Field():
46 def __init__(self, clazz, raw):
47 self.clazz = clazz
48 self.raw = raw.strip(" {;")
49
50 raw = raw.split()
51 self.split = list(raw)
52
53 for r in ["field", "volatile", "transient", "public", "protected", "static", "final", "deprecated"]:
54 while r in raw: raw.remove(r)
55
56 self.typ = raw[0]
57 self.name = raw[1].strip(";")
58 if len(raw) >= 4 and raw[2] == "=":
59 self.value = raw[3].strip(';"')
60 else:
61 self.value = None
62
63 def __repr__(self):
64 return self.raw
65
66
67class Method():
68 def __init__(self, clazz, raw):
69 self.clazz = clazz
70 self.raw = raw.strip(" {;")
71
72 raw = re.split("[\s(),;]+", raw)
73 for r in ["", ";"]:
74 while r in raw: raw.remove(r)
75 self.split = list(raw)
76
77 for r in ["method", "public", "protected", "static", "final", "deprecated", "abstract"]:
78 while r in raw: raw.remove(r)
79
80 self.typ = raw[0]
81 self.name = raw[1]
82 self.args = []
83 for r in raw[2:]:
84 if r == "throws": break
85 self.args.append(r)
86
87 def __repr__(self):
88 return self.raw
89
90
91class Class():
92 def __init__(self, pkg, raw):
93 self.pkg = pkg
94 self.raw = raw.strip(" {;")
95 self.ctors = []
96 self.fields = []
97 self.methods = []
98
99 raw = raw.split()
100 self.split = list(raw)
101 if "class" in raw:
102 self.fullname = raw[raw.index("class")+1]
103 elif "interface" in raw:
104 self.fullname = raw[raw.index("interface")+1]
105
106 if "." in self.fullname:
107 self.name = self.fullname[self.fullname.rindex(".")+1:]
108 else:
109 self.name = self.fullname
110
111 def __repr__(self):
112 return self.raw
113
114
115class Package():
116 def __init__(self, raw):
117 self.raw = raw.strip(" {;")
118
119 raw = raw.split()
120 self.name = raw[raw.index("package")+1]
121
122 def __repr__(self):
123 return self.raw
124
125
126def parse_api(fn):
127 api = []
128 pkg = None
129 clazz = None
130
131 with open(fn) as f:
132 for raw in f.readlines():
133 raw = raw.rstrip()
134
135 if raw.startswith("package"):
136 pkg = Package(raw)
137 elif raw.startswith(" ") and raw.endswith("{"):
138 clazz = Class(pkg, raw)
139 api.append(clazz)
140 elif raw.startswith(" ctor"):
141 clazz.ctors.append(Method(clazz, raw))
142 elif raw.startswith(" method"):
143 clazz.methods.append(Method(clazz, raw))
144 elif raw.startswith(" field"):
145 clazz.fields.append(Field(clazz, raw))
146
147 return api
148
149
150failures = []
151
152def _fail(clazz, detail, msg):
153 """Records an API failure to be processed later."""
154 global failures
155
156 res = msg
157 if detail is not None:
158 res += "\n in " + repr(detail)
159 res += "\n in " + repr(clazz)
160 res += "\n in " + repr(clazz.pkg)
161 failures.append(res)
162
163def warn(clazz, detail, msg):
164 _fail(clazz, detail, "%sWarning:%s %s" % (format(fg=YELLOW, bg=BLACK), format(reset=True), msg))
165
166def error(clazz, detail, msg):
167 _fail(clazz, detail, "%sError:%s %s" % (format(fg=RED, bg=BLACK), format(reset=True), msg))
168
169
170def verify_constants(clazz):
171 """All static final constants must be FOO_NAME style."""
172 if re.match("R\.[a-z]+", clazz.fullname): return
173
174 for f in clazz.fields:
175 if "static" in f.split and "final" in f.split:
176 if re.match("[A-Z0-9_]+", f.name) is None:
177 error(clazz, f, "Constant field names should be FOO_NAME")
178
179
180def verify_enums(clazz):
181 """Enums are bad, mmkay?"""
182 if "extends java.lang.Enum" in clazz.raw:
183 error(clazz, None, "Enums are not allowed")
184
185
186def verify_class_names(clazz):
187 """Try catching malformed class names like myMtp or MTPUser."""
188 if re.search("[A-Z]{2,}", clazz.name) is not None:
189 warn(clazz, None, "Class name style should be Mtp not MTP")
190 if re.match("[^A-Z]", clazz.name):
191 error(clazz, None, "Class must start with uppercase char")
192
193
194def verify_method_names(clazz):
195 """Try catching malformed method names, like Foo() or getMTU()."""
196 if clazz.pkg.name == "android.opengl": return
197
198 for m in clazz.methods:
199 if re.search("[A-Z]{2,}", m.name) is not None:
200 warn(clazz, m, "Method name style should be getMtu() instead of getMTU()")
201 if re.match("[^a-z]", m.name):
202 error(clazz, None, "Method name must start with lowercase char")
203
204
205def verify_callbacks(clazz):
206 """Verify Callback classes.
207 All callback classes must be abstract.
208 All methods must follow onFoo() naming style."""
209
210 if clazz.name.endswith("Callbacks"):
211 error(clazz, None, "Class must be named exactly Callback")
212 if clazz.name.endswith("Observer"):
213 warn(clazz, None, "Class should be named Callback")
214
215 if clazz.name.endswith("Callback"):
216 if "interface" in clazz.split:
217 error(clazz, None, "Callback must be abstract class")
218
219 for m in clazz.methods:
220 if not re.match("on[A-Z][a-z]*", m.name):
221 error(clazz, m, "Callback method names must be onFoo style")
222
223
224def verify_listeners(clazz):
225 """Verify Listener classes.
226 All Listener classes must be interface.
227 All methods must follow onFoo() naming style.
228 If only a single method, it must match class name:
229 interface OnFooListener { void onFoo() }"""
230
231 if clazz.name.endswith("Listener"):
232 if " abstract class " in clazz.raw:
233 error(clazz, None, "Listener should be interface")
234
235 for m in clazz.methods:
236 if not re.match("on[A-Z][a-z]*", m.name):
237 error(clazz, m, "Listener method names must be onFoo style")
238
239 if len(clazz.methods) == 1 and clazz.name.startswith("On"):
240 m = clazz.methods[0]
241 if (m.name + "Listener").lower() != clazz.name.lower():
242 error(clazz, m, "Single method name should match class name")
243
244
245def verify_actions(clazz):
246 """Verify intent actions.
247 All action names must be named ACTION_FOO.
248 All action values must be scoped by package and match name:
249 package android.foo {
250 String ACTION_BAR = "android.foo.action.BAR";
251 }"""
252 for f in clazz.fields:
253 if f.value is None: continue
254 if f.name.startswith("EXTRA_"): continue
255
256 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
257 if "_ACTION" in f.name or "ACTION_" in f.name or ".action." in f.value.lower():
258 if not f.name.startswith("ACTION_"):
259 error(clazz, f, "Intent action must be ACTION_FOO")
260 else:
261 if clazz.name == "Intent":
262 prefix = "android.intent.action"
263 elif clazz.name == "Settings":
264 prefix = "android.settings"
265 else:
266 prefix = clazz.pkg.name + ".action"
267 expected = prefix + "." + f.name[7:]
268 if f.value != expected:
269 error(clazz, f, "Inconsistent action value")
270
271
272def verify_extras(clazz):
273 """Verify intent extras.
274 All extra names must be named EXTRA_FOO.
275 All extra values must be scoped by package and match name:
276 package android.foo {
277 String EXTRA_BAR = "android.foo.extra.BAR";
278 }"""
279 for f in clazz.fields:
280 if f.value is None: continue
281 if f.name.startswith("ACTION_"): continue
282
283 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
284 if "_EXTRA" in f.name or "EXTRA_" in f.name or ".extra" in f.value.lower():
285 if not f.name.startswith("EXTRA_"):
286 error(clazz, f, "Intent extra must be EXTRA_FOO")
287 else:
288 if clazz.name == "Intent":
289 prefix = "android.intent.extra"
290 else:
291 prefix = clazz.pkg.name + ".extra"
292 expected = prefix + "." + f.name[6:]
293 if f.value != expected:
294 error(clazz, f, "Inconsistent extra value")
295
296
297def verify_equals(clazz):
298 """Verify that equals() and hashCode() must be overridden together."""
299 methods = [ m.name for m in clazz.methods ]
300 eq = "equals" in methods
301 hc = "hashCode" in methods
302 if eq != hc:
303 error(clazz, None, "Must override both equals and hashCode")
304
305
306def verify_parcelable(clazz):
307 """Verify that Parcelable objects aren't hiding required bits."""
308 if "implements android.os.Parcelable" in clazz.raw:
309 creator = [ i for i in clazz.fields if i.name == "CREATOR" ]
310 write = [ i for i in clazz.methods if i.name == "writeToParcel" ]
311 describe = [ i for i in clazz.methods if i.name == "describeContents" ]
312
313 if len(creator) == 0 or len(write) == 0 or len(describe) == 0:
314 error(clazz, None, "Parcelable requires CREATOR, writeToParcel, and describeContents")
315
316
317def verify_protected(clazz):
318 """Verify that no protected methods are allowed."""
319 for m in clazz.methods:
320 if "protected" in m.split:
321 error(clazz, m, "Protected method")
322 for f in clazz.fields:
323 if "protected" in f.split:
324 error(clazz, f, "Protected field")
325
326
327def verify_fields(clazz):
328 """Verify that all exposed fields are final.
329 Exposed fields must follow myName style.
330 Catch internal mFoo objects being exposed."""
331 for f in clazz.fields:
332 if not "final" in f.split:
333 error(clazz, f, "Bare fields must be final; consider adding accessors")
334
335 if not "static" in f.split:
336 if not re.match("[a-z]([a-zA-Z]+)?", f.name):
337 error(clazz, f, "Non-static fields must be myName")
338
339 if re.match("[m][A-Z]", f.name):
340 error(clazz, f, "Don't expose your internal objects")
341
342
343def verify_register(clazz):
344 """Verify parity of registration methods.
345 Callback objects use register/unregister methods.
346 Listener objects use add/remove methods."""
347 methods = [ m.name for m in clazz.methods ]
348 for m in clazz.methods:
349 if "Callback" in m.raw:
350 if m.name.startswith("register"):
351 other = "unregister" + m.name[8:]
352 if other not in methods:
353 error(clazz, m, "Missing unregister")
354 if m.name.startswith("unregister"):
355 other = "register" + m.name[10:]
356 if other not in methods:
357 error(clazz, m, "Missing register")
358
359 if m.name.startswith("add") or m.name.startswith("remove"):
360 error(clazz, m, "Callback should be register/unregister")
361
362 if "Listener" in m.raw:
363 if m.name.startswith("add"):
364 other = "remove" + m.name[3:]
365 if other not in methods:
366 error(clazz, m, "Missing remove")
367 if m.name.startswith("remove") and not m.name.startswith("removeAll"):
368 other = "add" + m.name[6:]
369 if other not in methods:
370 error(clazz, m, "Missing add")
371
372 if m.name.startswith("register") or m.name.startswith("unregister"):
373 error(clazz, m, "Listener should be add/remove")
374
375
376def verify_sync(clazz):
377 """Verify synchronized methods aren't exposed."""
378 for m in clazz.methods:
379 if "synchronized" in m.split:
380 error(clazz, m, "Lock exposed")
381
382
383def verify_intent_builder(clazz):
384 """Verify that Intent builders are createFooIntent() style."""
385 if clazz.name == "Intent": return
386
387 for m in clazz.methods:
388 if m.typ == "android.content.Intent":
389 if m.name.startswith("create") and m.name.endswith("Intent"):
390 pass
391 else:
392 warn(clazz, m, "Should be createFooIntent()")
393
394
395def verify_helper_classes(clazz):
396 """Verify that helper classes are named consistently with what they extend."""
397 if "extends android.app.Service" in clazz.raw:
398 if not clazz.name.endswith("Service"):
399 error(clazz, None, "Inconsistent class name")
400 if "extends android.content.ContentProvider" in clazz.raw:
401 if not clazz.name.endswith("Provider"):
402 error(clazz, None, "Inconsistent class name")
403 if "extends android.content.BroadcastReceiver" in clazz.raw:
404 if not clazz.name.endswith("Receiver"):
405 error(clazz, None, "Inconsistent class name")
406
407
408def verify_builder(clazz):
409 """Verify builder classes.
410 Methods should return the builder to enable chaining."""
411 if " extends " in clazz.raw: return
412 if not clazz.name.endswith("Builder"): return
413
414 if clazz.name != "Builder":
415 warn(clazz, None, "Should be standalone Builder class")
416
417 has_build = False
418 for m in clazz.methods:
419 if m.name == "build":
420 has_build = True
421 continue
422
423 if m.name.startswith("get"): continue
424 if m.name.startswith("clear"): continue
425
426 if not m.typ.endswith(clazz.fullname):
427 warn(clazz, m, "Should return the builder")
428
429 if not has_build:
430 warn(clazz, None, "Missing build() method")
431
432
433def verify_aidl(clazz):
434 """Catch people exposing raw AIDL."""
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700435 if "extends android.os.Binder" in clazz.raw or "implements android.os.IInterface" in clazz.raw:
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700436 error(clazz, None, "Exposing raw AIDL interface")
437
438
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700439def verify_internal(clazz):
440 """Catch people exposing internal classes."""
441 if clazz.pkg.name.startswith("com.android"):
442 error(clazz, None, "Exposing internal class")
443
444
445def verify_layering(clazz):
446 """Catch package layering violations.
447 For example, something in android.os depending on android.app."""
448 ranking = [
449 ["android.service","android.accessibilityservice","android.inputmethodservice","android.printservice","android.appwidget","android.webkit","android.preference","android.gesture","android.print"],
450 "android.app",
451 "android.widget",
452 "android.view",
453 "android.animation",
454 "android.provider",
455 "android.content",
456 "android.database",
457 "android.graphics",
458 "android.text",
459 "android.os",
460 "android.util"
461 ]
462
463 def rank(p):
464 for i in range(len(ranking)):
465 if isinstance(ranking[i], list):
466 for j in ranking[i]:
467 if p.startswith(j): return i
468 else:
469 if p.startswith(ranking[i]): return i
470
471 cr = rank(clazz.pkg.name)
472 if cr is None: return
473
474 for f in clazz.fields:
475 ir = rank(f.typ)
476 if ir and ir < cr:
477 warn(clazz, f, "Field type violates package layering")
478
479 for m in clazz.methods:
480 ir = rank(m.typ)
481 if ir and ir < cr:
482 warn(clazz, m, "Method return type violates package layering")
483 for arg in m.args:
484 ir = rank(arg)
485 if ir and ir < cr:
486 warn(clazz, m, "Method argument type violates package layering")
487
488
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700489def verify_all(api):
490 global failures
491
492 failures = []
493 for clazz in api:
494 if clazz.pkg.name.startswith("java"): continue
495 if clazz.pkg.name.startswith("junit"): continue
496 if clazz.pkg.name.startswith("org.apache"): continue
497 if clazz.pkg.name.startswith("org.xml"): continue
498 if clazz.pkg.name.startswith("org.json"): continue
499 if clazz.pkg.name.startswith("org.w3c"): continue
500
501 verify_constants(clazz)
502 verify_enums(clazz)
503 verify_class_names(clazz)
504 verify_method_names(clazz)
505 verify_callbacks(clazz)
506 verify_listeners(clazz)
507 verify_actions(clazz)
508 verify_extras(clazz)
509 verify_equals(clazz)
510 verify_parcelable(clazz)
511 verify_protected(clazz)
512 verify_fields(clazz)
513 verify_register(clazz)
514 verify_sync(clazz)
515 verify_intent_builder(clazz)
516 verify_helper_classes(clazz)
517 verify_builder(clazz)
518 verify_aidl(clazz)
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700519 verify_internal(clazz)
520 verify_layering(clazz)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700521
522 return failures
523
524
525cur = parse_api(sys.argv[1])
526cur_fail = verify_all(cur)
527
528if len(sys.argv) > 2:
529 prev = parse_api(sys.argv[2])
530 prev_fail = verify_all(prev)
531
532 # ignore errors from previous API level
533 for p in prev_fail:
534 if p in cur_fail:
535 cur_fail.remove(p)
536
537
538for f in cur_fail:
539 print f
540 print