blob: 53501f96d0b8bcd6f024442b8bc63e2623eb14ea [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
Adam Metcalf1c7e70a2015-03-27 16:11:23 -070029import re, sys, collections, traceback, argparse
Jeff Sharkey8190f4882014-08-28 12:24:07 -070030
31
32BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
33
Adam Metcalf1c7e70a2015-03-27 16:11:23 -070034ALLOW_GOOGLE = False
35USE_COLOR = True
36
Jeff Sharkey8190f4882014-08-28 12:24:07 -070037def 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
Adam Metcalf1c7e70a2015-03-27 16:11:23 -070039 if not USE_COLOR: return ""
Jeff Sharkey8190f4882014-08-28 12:24:07 -070040 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():
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -070054 def __init__(self, clazz, line, raw, blame):
Jeff Sharkey8190f4882014-08-28 12:24:07 -070055 self.clazz = clazz
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -070056 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -070057 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -070058 self.blame = blame
Jeff Sharkey8190f4882014-08-28 12:24:07 -070059
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
Jeff Sharkey037458a2014-09-04 15:46:20 -070073 self.ident = self.raw.replace(" deprecated ", " ")
74
Jeff Sharkey8190f4882014-08-28 12:24:07 -070075 def __repr__(self):
76 return self.raw
77
78
79class Method():
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -070080 def __init__(self, clazz, line, raw, blame):
Jeff Sharkey8190f4882014-08-28 12:24:07 -070081 self.clazz = clazz
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -070082 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -070083 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -070084 self.blame = blame
85
86 # drop generics for now
87 raw = re.sub("<.+?>", "", raw)
Jeff Sharkey8190f4882014-08-28 12:24:07 -070088
89 raw = re.split("[\s(),;]+", raw)
90 for r in ["", ";"]:
91 while r in raw: raw.remove(r)
92 self.split = list(raw)
93
Filip Pavlisdb234ef2016-11-29 19:06:17 +000094 for r in ["method", "public", "protected", "static", "final", "deprecated", "abstract", "default"]:
Jeff Sharkey8190f4882014-08-28 12:24:07 -070095 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
Jeff Sharkey037458a2014-09-04 15:46:20 -0700104 # 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
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700113 def __repr__(self):
114 return self.raw
115
116
117class Class():
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700118 def __init__(self, pkg, line, raw, blame):
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700119 self.pkg = pkg
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700120 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700121 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700122 self.blame = blame
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700123 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]
Jeff Sharkey037458a2014-09-04 15:46:20 -0700133 else:
134 raise ValueError("Funky class type %s" % (self.raw))
135
136 if "extends" in raw:
137 self.extends = raw[raw.index("extends")+1]
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800138 self.extends_path = self.extends.split(".")
Jeff Sharkey037458a2014-09-04 15:46:20 -0700139 else:
140 self.extends = None
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800141 self.extends_path = []
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700142
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700143 self.fullname = self.pkg.name + "." + self.fullname
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800144 self.fullname_path = self.fullname.split(".")
145
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700146 self.name = self.fullname[self.fullname.rindex(".")+1:]
147
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700148 def __repr__(self):
149 return self.raw
150
151
152class Package():
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700153 def __init__(self, line, raw, blame):
154 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700155 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700156 self.blame = blame
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700157
158 raw = raw.split()
159 self.name = raw[raw.index("package")+1]
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800160 self.name_path = self.name.split(".")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700161
162 def __repr__(self):
163 return self.raw
164
165
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800166def _parse_stream(f, clazz_cb=None):
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700167 line = 0
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700168 api = {}
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700169 pkg = None
170 clazz = None
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700171 blame = None
172
173 re_blame = re.compile("^([a-z0-9]{7,}) \(<([^>]+)>.+?\) (.+?)$")
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800174 for raw in f:
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700175 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
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700183
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700184 if raw.startswith("package"):
185 pkg = Package(line, raw, blame)
186 elif raw.startswith(" ") and raw.endswith("{"):
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800187 # 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)
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700191 clazz = Class(pkg, line, raw, blame)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800192 if not clazz_cb:
193 api[clazz.fullname] = clazz
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700194 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))
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700200
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800201 # Handle last trailing class
202 if clazz and clazz_cb:
203 clazz_cb(clazz)
204
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700205 return api
206
207
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700208class Failure():
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800209 def __init__(self, sig, clazz, detail, error, rule, msg):
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700210 self.sig = sig
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700211 self.error = error
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800212 self.rule = rule
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700213 self.msg = msg
214
215 if error:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800216 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)
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700218 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800219 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)
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700221
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
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700240failures = {}
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700241
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800242def _fail(clazz, detail, error, rule, msg):
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700243 """Records an API failure to be processed later."""
244 global failures
245
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700246 sig = "%s-%s-%s" % (clazz.fullname, repr(detail), msg)
247 sig = sig.replace(" deprecated ", " ")
248
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800249 failures[sig] = Failure(sig, clazz, detail, error, rule, msg)
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700250
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700251
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800252def warn(clazz, detail, rule, msg):
253 _fail(clazz, detail, False, rule, msg)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700254
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800255def error(clazz, detail, rule, msg):
256 _fail(clazz, detail, True, rule, msg)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700257
258
259def verify_constants(clazz):
260 """All static final constants must be FOO_NAME style."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700261 if re.match("android\.R\.[a-z]+", clazz.fullname): return
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600262 if clazz.fullname.startswith("android.os.Build"): return
263 if clazz.fullname == "android.system.OsConstants": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700264
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600265 req = ["java.lang.String","byte","short","int","long","float","double","boolean","char"]
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700266 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:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800269 error(clazz, f, "C2", "Constant field names must be FOO_NAME")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600270 if f.typ != "java.lang.String":
Jeff Sharkey331279b2016-02-29 16:02:02 -0700271 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")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600273 if f.typ in req and f.value is None:
274 error(clazz, f, None, "All constants must be defined at compile time")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700275
276
277def verify_enums(clazz):
278 """Enums are bad, mmkay?"""
279 if "extends java.lang.Enum" in clazz.raw:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800280 error(clazz, None, "F5", "Enums are not allowed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700281
282
283def verify_class_names(clazz):
284 """Try catching malformed class names like myMtp or MTPUser."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700285 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
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700289 if re.search("[A-Z]{2,}", clazz.name) is not None:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800290 warn(clazz, None, "S1", "Class names with acronyms should be Mtp not MTP")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700291 if re.match("[^A-Z]", clazz.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800292 error(clazz, None, "S1", "Class must start with uppercase char")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700293
294
295def verify_method_names(clazz):
296 """Try catching malformed method names, like Foo() or getMTU()."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700297 if clazz.fullname.startswith("android.opengl"): return
298 if clazz.fullname.startswith("android.renderscript"): return
299 if clazz.fullname == "android.system.OsConstants": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700300
301 for m in clazz.methods:
302 if re.search("[A-Z]{2,}", m.name) is not None:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800303 warn(clazz, m, "S1", "Method names with acronyms should be getMtu() instead of getMTU()")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700304 if re.match("[^a-z]", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800305 error(clazz, m, "S1", "Method name must start with lowercase char")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700306
307
308def verify_callbacks(clazz):
309 """Verify Callback classes.
310 All callback classes must be abstract.
311 All methods must follow onFoo() naming style."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700312 if clazz.fullname == "android.speech.tts.SynthesisCallback": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700313
314 if clazz.name.endswith("Callbacks"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800315 error(clazz, None, "L1", "Callback class names should be singular")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700316 if clazz.name.endswith("Observer"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800317 warn(clazz, None, "L1", "Class should be named FooCallback")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700318
319 if clazz.name.endswith("Callback"):
320 if "interface" in clazz.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800321 error(clazz, None, "CL3", "Callbacks must be abstract class to enable extension in future API levels")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700322
323 for m in clazz.methods:
324 if not re.match("on[A-Z][a-z]*", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800325 error(clazz, m, "L1", "Callback method names must be onFoo() style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700326
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:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800337 error(clazz, None, "L1", "Listeners should be an interface, or otherwise renamed Callback")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700338
339 for m in clazz.methods:
340 if not re.match("on[A-Z][a-z]*", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800341 error(clazz, m, "L1", "Listener method names must be onFoo() style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700342
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():
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800346 error(clazz, m, "L1", "Single listener method name must match class name")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700347
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
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700359 if f.name == "SERVICE_INTERFACE" or f.name == "PROVIDER_INTERFACE": continue
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600360 if "INTERACTION" in f.name: continue
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700361
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_"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800365 error(clazz, f, "C3", "Intent action constant name must be ACTION_FOO")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700366 else:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700367 if clazz.fullname == "android.content.Intent":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700368 prefix = "android.intent.action"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700369 elif clazz.fullname == "android.provider.Settings":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700370 prefix = "android.settings"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700371 elif clazz.fullname == "android.app.admin.DevicePolicyManager" or clazz.fullname == "android.app.admin.DeviceAdminReceiver":
372 prefix = "android.app.action"
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700373 else:
374 prefix = clazz.pkg.name + ".action"
375 expected = prefix + "." + f.name[7:]
376 if f.value != expected:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800377 error(clazz, f, "C4", "Inconsistent action value; expected %s" % (expected))
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700378
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 }"""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700387 if clazz.fullname == "android.app.Notification": return
388 if clazz.fullname == "android.appwidget.AppWidgetManager": return
389
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700390 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_"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800397 error(clazz, f, "C3", "Intent extra must be EXTRA_FOO")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700398 else:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700399 if clazz.pkg.name == "android.content" and clazz.name == "Intent":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700400 prefix = "android.intent.extra"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700401 elif clazz.pkg.name == "android.app.admin":
402 prefix = "android.app.extra"
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700403 else:
404 prefix = clazz.pkg.name + ".extra"
405 expected = prefix + "." + f.name[6:]
406 if f.value != expected:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800407 error(clazz, f, "C4", "Inconsistent extra value; expected %s" % (expected))
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700408
409
410def verify_equals(clazz):
411 """Verify that equals() and hashCode() must be overridden together."""
Jeff Sharkey40d623e2016-12-21 13:46:33 -0700412 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
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700418 if eq != hc:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800419 error(clazz, None, "M8", "Must override both equals and hashCode; missing one")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700420
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:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800430 error(clazz, None, "FW3", "Parcelable requires CREATOR, writeToParcel, and describeContents; missing one")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700431
Jeff Sharkey331279b2016-02-29 16:02:02 -0700432 if " final class " not in clazz.raw:
433 error(clazz, None, "FW8", "Parcelable classes must be final")
434
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700435
436def verify_protected(clazz):
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800437 """Verify that no protected methods or fields are allowed."""
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700438 for m in clazz.methods:
439 if "protected" in m.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800440 error(clazz, m, "M7", "Protected methods not allowed; must be public")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700441 for f in clazz.fields:
442 if "protected" in f.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800443 error(clazz, f, "M7", "Protected fields not allowed; must be public")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700444
445
446def verify_fields(clazz):
447 """Verify that all exposed fields are final.
448 Exposed fields must follow myName style.
449 Catch internal mFoo objects being exposed."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700450
451 IGNORE_BARE_FIELDS = [
452 "android.app.ActivityManager.RecentTaskInfo",
453 "android.app.Notification",
454 "android.content.pm.ActivityInfo",
455 "android.content.pm.ApplicationInfo",
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600456 "android.content.pm.ComponentInfo",
457 "android.content.pm.ResolveInfo",
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700458 "android.content.pm.FeatureGroupInfo",
459 "android.content.pm.InstrumentationInfo",
460 "android.content.pm.PackageInfo",
461 "android.content.pm.PackageItemInfo",
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600462 "android.content.res.Configuration",
463 "android.graphics.BitmapFactory.Options",
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700464 "android.os.Message",
465 "android.system.StructPollfd",
466 ]
467
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700468 for f in clazz.fields:
469 if not "final" in f.split:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700470 if clazz.fullname in IGNORE_BARE_FIELDS:
471 pass
472 elif clazz.fullname.endswith("LayoutParams"):
473 pass
474 elif clazz.fullname.startswith("android.util.Mutable"):
475 pass
476 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800477 error(clazz, f, "F2", "Bare fields must be marked final, or add accessors if mutable")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700478
479 if not "static" in f.split:
480 if not re.match("[a-z]([a-zA-Z]+)?", f.name):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800481 error(clazz, f, "S1", "Non-static fields must be named using myField style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700482
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700483 if re.match("[ms][A-Z]", f.name):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800484 error(clazz, f, "F1", "Internal objects must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700485
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700486 if re.match("[A-Z_]+", f.name):
487 if "static" not in f.split or "final" not in f.split:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800488 error(clazz, f, "C2", "Constants must be marked static final")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700489
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700490
491def verify_register(clazz):
492 """Verify parity of registration methods.
493 Callback objects use register/unregister methods.
494 Listener objects use add/remove methods."""
495 methods = [ m.name for m in clazz.methods ]
496 for m in clazz.methods:
497 if "Callback" in m.raw:
498 if m.name.startswith("register"):
499 other = "unregister" + m.name[8:]
500 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800501 error(clazz, m, "L2", "Missing unregister method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700502 if m.name.startswith("unregister"):
503 other = "register" + m.name[10:]
504 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800505 error(clazz, m, "L2", "Missing register method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700506
507 if m.name.startswith("add") or m.name.startswith("remove"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800508 error(clazz, m, "L3", "Callback methods should be named register/unregister")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700509
510 if "Listener" in m.raw:
511 if m.name.startswith("add"):
512 other = "remove" + m.name[3:]
513 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800514 error(clazz, m, "L2", "Missing remove method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700515 if m.name.startswith("remove") and not m.name.startswith("removeAll"):
516 other = "add" + m.name[6:]
517 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800518 error(clazz, m, "L2", "Missing add method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700519
520 if m.name.startswith("register") or m.name.startswith("unregister"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800521 error(clazz, m, "L3", "Listener methods should be named add/remove")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700522
523
524def verify_sync(clazz):
525 """Verify synchronized methods aren't exposed."""
526 for m in clazz.methods:
527 if "synchronized" in m.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800528 error(clazz, m, "M5", "Internal locks must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700529
530
531def verify_intent_builder(clazz):
532 """Verify that Intent builders are createFooIntent() style."""
533 if clazz.name == "Intent": return
534
535 for m in clazz.methods:
536 if m.typ == "android.content.Intent":
537 if m.name.startswith("create") and m.name.endswith("Intent"):
538 pass
539 else:
Adam Powell539ea122015-04-10 13:01:37 -0700540 warn(clazz, m, "FW1", "Methods creating an Intent should be named createFooIntent()")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700541
542
543def verify_helper_classes(clazz):
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700544 """Verify that helper classes are named consistently with what they extend.
545 All developer extendable methods should be named onFoo()."""
546 test_methods = False
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700547 if "extends android.app.Service" in clazz.raw:
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700548 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700549 if not clazz.name.endswith("Service"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800550 error(clazz, None, "CL4", "Inconsistent class name; should be FooService")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700551
552 found = False
553 for f in clazz.fields:
554 if f.name == "SERVICE_INTERFACE":
555 found = True
556 if f.value != clazz.fullname:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800557 error(clazz, f, "C4", "Inconsistent interface constant; expected %s" % (clazz.fullname))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700558
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700559 if "extends android.content.ContentProvider" in clazz.raw:
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700560 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700561 if not clazz.name.endswith("Provider"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800562 error(clazz, None, "CL4", "Inconsistent class name; should be FooProvider")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700563
564 found = False
565 for f in clazz.fields:
566 if f.name == "PROVIDER_INTERFACE":
567 found = True
568 if f.value != clazz.fullname:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800569 error(clazz, f, "C4", "Inconsistent interface constant; expected %s" % (clazz.fullname))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700570
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700571 if "extends android.content.BroadcastReceiver" in clazz.raw:
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700572 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700573 if not clazz.name.endswith("Receiver"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800574 error(clazz, None, "CL4", "Inconsistent class name; should be FooReceiver")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700575
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700576 if "extends android.app.Activity" in clazz.raw:
577 test_methods = True
578 if not clazz.name.endswith("Activity"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800579 error(clazz, None, "CL4", "Inconsistent class name; should be FooActivity")
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700580
581 if test_methods:
582 for m in clazz.methods:
583 if "final" in m.split: continue
584 if not re.match("on[A-Z]", m.name):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700585 if "abstract" in m.split:
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800586 warn(clazz, m, None, "Methods implemented by developers should be named onFoo()")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700587 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800588 warn(clazz, m, None, "If implemented by developer, should be named onFoo(); otherwise consider marking final")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700589
590
591def verify_builder(clazz):
592 """Verify builder classes.
593 Methods should return the builder to enable chaining."""
594 if " extends " in clazz.raw: return
595 if not clazz.name.endswith("Builder"): return
596
597 if clazz.name != "Builder":
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800598 warn(clazz, None, None, "Builder should be defined as inner class")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700599
600 has_build = False
601 for m in clazz.methods:
602 if m.name == "build":
603 has_build = True
604 continue
605
606 if m.name.startswith("get"): continue
607 if m.name.startswith("clear"): continue
608
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700609 if m.name.startswith("with"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800610 warn(clazz, m, None, "Builder methods names should use setFoo() style")
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700611
612 if m.name.startswith("set"):
613 if not m.typ.endswith(clazz.fullname):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800614 warn(clazz, m, "M4", "Methods must return the builder object")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700615
616 if not has_build:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800617 warn(clazz, None, None, "Missing build() method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700618
619
620def verify_aidl(clazz):
621 """Catch people exposing raw AIDL."""
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700622 if "extends android.os.Binder" in clazz.raw or "implements android.os.IInterface" in clazz.raw:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800623 error(clazz, None, None, "Raw AIDL interfaces must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700624
625
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700626def verify_internal(clazz):
627 """Catch people exposing internal classes."""
628 if clazz.pkg.name.startswith("com.android"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800629 error(clazz, None, None, "Internal classes must not be exposed")
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700630
631
632def verify_layering(clazz):
633 """Catch package layering violations.
634 For example, something in android.os depending on android.app."""
635 ranking = [
636 ["android.service","android.accessibilityservice","android.inputmethodservice","android.printservice","android.appwidget","android.webkit","android.preference","android.gesture","android.print"],
637 "android.app",
638 "android.widget",
639 "android.view",
640 "android.animation",
641 "android.provider",
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700642 ["android.content","android.graphics.drawable"],
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700643 "android.database",
644 "android.graphics",
645 "android.text",
646 "android.os",
647 "android.util"
648 ]
649
650 def rank(p):
651 for i in range(len(ranking)):
652 if isinstance(ranking[i], list):
653 for j in ranking[i]:
654 if p.startswith(j): return i
655 else:
656 if p.startswith(ranking[i]): return i
657
658 cr = rank(clazz.pkg.name)
659 if cr is None: return
660
661 for f in clazz.fields:
662 ir = rank(f.typ)
663 if ir and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800664 warn(clazz, f, "FW6", "Field type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700665
666 for m in clazz.methods:
667 ir = rank(m.typ)
668 if ir and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800669 warn(clazz, m, "FW6", "Method return type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700670 for arg in m.args:
671 ir = rank(arg)
672 if ir and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800673 warn(clazz, m, "FW6", "Method argument type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700674
675
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800676def verify_boolean(clazz):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800677 """Verifies that boolean accessors are named correctly.
678 For example, hasFoo() and setHasFoo()."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700679
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800680 def is_get(m): return len(m.args) == 0 and m.typ == "boolean"
681 def is_set(m): return len(m.args) == 1 and m.args[0] == "boolean"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700682
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800683 gets = [ m for m in clazz.methods if is_get(m) ]
684 sets = [ m for m in clazz.methods if is_set(m) ]
685
686 def error_if_exists(methods, trigger, expected, actual):
687 for m in methods:
688 if m.name == actual:
689 error(clazz, m, "M6", "Symmetric method for %s must be named %s" % (trigger, expected))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700690
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700691 for m in clazz.methods:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800692 if is_get(m):
693 if re.match("is[A-Z]", m.name):
694 target = m.name[2:]
695 expected = "setIs" + target
696 error_if_exists(sets, m.name, expected, "setHas" + target)
697 elif re.match("has[A-Z]", m.name):
698 target = m.name[3:]
699 expected = "setHas" + target
700 error_if_exists(sets, m.name, expected, "setIs" + target)
701 error_if_exists(sets, m.name, expected, "set" + target)
702 elif re.match("get[A-Z]", m.name):
703 target = m.name[3:]
704 expected = "set" + target
705 error_if_exists(sets, m.name, expected, "setIs" + target)
706 error_if_exists(sets, m.name, expected, "setHas" + target)
707
708 if is_set(m):
709 if re.match("set[A-Z]", m.name):
710 target = m.name[3:]
711 expected = "get" + target
712 error_if_exists(sets, m.name, expected, "is" + target)
713 error_if_exists(sets, m.name, expected, "has" + target)
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700714
715
716def verify_collections(clazz):
717 """Verifies that collection types are interfaces."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700718 if clazz.fullname == "android.os.Bundle": return
719
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700720 bad = ["java.util.Vector", "java.util.LinkedList", "java.util.ArrayList", "java.util.Stack",
721 "java.util.HashMap", "java.util.HashSet", "android.util.ArraySet", "android.util.ArrayMap"]
722 for m in clazz.methods:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700723 if m.typ in bad:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800724 error(clazz, m, "CL2", "Return type is concrete collection; must be higher-level interface")
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700725 for arg in m.args:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700726 if arg in bad:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800727 error(clazz, m, "CL2", "Argument is concrete collection; must be higher-level interface")
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700728
729
730def verify_flags(clazz):
731 """Verifies that flags are non-overlapping."""
732 known = collections.defaultdict(int)
733 for f in clazz.fields:
734 if "FLAG_" in f.name:
735 try:
736 val = int(f.value)
737 except:
738 continue
739
740 scope = f.name[0:f.name.index("FLAG_")]
741 if val & known[scope]:
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800742 warn(clazz, f, "C1", "Found overlapping flag constant value")
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700743 known[scope] |= val
744
745
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800746def verify_exception(clazz):
747 """Verifies that methods don't throw generic exceptions."""
748 for m in clazz.methods:
749 if "throws java.lang.Exception" in m.raw or "throws java.lang.Throwable" in m.raw or "throws java.lang.Error" in m.raw:
750 error(clazz, m, "S1", "Methods must not throw generic exceptions")
751
Jeff Sharkey331279b2016-02-29 16:02:02 -0700752 if "throws android.os.RemoteException" in m.raw:
753 if clazz.name == "android.content.ContentProviderClient": continue
754 if clazz.name == "android.os.Binder": continue
755 if clazz.name == "android.os.IBinder": continue
756
757 error(clazz, m, "FW9", "Methods calling into system server should rethrow RemoteException as RuntimeException")
758
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800759
760def verify_google(clazz):
761 """Verifies that APIs never reference Google."""
762
763 if re.search("google", clazz.raw, re.IGNORECASE):
764 error(clazz, None, None, "Must never reference Google")
765
766 test = []
767 test.extend(clazz.ctors)
768 test.extend(clazz.fields)
769 test.extend(clazz.methods)
770
771 for t in test:
772 if re.search("google", t.raw, re.IGNORECASE):
773 error(clazz, t, None, "Must never reference Google")
774
775
776def verify_bitset(clazz):
777 """Verifies that we avoid using heavy BitSet."""
778
779 for f in clazz.fields:
780 if f.typ == "java.util.BitSet":
781 error(clazz, f, None, "Field type must not be heavy BitSet")
782
783 for m in clazz.methods:
784 if m.typ == "java.util.BitSet":
785 error(clazz, m, None, "Return type must not be heavy BitSet")
786 for arg in m.args:
787 if arg == "java.util.BitSet":
788 error(clazz, m, None, "Argument type must not be heavy BitSet")
789
790
791def verify_manager(clazz):
792 """Verifies that FooManager is only obtained from Context."""
793
794 if not clazz.name.endswith("Manager"): return
795
796 for c in clazz.ctors:
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800797 error(clazz, c, None, "Managers must always be obtained from Context; no direct constructors")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800798
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600799 for m in clazz.methods:
800 if m.typ == clazz.fullname:
801 error(clazz, m, None, "Managers must always be obtained from Context")
802
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800803
804def verify_boxed(clazz):
805 """Verifies that methods avoid boxed primitives."""
806
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800807 boxed = ["java.lang.Number","java.lang.Byte","java.lang.Double","java.lang.Float","java.lang.Integer","java.lang.Long","java.lang.Short"]
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800808
809 for c in clazz.ctors:
810 for arg in c.args:
811 if arg in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800812 error(clazz, c, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800813
814 for f in clazz.fields:
815 if f.typ in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800816 error(clazz, f, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800817
818 for m in clazz.methods:
819 if m.typ in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800820 error(clazz, m, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800821 for arg in m.args:
822 if arg in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800823 error(clazz, m, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800824
825
826def verify_static_utils(clazz):
827 """Verifies that helper classes can't be constructed."""
828 if clazz.fullname.startswith("android.opengl"): return
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600829 if clazz.fullname.startswith("android.R"): return
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800830
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600831 # Only care about classes with default constructors
832 if len(clazz.ctors) == 1 and len(clazz.ctors[0].args) == 0:
833 test = []
834 test.extend(clazz.fields)
835 test.extend(clazz.methods)
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800836
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600837 if len(test) == 0: return
838 for t in test:
839 if "static" not in t.split:
840 return
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800841
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800842 error(clazz, None, None, "Fully-static utility classes must not have constructor")
843
844
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800845def verify_overload_args(clazz):
846 """Verifies that method overloads add new arguments at the end."""
847 if clazz.fullname.startswith("android.opengl"): return
848
849 overloads = collections.defaultdict(list)
850 for m in clazz.methods:
851 if "deprecated" in m.split: continue
852 overloads[m.name].append(m)
853
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800854 for name, methods in overloads.items():
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800855 if len(methods) <= 1: continue
856
857 # Look for arguments common across all overloads
858 def cluster(args):
859 count = collections.defaultdict(int)
860 res = set()
861 for i in range(len(args)):
862 a = args[i]
863 res.add("%s#%d" % (a, count[a]))
864 count[a] += 1
865 return res
866
867 common_args = cluster(methods[0].args)
868 for m in methods:
869 common_args = common_args & cluster(m.args)
870
871 if len(common_args) == 0: continue
872
873 # Require that all common arguments are present at start of signature
874 locked_sig = None
875 for m in methods:
876 sig = m.args[0:len(common_args)]
877 if not common_args.issubset(cluster(sig)):
878 warn(clazz, m, "M2", "Expected common arguments [%s] at beginning of overloaded method" % (", ".join(common_args)))
879 elif not locked_sig:
880 locked_sig = sig
881 elif locked_sig != sig:
882 error(clazz, m, "M2", "Expected consistent argument ordering between overloads: %s..." % (", ".join(locked_sig)))
883
884
885def verify_callback_handlers(clazz):
886 """Verifies that methods adding listener/callback have overload
887 for specifying delivery thread."""
888
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800889 # Ignore UI packages which assume main thread
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800890 skip = [
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800891 "animation",
892 "view",
893 "graphics",
894 "transition",
895 "widget",
896 "webkit",
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800897 ]
898 for s in skip:
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800899 if s in clazz.pkg.name_path: return
900 if s in clazz.extends_path: return
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800901
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800902 # Ignore UI classes which assume main thread
903 if "app" in clazz.pkg.name_path or "app" in clazz.extends_path:
904 for s in ["ActionBar","Dialog","Application","Activity","Fragment","Loader"]:
905 if s in clazz.fullname: return
906 if "content" in clazz.pkg.name_path or "content" in clazz.extends_path:
907 for s in ["Loader"]:
908 if s in clazz.fullname: return
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800909
910 found = {}
911 by_name = collections.defaultdict(list)
912 for m in clazz.methods:
913 if m.name.startswith("unregister"): continue
914 if m.name.startswith("remove"): continue
915 if re.match("on[A-Z]+", m.name): continue
916
917 by_name[m.name].append(m)
918
919 for a in m.args:
920 if a.endswith("Listener") or a.endswith("Callback") or a.endswith("Callbacks"):
921 found[m.name] = m
922
923 for f in found.values():
924 takes_handler = False
925 for m in by_name[f.name]:
926 if "android.os.Handler" in m.args:
927 takes_handler = True
928 if not takes_handler:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800929 warn(clazz, f, "L1", "Registration methods should have overload that accepts delivery Handler")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800930
931
932def verify_context_first(clazz):
933 """Verifies that methods accepting a Context keep it the first argument."""
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800934 examine = clazz.ctors + clazz.methods
935 for m in examine:
936 if len(m.args) > 1 and m.args[0] != "android.content.Context":
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800937 if "android.content.Context" in m.args[1:]:
938 error(clazz, m, "M3", "Context is distinct, so it must be the first argument")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600939 if len(m.args) > 1 and m.args[0] != "android.content.ContentResolver":
940 if "android.content.ContentResolver" in m.args[1:]:
941 error(clazz, m, "M3", "ContentResolver is distinct, so it must be the first argument")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800942
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800943
944def verify_listener_last(clazz):
945 """Verifies that methods accepting a Listener or Callback keep them as last arguments."""
946 examine = clazz.ctors + clazz.methods
947 for m in examine:
948 if "Listener" in m.name or "Callback" in m.name: continue
949 found = False
950 for a in m.args:
951 if a.endswith("Callback") or a.endswith("Callbacks") or a.endswith("Listener"):
952 found = True
953 elif found and a != "android.os.Handler":
954 warn(clazz, m, "M3", "Listeners should always be at end of argument list")
955
956
957def verify_resource_names(clazz):
958 """Verifies that resource names have consistent case."""
959 if not re.match("android\.R\.[a-z]+", clazz.fullname): return
960
961 # Resources defined by files are foo_bar_baz
962 if clazz.name in ["anim","animator","color","dimen","drawable","interpolator","layout","transition","menu","mipmap","string","plurals","raw","xml"]:
963 for f in clazz.fields:
964 if re.match("[a-z1-9_]+$", f.name): continue
965 error(clazz, f, None, "Expected resource name in this class to be foo_bar_baz style")
966
967 # Resources defined inside files are fooBarBaz
968 if clazz.name in ["array","attr","id","bool","fraction","integer"]:
969 for f in clazz.fields:
970 if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue
971 if re.match("layout_[a-z][a-zA-Z1-9]*$", f.name): continue
972 if re.match("state_[a-z_]*$", f.name): continue
973
974 if re.match("[a-z][a-zA-Z1-9]*$", f.name): continue
975 error(clazz, f, "C7", "Expected resource name in this class to be fooBarBaz style")
976
977 # Styles are FooBar_Baz
978 if clazz.name in ["style"]:
979 for f in clazz.fields:
980 if re.match("[A-Z][A-Za-z1-9]+(_[A-Z][A-Za-z1-9]+?)*$", f.name): continue
981 error(clazz, f, "C7", "Expected resource name in this class to be FooBar_Baz style")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800982
983
Jeff Sharkey331279b2016-02-29 16:02:02 -0700984def verify_files(clazz):
985 """Verifies that methods accepting File also accept streams."""
986
987 has_file = set()
988 has_stream = set()
989
990 test = []
991 test.extend(clazz.ctors)
992 test.extend(clazz.methods)
993
994 for m in test:
995 if "java.io.File" in m.args:
996 has_file.add(m)
997 if "java.io.FileDescriptor" in m.args or "android.os.ParcelFileDescriptor" in m.args or "java.io.InputStream" in m.args or "java.io.OutputStream" in m.args:
998 has_stream.add(m.name)
999
1000 for m in has_file:
1001 if m.name not in has_stream:
1002 warn(clazz, m, "M10", "Methods accepting File should also accept FileDescriptor or streams")
1003
1004
Jeff Sharkey70168dd2016-03-30 21:47:16 -06001005def verify_manager_list(clazz):
1006 """Verifies that managers return List<? extends Parcelable> instead of arrays."""
1007
1008 if not clazz.name.endswith("Manager"): return
1009
1010 for m in clazz.methods:
1011 if m.typ.startswith("android.") and m.typ.endswith("[]"):
1012 warn(clazz, m, None, "Methods should return List<? extends Parcelable> instead of Parcelable[] to support ParceledListSlice under the hood")
1013
1014
Jeff Sharkey26c80902016-12-21 13:41:17 -07001015def verify_abstract_inner(clazz):
1016 """Verifies that abstract inner classes are static."""
1017
1018 if re.match(".+?\.[A-Z][^\.]+\.[A-Z]", clazz.fullname):
1019 if " abstract " in clazz.raw and " static " not in clazz.raw:
1020 warn(clazz, None, None, "Abstract inner classes should be static to improve testability")
1021
1022
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001023def verify_runtime_exceptions(clazz):
1024 """Verifies that runtime exceptions aren't listed in throws."""
1025
1026 banned = [
1027 "java.lang.NullPointerException",
1028 "java.lang.ClassCastException",
1029 "java.lang.IndexOutOfBoundsException",
1030 "java.lang.reflect.UndeclaredThrowableException",
1031 "java.lang.reflect.MalformedParametersException",
1032 "java.lang.reflect.MalformedParameterizedTypeException",
1033 "java.lang.invoke.WrongMethodTypeException",
1034 "java.lang.EnumConstantNotPresentException",
1035 "java.lang.IllegalMonitorStateException",
1036 "java.lang.SecurityException",
1037 "java.lang.UnsupportedOperationException",
1038 "java.lang.annotation.AnnotationTypeMismatchException",
1039 "java.lang.annotation.IncompleteAnnotationException",
1040 "java.lang.TypeNotPresentException",
1041 "java.lang.IllegalStateException",
1042 "java.lang.ArithmeticException",
1043 "java.lang.IllegalArgumentException",
1044 "java.lang.ArrayStoreException",
1045 "java.lang.NegativeArraySizeException",
1046 "java.util.MissingResourceException",
1047 "java.util.EmptyStackException",
1048 "java.util.concurrent.CompletionException",
1049 "java.util.concurrent.RejectedExecutionException",
1050 "java.util.IllformedLocaleException",
1051 "java.util.ConcurrentModificationException",
1052 "java.util.NoSuchElementException",
1053 "java.io.UncheckedIOException",
1054 "java.time.DateTimeException",
1055 "java.security.ProviderException",
1056 "java.nio.BufferUnderflowException",
1057 "java.nio.BufferOverflowException",
1058 ]
1059
1060 test = []
1061 test.extend(clazz.ctors)
1062 test.extend(clazz.methods)
1063
1064 for t in test:
1065 if " throws " not in t.raw: continue
1066 throws = t.raw[t.raw.index(" throws "):]
1067 for b in banned:
1068 if b in throws:
1069 error(clazz, t, None, "Methods must not mention RuntimeException subclasses in throws clauses")
1070
1071
1072def verify_error(clazz):
1073 """Verifies that we always use Exception instead of Error."""
1074 if not clazz.extends: return
1075 if clazz.extends.endswith("Error"):
1076 error(clazz, None, None, "Trouble must be reported through an Exception, not Error")
1077 if clazz.extends.endswith("Exception") and not clazz.name.endswith("Exception"):
1078 error(clazz, None, None, "Exceptions must be named FooException")
1079
1080
1081def verify_units(clazz):
1082 """Verifies that we use consistent naming for units."""
1083
1084 # If we find K, recommend replacing with V
1085 bad = {
1086 "Ns": "Nanos",
1087 "Ms": "Millis or Micros",
1088 "Sec": "Seconds", "Secs": "Seconds",
1089 "Hr": "Hours", "Hrs": "Hours",
1090 "Mo": "Months", "Mos": "Months",
1091 "Yr": "Years", "Yrs": "Years",
1092 "Byte": "Bytes", "Space": "Bytes",
1093 }
1094
1095 for m in clazz.methods:
1096 if m.typ not in ["short","int","long"]: continue
1097 for k, v in bad.iteritems():
1098 if m.name.endswith(k):
1099 error(clazz, m, None, "Expected method name units to be " + v)
1100 if m.name.endswith("Nanos") or m.name.endswith("Micros"):
1101 warn(clazz, m, None, "Returned time values are strongly encouraged to be in milliseconds unless you need the extra precision")
1102 if m.name.endswith("Seconds"):
1103 error(clazz, m, None, "Returned time values must be in milliseconds")
1104
1105 for m in clazz.methods:
1106 typ = m.typ
1107 if typ == "void":
1108 if len(m.args) != 1: continue
1109 typ = m.args[0]
1110
1111 if m.name.endswith("Fraction") and typ != "float":
1112 error(clazz, m, None, "Fractions must use floats")
1113 if m.name.endswith("Percentage") and typ != "int":
1114 error(clazz, m, None, "Percentage must use ints")
1115
1116
1117def verify_closable(clazz):
1118 """Verifies that classes are AutoClosable."""
1119 if "implements java.lang.AutoCloseable" in clazz.raw: return
1120 if "implements java.io.Closeable" in clazz.raw: return
1121
1122 for m in clazz.methods:
1123 if len(m.args) > 0: continue
1124 if m.name in ["close","release","destroy","finish","finalize","disconnect","shutdown","stop","free","quit"]:
1125 warn(clazz, m, None, "Classes that release resources should implement AutoClosable and CloseGuard")
1126 return
1127
1128
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001129def examine_clazz(clazz):
1130 """Find all style issues in the given class."""
1131 if clazz.pkg.name.startswith("java"): return
1132 if clazz.pkg.name.startswith("junit"): return
1133 if clazz.pkg.name.startswith("org.apache"): return
1134 if clazz.pkg.name.startswith("org.xml"): return
1135 if clazz.pkg.name.startswith("org.json"): return
1136 if clazz.pkg.name.startswith("org.w3c"): return
Jeff Sharkey331279b2016-02-29 16:02:02 -07001137 if clazz.pkg.name.startswith("android.icu."): return
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001138
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001139 verify_constants(clazz)
1140 verify_enums(clazz)
1141 verify_class_names(clazz)
1142 verify_method_names(clazz)
1143 verify_callbacks(clazz)
1144 verify_listeners(clazz)
1145 verify_actions(clazz)
1146 verify_extras(clazz)
1147 verify_equals(clazz)
1148 verify_parcelable(clazz)
1149 verify_protected(clazz)
1150 verify_fields(clazz)
1151 verify_register(clazz)
1152 verify_sync(clazz)
1153 verify_intent_builder(clazz)
1154 verify_helper_classes(clazz)
1155 verify_builder(clazz)
1156 verify_aidl(clazz)
1157 verify_internal(clazz)
1158 verify_layering(clazz)
1159 verify_boolean(clazz)
1160 verify_collections(clazz)
1161 verify_flags(clazz)
1162 verify_exception(clazz)
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07001163 if not ALLOW_GOOGLE: verify_google(clazz)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001164 verify_bitset(clazz)
1165 verify_manager(clazz)
1166 verify_boxed(clazz)
1167 verify_static_utils(clazz)
1168 verify_overload_args(clazz)
1169 verify_callback_handlers(clazz)
1170 verify_context_first(clazz)
1171 verify_listener_last(clazz)
1172 verify_resource_names(clazz)
Jeff Sharkey331279b2016-02-29 16:02:02 -07001173 verify_files(clazz)
Jeff Sharkey70168dd2016-03-30 21:47:16 -06001174 verify_manager_list(clazz)
Jeff Sharkey26c80902016-12-21 13:41:17 -07001175 verify_abstract_inner(clazz)
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001176 verify_runtime_exceptions(clazz)
1177 verify_error(clazz)
1178 verify_units(clazz)
1179 verify_closable(clazz)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001180
1181
1182def examine_stream(stream):
1183 """Find all style issues in the given API stream."""
1184 global failures
1185 failures = {}
1186 _parse_stream(stream, examine_clazz)
1187 return failures
1188
1189
1190def examine_api(api):
1191 """Find all style issues in the given parsed API."""
1192 global failures
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001193 failures = {}
1194 for key in sorted(api.keys()):
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001195 examine_clazz(api[key])
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001196 return failures
1197
1198
Jeff Sharkey037458a2014-09-04 15:46:20 -07001199def verify_compat(cur, prev):
1200 """Find any incompatible API changes between two levels."""
1201 global failures
1202
1203 def class_exists(api, test):
1204 return test.fullname in api
1205
1206 def ctor_exists(api, clazz, test):
1207 for m in clazz.ctors:
1208 if m.ident == test.ident: return True
1209 return False
1210
1211 def all_methods(api, clazz):
1212 methods = list(clazz.methods)
1213 if clazz.extends is not None:
1214 methods.extend(all_methods(api, api[clazz.extends]))
1215 return methods
1216
1217 def method_exists(api, clazz, test):
1218 methods = all_methods(api, clazz)
1219 for m in methods:
1220 if m.ident == test.ident: return True
1221 return False
1222
1223 def field_exists(api, clazz, test):
1224 for f in clazz.fields:
1225 if f.ident == test.ident: return True
1226 return False
1227
1228 failures = {}
1229 for key in sorted(prev.keys()):
1230 prev_clazz = prev[key]
1231
1232 if not class_exists(cur, prev_clazz):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001233 error(prev_clazz, None, None, "Class removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07001234 continue
1235
1236 cur_clazz = cur[key]
1237
1238 for test in prev_clazz.ctors:
1239 if not ctor_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001240 error(prev_clazz, prev_ctor, None, "Constructor removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07001241
1242 methods = all_methods(prev, prev_clazz)
1243 for test in methods:
1244 if not method_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001245 error(prev_clazz, test, None, "Method removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07001246
1247 for test in prev_clazz.fields:
1248 if not field_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001249 error(prev_clazz, test, None, "Field removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07001250
1251 return failures
1252
1253
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07001254if __name__ == "__main__":
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07001255 parser = argparse.ArgumentParser(description="Enforces common Android public API design \
1256 patterns. It ignores lint messages from a previous API level, if provided.")
1257 parser.add_argument("current.txt", type=argparse.FileType('r'), help="current.txt")
1258 parser.add_argument("previous.txt", nargs='?', type=argparse.FileType('r'), default=None,
1259 help="previous.txt")
1260 parser.add_argument("--no-color", action='store_const', const=True,
1261 help="Disable terminal colors")
1262 parser.add_argument("--allow-google", action='store_const', const=True,
1263 help="Allow references to Google")
1264 args = vars(parser.parse_args())
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001265
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07001266 if args['no_color']:
1267 USE_COLOR = False
1268
1269 if args['allow_google']:
1270 ALLOW_GOOGLE = True
1271
1272 current_file = args['current.txt']
1273 previous_file = args['previous.txt']
1274
1275 with current_file as f:
1276 cur_fail = examine_stream(f)
1277 if not previous_file is None:
1278 with previous_file as f:
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001279 prev_fail = examine_stream(f)
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001280
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07001281 # ignore errors from previous API level
1282 for p in prev_fail:
1283 if p in cur_fail:
1284 del cur_fail[p]
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001285
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001286 """
1287 # NOTE: disabled because of memory pressure
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07001288 # look for compatibility issues
1289 compat_fail = verify_compat(cur, prev)
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001290
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07001291 print "%s API compatibility issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
1292 for f in sorted(compat_fail):
1293 print compat_fail[f]
1294 print
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001295 """
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07001296
1297 print "%s API style issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
1298 for f in sorted(cur_fail):
1299 print cur_fail[f]
Jeff Sharkey037458a2014-09-04 15:46:20 -07001300 print