blob: 1330c28099fa822c4a2fce121a7ee03da4db878a [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
Jeff Sharkey1498f9c2014-09-04 12:45:33 -070023
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
Jeff Sharkey8190f4882014-08-28 12:24:07 -070027"""
28
Jeff Sharkey1498f9c2014-09-04 12:45:33 -070029import re, sys, collections, traceback
Jeff Sharkey8190f4882014-08-28 12:24:07 -070030
31
32BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
33
34def format(fg=None, bg=None, bright=False, bold=False, dim=False, reset=False):
35 # manually derived from http://en.wikipedia.org/wiki/ANSI_escape_code#Codes
Jeff Sharkey1498f9c2014-09-04 12:45:33 -070036 if "--no-color" in sys.argv: return ""
Jeff Sharkey8190f4882014-08-28 12:24:07 -070037 codes = []
38 if reset: codes.append("0")
39 else:
40 if not fg is None: codes.append("3%d" % (fg))
41 if not bg is None:
42 if not bright: codes.append("4%d" % (bg))
43 else: codes.append("10%d" % (bg))
44 if bold: codes.append("1")
45 elif dim: codes.append("2")
46 else: codes.append("22")
47 return "\033[%sm" % (";".join(codes))
48
49
50class Field():
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -070051 def __init__(self, clazz, line, raw, blame):
Jeff Sharkey8190f4882014-08-28 12:24:07 -070052 self.clazz = clazz
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -070053 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -070054 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -070055 self.blame = blame
Jeff Sharkey8190f4882014-08-28 12:24:07 -070056
57 raw = raw.split()
58 self.split = list(raw)
59
60 for r in ["field", "volatile", "transient", "public", "protected", "static", "final", "deprecated"]:
61 while r in raw: raw.remove(r)
62
63 self.typ = raw[0]
64 self.name = raw[1].strip(";")
65 if len(raw) >= 4 and raw[2] == "=":
66 self.value = raw[3].strip(';"')
67 else:
68 self.value = None
69
Jeff Sharkey037458a2014-09-04 15:46:20 -070070 self.ident = self.raw.replace(" deprecated ", " ")
71
Jeff Sharkey8190f4882014-08-28 12:24:07 -070072 def __repr__(self):
73 return self.raw
74
75
76class Method():
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -070077 def __init__(self, clazz, line, raw, blame):
Jeff Sharkey8190f4882014-08-28 12:24:07 -070078 self.clazz = clazz
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -070079 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -070080 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -070081 self.blame = blame
82
83 # drop generics for now
84 raw = re.sub("<.+?>", "", raw)
Jeff Sharkey8190f4882014-08-28 12:24:07 -070085
86 raw = re.split("[\s(),;]+", raw)
87 for r in ["", ";"]:
88 while r in raw: raw.remove(r)
89 self.split = list(raw)
90
91 for r in ["method", "public", "protected", "static", "final", "deprecated", "abstract"]:
92 while r in raw: raw.remove(r)
93
94 self.typ = raw[0]
95 self.name = raw[1]
96 self.args = []
97 for r in raw[2:]:
98 if r == "throws": break
99 self.args.append(r)
100
Jeff Sharkey037458a2014-09-04 15:46:20 -0700101 # identity for compat purposes
102 ident = self.raw
103 ident = ident.replace(" deprecated ", " ")
104 ident = ident.replace(" synchronized ", " ")
105 ident = re.sub("<.+?>", "", ident)
106 if " throws " in ident:
107 ident = ident[:ident.index(" throws ")]
108 self.ident = ident
109
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700110 def __repr__(self):
111 return self.raw
112
113
114class Class():
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700115 def __init__(self, pkg, line, raw, blame):
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700116 self.pkg = pkg
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700117 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700118 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700119 self.blame = blame
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700120 self.ctors = []
121 self.fields = []
122 self.methods = []
123
124 raw = raw.split()
125 self.split = list(raw)
126 if "class" in raw:
127 self.fullname = raw[raw.index("class")+1]
128 elif "interface" in raw:
129 self.fullname = raw[raw.index("interface")+1]
Jeff Sharkey037458a2014-09-04 15:46:20 -0700130 else:
131 raise ValueError("Funky class type %s" % (self.raw))
132
133 if "extends" in raw:
134 self.extends = raw[raw.index("extends")+1]
135 else:
136 self.extends = None
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700137
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700138 self.fullname = self.pkg.name + "." + self.fullname
139 self.name = self.fullname[self.fullname.rindex(".")+1:]
140
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700141 def __repr__(self):
142 return self.raw
143
144
145class Package():
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700146 def __init__(self, line, raw, blame):
147 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700148 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700149 self.blame = blame
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700150
151 raw = raw.split()
152 self.name = raw[raw.index("package")+1]
153
154 def __repr__(self):
155 return self.raw
156
157
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700158def parse_api(f):
159 line = 0
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700160 api = {}
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700161 pkg = None
162 clazz = None
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700163 blame = None
164
165 re_blame = re.compile("^([a-z0-9]{7,}) \(<([^>]+)>.+?\) (.+?)$")
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700166 for raw in f.readlines():
167 line += 1
168 raw = raw.rstrip()
169 match = re_blame.match(raw)
170 if match is not None:
171 blame = match.groups()[0:2]
172 raw = match.groups()[2]
173 else:
174 blame = None
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700175
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700176 if raw.startswith("package"):
177 pkg = Package(line, raw, blame)
178 elif raw.startswith(" ") and raw.endswith("{"):
179 clazz = Class(pkg, line, raw, blame)
180 api[clazz.fullname] = clazz
181 elif raw.startswith(" ctor"):
182 clazz.ctors.append(Method(clazz, line, raw, blame))
183 elif raw.startswith(" method"):
184 clazz.methods.append(Method(clazz, line, raw, blame))
185 elif raw.startswith(" field"):
186 clazz.fields.append(Field(clazz, line, raw, blame))
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700187
188 return api
189
190
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700191def parse_api_file(fn):
192 with open(fn) as f:
193 return parse_api(f)
194
195
196class Failure():
197 def __init__(self, sig, clazz, detail, error, msg):
198 self.sig = sig
199 self.clazz = clazz
200 self.detail = detail
201 self.error = error
202 self.msg = msg
203
204 if error:
205 dump = "%sError:%s %s" % (format(fg=RED, bg=BLACK, bold=True), format(reset=True), msg)
206 else:
207 dump = "%sWarning:%s %s" % (format(fg=YELLOW, bg=BLACK, bold=True), format(reset=True), msg)
208
209 self.line = clazz.line
210 blame = clazz.blame
211 if detail is not None:
212 dump += "\n in " + repr(detail)
213 self.line = detail.line
214 blame = detail.blame
215 dump += "\n in " + repr(clazz)
216 dump += "\n in " + repr(clazz.pkg)
217 dump += "\n at line " + repr(self.line)
218 if blame is not None:
219 dump += "\n last modified by %s in %s" % (blame[1], blame[0])
220
221 self.dump = dump
222
223 def __repr__(self):
224 return self.dump
225
226
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700227failures = {}
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700228
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700229def _fail(clazz, detail, error, msg):
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700230 """Records an API failure to be processed later."""
231 global failures
232
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700233 sig = "%s-%s-%s" % (clazz.fullname, repr(detail), msg)
234 sig = sig.replace(" deprecated ", " ")
235
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700236 failures[sig] = Failure(sig, clazz, detail, error, msg)
237
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700238
239def warn(clazz, detail, msg):
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700240 _fail(clazz, detail, False, msg)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700241
242def error(clazz, detail, msg):
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700243 _fail(clazz, detail, True, msg)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700244
245
246def verify_constants(clazz):
247 """All static final constants must be FOO_NAME style."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700248 if re.match("android\.R\.[a-z]+", clazz.fullname): return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700249
250 for f in clazz.fields:
251 if "static" in f.split and "final" in f.split:
252 if re.match("[A-Z0-9_]+", f.name) is None:
253 error(clazz, f, "Constant field names should be FOO_NAME")
254
255
256def verify_enums(clazz):
257 """Enums are bad, mmkay?"""
258 if "extends java.lang.Enum" in clazz.raw:
259 error(clazz, None, "Enums are not allowed")
260
261
262def verify_class_names(clazz):
263 """Try catching malformed class names like myMtp or MTPUser."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700264 if clazz.fullname.startswith("android.opengl"): return
265 if clazz.fullname.startswith("android.renderscript"): return
266 if re.match("android\.R\.[a-z]+", clazz.fullname): return
267
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700268 if re.search("[A-Z]{2,}", clazz.name) is not None:
269 warn(clazz, None, "Class name style should be Mtp not MTP")
270 if re.match("[^A-Z]", clazz.name):
271 error(clazz, None, "Class must start with uppercase char")
272
273
274def verify_method_names(clazz):
275 """Try catching malformed method names, like Foo() or getMTU()."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700276 if clazz.fullname.startswith("android.opengl"): return
277 if clazz.fullname.startswith("android.renderscript"): return
278 if clazz.fullname == "android.system.OsConstants": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700279
280 for m in clazz.methods:
281 if re.search("[A-Z]{2,}", m.name) is not None:
282 warn(clazz, m, "Method name style should be getMtu() instead of getMTU()")
283 if re.match("[^a-z]", m.name):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700284 error(clazz, m, "Method name must start with lowercase char")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700285
286
287def verify_callbacks(clazz):
288 """Verify Callback classes.
289 All callback classes must be abstract.
290 All methods must follow onFoo() naming style."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700291 if clazz.fullname == "android.speech.tts.SynthesisCallback": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700292
293 if clazz.name.endswith("Callbacks"):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700294 error(clazz, None, "Class name must not be plural")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700295 if clazz.name.endswith("Observer"):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700296 warn(clazz, None, "Class should be named FooCallback")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700297
298 if clazz.name.endswith("Callback"):
299 if "interface" in clazz.split:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700300 error(clazz, None, "Callback must be abstract class to enable extension in future API levels")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700301
302 for m in clazz.methods:
303 if not re.match("on[A-Z][a-z]*", m.name):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700304 error(clazz, m, "Callback method names must be onFoo() style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700305
306
307def verify_listeners(clazz):
308 """Verify Listener classes.
309 All Listener classes must be interface.
310 All methods must follow onFoo() naming style.
311 If only a single method, it must match class name:
312 interface OnFooListener { void onFoo() }"""
313
314 if clazz.name.endswith("Listener"):
315 if " abstract class " in clazz.raw:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700316 error(clazz, None, "Listener should be an interface, otherwise renamed Callback")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700317
318 for m in clazz.methods:
319 if not re.match("on[A-Z][a-z]*", m.name):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700320 error(clazz, m, "Listener method names must be onFoo() style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700321
322 if len(clazz.methods) == 1 and clazz.name.startswith("On"):
323 m = clazz.methods[0]
324 if (m.name + "Listener").lower() != clazz.name.lower():
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700325 error(clazz, m, "Single listener method name should match class name")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700326
327
328def verify_actions(clazz):
329 """Verify intent actions.
330 All action names must be named ACTION_FOO.
331 All action values must be scoped by package and match name:
332 package android.foo {
333 String ACTION_BAR = "android.foo.action.BAR";
334 }"""
335 for f in clazz.fields:
336 if f.value is None: continue
337 if f.name.startswith("EXTRA_"): continue
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700338 if f.name == "SERVICE_INTERFACE" or f.name == "PROVIDER_INTERFACE": continue
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700339
340 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
341 if "_ACTION" in f.name or "ACTION_" in f.name or ".action." in f.value.lower():
342 if not f.name.startswith("ACTION_"):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700343 error(clazz, f, "Intent action constant name must be ACTION_FOO")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700344 else:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700345 if clazz.fullname == "android.content.Intent":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700346 prefix = "android.intent.action"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700347 elif clazz.fullname == "android.provider.Settings":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700348 prefix = "android.settings"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700349 elif clazz.fullname == "android.app.admin.DevicePolicyManager" or clazz.fullname == "android.app.admin.DeviceAdminReceiver":
350 prefix = "android.app.action"
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700351 else:
352 prefix = clazz.pkg.name + ".action"
353 expected = prefix + "." + f.name[7:]
354 if f.value != expected:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700355 error(clazz, f, "Inconsistent action value; expected %s" % (expected))
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700356
357
358def verify_extras(clazz):
359 """Verify intent extras.
360 All extra names must be named EXTRA_FOO.
361 All extra values must be scoped by package and match name:
362 package android.foo {
363 String EXTRA_BAR = "android.foo.extra.BAR";
364 }"""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700365 if clazz.fullname == "android.app.Notification": return
366 if clazz.fullname == "android.appwidget.AppWidgetManager": return
367
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700368 for f in clazz.fields:
369 if f.value is None: continue
370 if f.name.startswith("ACTION_"): continue
371
372 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
373 if "_EXTRA" in f.name or "EXTRA_" in f.name or ".extra" in f.value.lower():
374 if not f.name.startswith("EXTRA_"):
375 error(clazz, f, "Intent extra must be EXTRA_FOO")
376 else:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700377 if clazz.pkg.name == "android.content" and clazz.name == "Intent":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700378 prefix = "android.intent.extra"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700379 elif clazz.pkg.name == "android.app.admin":
380 prefix = "android.app.extra"
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700381 else:
382 prefix = clazz.pkg.name + ".extra"
383 expected = prefix + "." + f.name[6:]
384 if f.value != expected:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700385 error(clazz, f, "Inconsistent extra value; expected %s" % (expected))
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700386
387
388def verify_equals(clazz):
389 """Verify that equals() and hashCode() must be overridden together."""
390 methods = [ m.name for m in clazz.methods ]
391 eq = "equals" in methods
392 hc = "hashCode" in methods
393 if eq != hc:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700394 error(clazz, None, "Must override both equals and hashCode; missing one")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700395
396
397def verify_parcelable(clazz):
398 """Verify that Parcelable objects aren't hiding required bits."""
399 if "implements android.os.Parcelable" in clazz.raw:
400 creator = [ i for i in clazz.fields if i.name == "CREATOR" ]
401 write = [ i for i in clazz.methods if i.name == "writeToParcel" ]
402 describe = [ i for i in clazz.methods if i.name == "describeContents" ]
403
404 if len(creator) == 0 or len(write) == 0 or len(describe) == 0:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700405 error(clazz, None, "Parcelable requires CREATOR, writeToParcel, and describeContents; missing one")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700406
407
408def verify_protected(clazz):
409 """Verify that no protected methods are allowed."""
410 for m in clazz.methods:
411 if "protected" in m.split:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700412 error(clazz, m, "No protected methods; must be public")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700413 for f in clazz.fields:
414 if "protected" in f.split:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700415 error(clazz, f, "No protected fields; must be public")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700416
417
418def verify_fields(clazz):
419 """Verify that all exposed fields are final.
420 Exposed fields must follow myName style.
421 Catch internal mFoo objects being exposed."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700422
423 IGNORE_BARE_FIELDS = [
424 "android.app.ActivityManager.RecentTaskInfo",
425 "android.app.Notification",
426 "android.content.pm.ActivityInfo",
427 "android.content.pm.ApplicationInfo",
428 "android.content.pm.FeatureGroupInfo",
429 "android.content.pm.InstrumentationInfo",
430 "android.content.pm.PackageInfo",
431 "android.content.pm.PackageItemInfo",
432 "android.os.Message",
433 "android.system.StructPollfd",
434 ]
435
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700436 for f in clazz.fields:
437 if not "final" in f.split:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700438 if clazz.fullname in IGNORE_BARE_FIELDS:
439 pass
440 elif clazz.fullname.endswith("LayoutParams"):
441 pass
442 elif clazz.fullname.startswith("android.util.Mutable"):
443 pass
444 else:
445 error(clazz, f, "Bare fields must be marked final; consider adding accessors")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700446
447 if not "static" in f.split:
448 if not re.match("[a-z]([a-zA-Z]+)?", f.name):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700449 error(clazz, f, "Non-static fields must be named with myField style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700450
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700451 if re.match("[ms][A-Z]", f.name):
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700452 error(clazz, f, "Don't expose your internal objects")
453
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700454 if re.match("[A-Z_]+", f.name):
455 if "static" not in f.split or "final" not in f.split:
456 error(clazz, f, "Constants must be marked static final")
457
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700458
459def verify_register(clazz):
460 """Verify parity of registration methods.
461 Callback objects use register/unregister methods.
462 Listener objects use add/remove methods."""
463 methods = [ m.name for m in clazz.methods ]
464 for m in clazz.methods:
465 if "Callback" in m.raw:
466 if m.name.startswith("register"):
467 other = "unregister" + m.name[8:]
468 if other not in methods:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700469 error(clazz, m, "Missing unregister method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700470 if m.name.startswith("unregister"):
471 other = "register" + m.name[10:]
472 if other not in methods:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700473 error(clazz, m, "Missing register method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700474
475 if m.name.startswith("add") or m.name.startswith("remove"):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700476 error(clazz, m, "Callback methods should be named register/unregister")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700477
478 if "Listener" in m.raw:
479 if m.name.startswith("add"):
480 other = "remove" + m.name[3:]
481 if other not in methods:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700482 error(clazz, m, "Missing remove method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700483 if m.name.startswith("remove") and not m.name.startswith("removeAll"):
484 other = "add" + m.name[6:]
485 if other not in methods:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700486 error(clazz, m, "Missing add method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700487
488 if m.name.startswith("register") or m.name.startswith("unregister"):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700489 error(clazz, m, "Listener methods should be named add/remove")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700490
491
492def verify_sync(clazz):
493 """Verify synchronized methods aren't exposed."""
494 for m in clazz.methods:
495 if "synchronized" in m.split:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700496 error(clazz, m, "Internal lock exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700497
498
499def verify_intent_builder(clazz):
500 """Verify that Intent builders are createFooIntent() style."""
501 if clazz.name == "Intent": return
502
503 for m in clazz.methods:
504 if m.typ == "android.content.Intent":
505 if m.name.startswith("create") and m.name.endswith("Intent"):
506 pass
507 else:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700508 error(clazz, m, "Methods creating an Intent should be named createFooIntent()")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700509
510
511def verify_helper_classes(clazz):
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700512 """Verify that helper classes are named consistently with what they extend.
513 All developer extendable methods should be named onFoo()."""
514 test_methods = False
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700515 if "extends android.app.Service" in clazz.raw:
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700516 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700517 if not clazz.name.endswith("Service"):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700518 error(clazz, None, "Inconsistent class name; should be FooService")
519
520 found = False
521 for f in clazz.fields:
522 if f.name == "SERVICE_INTERFACE":
523 found = True
524 if f.value != clazz.fullname:
525 error(clazz, f, "Inconsistent interface constant; expected %s" % (clazz.fullname))
526
527 if not found:
528 warn(clazz, None, "Missing SERVICE_INTERFACE constant")
529
530 if "abstract" in clazz.split and not clazz.fullname.startswith("android.service."):
531 warn(clazz, None, "Services extended by developers should be under android.service")
532
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700533 if "extends android.content.ContentProvider" in clazz.raw:
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700534 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700535 if not clazz.name.endswith("Provider"):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700536 error(clazz, None, "Inconsistent class name; should be FooProvider")
537
538 found = False
539 for f in clazz.fields:
540 if f.name == "PROVIDER_INTERFACE":
541 found = True
542 if f.value != clazz.fullname:
543 error(clazz, f, "Inconsistent interface name; expected %s" % (clazz.fullname))
544
545 if not found:
546 warn(clazz, None, "Missing PROVIDER_INTERFACE constant")
547
548 if "abstract" in clazz.split and not clazz.fullname.startswith("android.provider."):
549 warn(clazz, None, "Providers extended by developers should be under android.provider")
550
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700551 if "extends android.content.BroadcastReceiver" in clazz.raw:
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700552 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700553 if not clazz.name.endswith("Receiver"):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700554 error(clazz, None, "Inconsistent class name; should be FooReceiver")
555
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700556 if "extends android.app.Activity" in clazz.raw:
557 test_methods = True
558 if not clazz.name.endswith("Activity"):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700559 error(clazz, None, "Inconsistent class name; should be FooActivity")
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700560
561 if test_methods:
562 for m in clazz.methods:
563 if "final" in m.split: continue
564 if not re.match("on[A-Z]", m.name):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700565 if "abstract" in m.split:
566 error(clazz, m, "Methods implemented by developers must be named onFoo()")
567 else:
568 warn(clazz, m, "If implemented by developer, should be named onFoo(); otherwise consider marking final")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700569
570
571def verify_builder(clazz):
572 """Verify builder classes.
573 Methods should return the builder to enable chaining."""
574 if " extends " in clazz.raw: return
575 if not clazz.name.endswith("Builder"): return
576
577 if clazz.name != "Builder":
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700578 warn(clazz, None, "Builder should be defined as inner class")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700579
580 has_build = False
581 for m in clazz.methods:
582 if m.name == "build":
583 has_build = True
584 continue
585
586 if m.name.startswith("get"): continue
587 if m.name.startswith("clear"): continue
588
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700589 if m.name.startswith("with"):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700590 error(clazz, m, "Builder methods names must follow setFoo() style")
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700591
592 if m.name.startswith("set"):
593 if not m.typ.endswith(clazz.fullname):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700594 warn(clazz, m, "Methods should return the builder")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700595
596 if not has_build:
597 warn(clazz, None, "Missing build() method")
598
599
600def verify_aidl(clazz):
601 """Catch people exposing raw AIDL."""
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700602 if "extends android.os.Binder" in clazz.raw or "implements android.os.IInterface" in clazz.raw:
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700603 error(clazz, None, "Exposing raw AIDL interface")
604
605
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700606def verify_internal(clazz):
607 """Catch people exposing internal classes."""
608 if clazz.pkg.name.startswith("com.android"):
609 error(clazz, None, "Exposing internal class")
610
611
612def verify_layering(clazz):
613 """Catch package layering violations.
614 For example, something in android.os depending on android.app."""
615 ranking = [
616 ["android.service","android.accessibilityservice","android.inputmethodservice","android.printservice","android.appwidget","android.webkit","android.preference","android.gesture","android.print"],
617 "android.app",
618 "android.widget",
619 "android.view",
620 "android.animation",
621 "android.provider",
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700622 ["android.content","android.graphics.drawable"],
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700623 "android.database",
624 "android.graphics",
625 "android.text",
626 "android.os",
627 "android.util"
628 ]
629
630 def rank(p):
631 for i in range(len(ranking)):
632 if isinstance(ranking[i], list):
633 for j in ranking[i]:
634 if p.startswith(j): return i
635 else:
636 if p.startswith(ranking[i]): return i
637
638 cr = rank(clazz.pkg.name)
639 if cr is None: return
640
641 for f in clazz.fields:
642 ir = rank(f.typ)
643 if ir and ir < cr:
644 warn(clazz, f, "Field type violates package layering")
645
646 for m in clazz.methods:
647 ir = rank(m.typ)
648 if ir and ir < cr:
649 warn(clazz, m, "Method return type violates package layering")
650 for arg in m.args:
651 ir = rank(arg)
652 if ir and ir < cr:
653 warn(clazz, m, "Method argument type violates package layering")
654
655
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700656def verify_boolean(clazz, api):
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700657 """Catches people returning boolean from getFoo() style methods.
658 Ignores when matching setFoo() is present."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700659
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700660 methods = [ m.name for m in clazz.methods ]
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700661
662 builder = clazz.fullname + ".Builder"
663 builder_methods = []
664 if builder in api:
665 builder_methods = [ m.name for m in api[builder].methods ]
666
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700667 for m in clazz.methods:
668 if m.typ == "boolean" and m.name.startswith("get") and m.name != "get" and len(m.args) == 0:
669 setter = "set" + m.name[3:]
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700670 if setter in methods:
671 pass
672 elif builder is not None and setter in builder_methods:
673 pass
674 else:
675 warn(clazz, m, "Methods returning boolean should be named isFoo, hasFoo, areFoo")
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700676
677
678def verify_collections(clazz):
679 """Verifies that collection types are interfaces."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700680 if clazz.fullname == "android.os.Bundle": return
681
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700682 bad = ["java.util.Vector", "java.util.LinkedList", "java.util.ArrayList", "java.util.Stack",
683 "java.util.HashMap", "java.util.HashSet", "android.util.ArraySet", "android.util.ArrayMap"]
684 for m in clazz.methods:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700685 if m.typ in bad:
686 error(clazz, m, "Return type is concrete collection; should be interface")
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700687 for arg in m.args:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700688 if arg in bad:
689 error(clazz, m, "Argument is concrete collection; should be interface")
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700690
691
692def verify_flags(clazz):
693 """Verifies that flags are non-overlapping."""
694 known = collections.defaultdict(int)
695 for f in clazz.fields:
696 if "FLAG_" in f.name:
697 try:
698 val = int(f.value)
699 except:
700 continue
701
702 scope = f.name[0:f.name.index("FLAG_")]
703 if val & known[scope]:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700704 warn(clazz, f, "Found overlapping flag constant value")
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700705 known[scope] |= val
706
707
Jeff Sharkey037458a2014-09-04 15:46:20 -0700708def verify_style(api):
709 """Find all style issues in the given API level."""
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700710 global failures
711
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700712 failures = {}
713 for key in sorted(api.keys()):
714 clazz = api[key]
715
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700716 if clazz.pkg.name.startswith("java"): continue
717 if clazz.pkg.name.startswith("junit"): continue
718 if clazz.pkg.name.startswith("org.apache"): continue
719 if clazz.pkg.name.startswith("org.xml"): continue
720 if clazz.pkg.name.startswith("org.json"): continue
721 if clazz.pkg.name.startswith("org.w3c"): continue
722
723 verify_constants(clazz)
724 verify_enums(clazz)
725 verify_class_names(clazz)
726 verify_method_names(clazz)
727 verify_callbacks(clazz)
728 verify_listeners(clazz)
729 verify_actions(clazz)
730 verify_extras(clazz)
731 verify_equals(clazz)
732 verify_parcelable(clazz)
733 verify_protected(clazz)
734 verify_fields(clazz)
735 verify_register(clazz)
736 verify_sync(clazz)
737 verify_intent_builder(clazz)
738 verify_helper_classes(clazz)
739 verify_builder(clazz)
740 verify_aidl(clazz)
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700741 verify_internal(clazz)
742 verify_layering(clazz)
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700743 verify_boolean(clazz, api)
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700744 verify_collections(clazz)
745 verify_flags(clazz)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700746
747 return failures
748
749
Jeff Sharkey037458a2014-09-04 15:46:20 -0700750def verify_compat(cur, prev):
751 """Find any incompatible API changes between two levels."""
752 global failures
753
754 def class_exists(api, test):
755 return test.fullname in api
756
757 def ctor_exists(api, clazz, test):
758 for m in clazz.ctors:
759 if m.ident == test.ident: return True
760 return False
761
762 def all_methods(api, clazz):
763 methods = list(clazz.methods)
764 if clazz.extends is not None:
765 methods.extend(all_methods(api, api[clazz.extends]))
766 return methods
767
768 def method_exists(api, clazz, test):
769 methods = all_methods(api, clazz)
770 for m in methods:
771 if m.ident == test.ident: return True
772 return False
773
774 def field_exists(api, clazz, test):
775 for f in clazz.fields:
776 if f.ident == test.ident: return True
777 return False
778
779 failures = {}
780 for key in sorted(prev.keys()):
781 prev_clazz = prev[key]
782
783 if not class_exists(cur, prev_clazz):
784 error(prev_clazz, None, "Class removed or incompatible change")
785 continue
786
787 cur_clazz = cur[key]
788
789 for test in prev_clazz.ctors:
790 if not ctor_exists(cur, cur_clazz, test):
791 error(prev_clazz, prev_ctor, "Constructor removed or incompatible change")
792
793 methods = all_methods(prev, prev_clazz)
794 for test in methods:
795 if not method_exists(cur, cur_clazz, test):
796 error(prev_clazz, test, "Method removed or incompatible change")
797
798 for test in prev_clazz.fields:
799 if not field_exists(cur, cur_clazz, test):
800 error(prev_clazz, test, "Field removed or incompatible change")
801
802 return failures
803
804
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700805if __name__ == "__main__":
806 cur = parse_api_file(sys.argv[1])
807 cur_fail = verify_style(cur)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700808
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700809 if len(sys.argv) > 2:
810 prev = parse_api_file(sys.argv[2])
811 prev_fail = verify_style(prev)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700812
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700813 # ignore errors from previous API level
814 for p in prev_fail:
815 if p in cur_fail:
816 del cur_fail[p]
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700817
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700818 # look for compatibility issues
819 compat_fail = verify_compat(cur, prev)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700820
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700821 print "%s API compatibility issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
822 for f in sorted(compat_fail):
823 print compat_fail[f]
824 print
825
826
827 print "%s API style issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
828 for f in sorted(cur_fail):
829 print cur_fail[f]
Jeff Sharkey037458a2014-09-04 15:46:20 -0700830 print