blob: dec73400f47f5a9e82bfd7c2628a8703573b3196 [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."""
435 if "extends android.os.Binder" in clazz.raw:
436 error(clazz, None, "Exposing raw AIDL interface")
437
438
439def verify_all(api):
440 global failures
441
442 failures = []
443 for clazz in api:
444 if clazz.pkg.name.startswith("java"): continue
445 if clazz.pkg.name.startswith("junit"): continue
446 if clazz.pkg.name.startswith("org.apache"): continue
447 if clazz.pkg.name.startswith("org.xml"): continue
448 if clazz.pkg.name.startswith("org.json"): continue
449 if clazz.pkg.name.startswith("org.w3c"): continue
450
451 verify_constants(clazz)
452 verify_enums(clazz)
453 verify_class_names(clazz)
454 verify_method_names(clazz)
455 verify_callbacks(clazz)
456 verify_listeners(clazz)
457 verify_actions(clazz)
458 verify_extras(clazz)
459 verify_equals(clazz)
460 verify_parcelable(clazz)
461 verify_protected(clazz)
462 verify_fields(clazz)
463 verify_register(clazz)
464 verify_sync(clazz)
465 verify_intent_builder(clazz)
466 verify_helper_classes(clazz)
467 verify_builder(clazz)
468 verify_aidl(clazz)
469
470 return failures
471
472
473cur = parse_api(sys.argv[1])
474cur_fail = verify_all(cur)
475
476if len(sys.argv) > 2:
477 prev = parse_api(sys.argv[2])
478 prev_fail = verify_all(prev)
479
480 # ignore errors from previous API level
481 for p in prev_fail:
482 if p in cur_fail:
483 cur_fail.remove(p)
484
485
486for f in cur_fail:
487 print f
488 print