blob: 934847f6eec20dd359689be86369575b1ee6b62c [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
Jeff Sharkey8b141b92018-04-11 10:05:44 -060053def ident(raw):
54 """Strips superficial signature changes, giving us a strong key that
55 can be used to identify members across API levels."""
56 raw = raw.replace(" deprecated ", " ")
57 raw = raw.replace(" synchronized ", " ")
58 raw = raw.replace(" final ", " ")
59 raw = re.sub("<.+?>", "", raw)
60 if " throws " in raw:
61 raw = raw[:raw.index(" throws ")]
62 return raw
63
64
Jeff Sharkey8190f4882014-08-28 12:24:07 -070065class Field():
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -070066 def __init__(self, clazz, line, raw, blame):
Jeff Sharkey8190f4882014-08-28 12:24:07 -070067 self.clazz = clazz
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -070068 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -070069 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -070070 self.blame = blame
Jeff Sharkey8190f4882014-08-28 12:24:07 -070071
Jeff Sharkey40d67f42018-07-17 13:29:40 -060072 # drop generics for now; may need multiple passes
73 raw = re.sub("<[^<]+?>", "", raw)
74 raw = re.sub("<[^<]+?>", "", raw)
75
Jeff Sharkey8190f4882014-08-28 12:24:07 -070076 raw = raw.split()
77 self.split = list(raw)
78
79 for r in ["field", "volatile", "transient", "public", "protected", "static", "final", "deprecated"]:
80 while r in raw: raw.remove(r)
81
Jeff Sharkey40d67f42018-07-17 13:29:40 -060082 # ignore annotations for now
83 raw = [ r for r in raw if not r.startswith("@") ]
84
Jeff Sharkey8190f4882014-08-28 12:24:07 -070085 self.typ = raw[0]
86 self.name = raw[1].strip(";")
87 if len(raw) >= 4 and raw[2] == "=":
88 self.value = raw[3].strip(';"')
89 else:
90 self.value = None
Jeff Sharkey8b141b92018-04-11 10:05:44 -060091 self.ident = ident(self.raw)
Jeff Sharkey037458a2014-09-04 15:46:20 -070092
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -070093 def __hash__(self):
94 return hash(self.raw)
95
Jeff Sharkey8190f4882014-08-28 12:24:07 -070096 def __repr__(self):
97 return self.raw
98
99
100class Method():
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700101 def __init__(self, clazz, line, raw, blame):
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700102 self.clazz = clazz
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700103 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700104 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700105 self.blame = blame
106
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600107 # drop generics for now; may need multiple passes
108 raw = re.sub("<[^<]+?>", "", raw)
109 raw = re.sub("<[^<]+?>", "", raw)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700110
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600111 # handle each clause differently
112 raw_prefix, raw_args, _, raw_throws = re.match(r"(.*?)\((.*?)\)( throws )?(.*?);$", raw).groups()
113
114 # parse prefixes
115 raw = re.split("[\s]+", raw_prefix)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700116 for r in ["", ";"]:
117 while r in raw: raw.remove(r)
118 self.split = list(raw)
119
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600120 for r in ["method", "public", "protected", "static", "final", "deprecated", "abstract", "default", "operator"]:
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700121 while r in raw: raw.remove(r)
122
123 self.typ = raw[0]
124 self.name = raw[1]
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600125
126 # parse args
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700127 self.args = []
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600128 for arg in re.split(",\s*", raw_args):
129 arg = re.split("\s", arg)
130 # ignore annotations for now
131 arg = [ a for a in arg if not a.startswith("@") ]
132 if len(arg[0]) > 0:
133 self.args.append(arg[0])
134
135 # parse throws
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700136 self.throws = []
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600137 for throw in re.split(",\s*", raw_throws):
138 self.throws.append(throw)
139
Jeff Sharkey8b141b92018-04-11 10:05:44 -0600140 self.ident = ident(self.raw)
Jeff Sharkey037458a2014-09-04 15:46:20 -0700141
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -0700142 def __hash__(self):
143 return hash(self.raw)
144
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700145 def __repr__(self):
146 return self.raw
147
148
149class Class():
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700150 def __init__(self, pkg, line, raw, blame):
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700151 self.pkg = pkg
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700152 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700153 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700154 self.blame = blame
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700155 self.ctors = []
156 self.fields = []
157 self.methods = []
158
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600159 # drop generics for now; may need multiple passes
160 raw = re.sub("<[^<]+?>", "", raw)
161 raw = re.sub("<[^<]+?>", "", raw)
162
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700163 raw = raw.split()
164 self.split = list(raw)
165 if "class" in raw:
166 self.fullname = raw[raw.index("class")+1]
167 elif "interface" in raw:
168 self.fullname = raw[raw.index("interface")+1]
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600169 elif "@interface" in raw:
170 self.fullname = raw[raw.index("@interface")+1]
Jeff Sharkey037458a2014-09-04 15:46:20 -0700171 else:
172 raise ValueError("Funky class type %s" % (self.raw))
173
174 if "extends" in raw:
175 self.extends = raw[raw.index("extends")+1]
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800176 self.extends_path = self.extends.split(".")
Jeff Sharkey037458a2014-09-04 15:46:20 -0700177 else:
178 self.extends = None
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800179 self.extends_path = []
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700180
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700181 self.fullname = self.pkg.name + "." + self.fullname
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800182 self.fullname_path = self.fullname.split(".")
183
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700184 self.name = self.fullname[self.fullname.rindex(".")+1:]
185
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -0700186 def __hash__(self):
187 return hash((self.raw, tuple(self.ctors), tuple(self.fields), tuple(self.methods)))
188
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700189 def __repr__(self):
190 return self.raw
191
192
193class Package():
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700194 def __init__(self, line, raw, blame):
195 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700196 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700197 self.blame = blame
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700198
199 raw = raw.split()
200 self.name = raw[raw.index("package")+1]
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800201 self.name_path = self.name.split(".")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700202
203 def __repr__(self):
204 return self.raw
205
206
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800207def _parse_stream(f, clazz_cb=None):
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700208 line = 0
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700209 api = {}
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700210 pkg = None
211 clazz = None
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700212 blame = None
213
214 re_blame = re.compile("^([a-z0-9]{7,}) \(<([^>]+)>.+?\) (.+?)$")
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800215 for raw in f:
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700216 line += 1
217 raw = raw.rstrip()
218 match = re_blame.match(raw)
219 if match is not None:
220 blame = match.groups()[0:2]
221 raw = match.groups()[2]
222 else:
223 blame = None
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700224
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700225 if raw.startswith("package"):
226 pkg = Package(line, raw, blame)
227 elif raw.startswith(" ") and raw.endswith("{"):
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800228 # When provided with class callback, we treat as incremental
229 # parse and don't build up entire API
230 if clazz and clazz_cb:
231 clazz_cb(clazz)
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700232 clazz = Class(pkg, line, raw, blame)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800233 if not clazz_cb:
234 api[clazz.fullname] = clazz
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700235 elif raw.startswith(" ctor"):
236 clazz.ctors.append(Method(clazz, line, raw, blame))
237 elif raw.startswith(" method"):
238 clazz.methods.append(Method(clazz, line, raw, blame))
239 elif raw.startswith(" field"):
240 clazz.fields.append(Field(clazz, line, raw, blame))
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700241
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800242 # Handle last trailing class
243 if clazz and clazz_cb:
244 clazz_cb(clazz)
245
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700246 return api
247
248
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700249class Failure():
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800250 def __init__(self, sig, clazz, detail, error, rule, msg):
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700251 self.sig = sig
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700252 self.error = error
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800253 self.rule = rule
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700254 self.msg = msg
255
256 if error:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800257 self.head = "Error %s" % (rule) if rule else "Error"
258 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 -0700259 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800260 self.head = "Warning %s" % (rule) if rule else "Warning"
261 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 -0700262
263 self.line = clazz.line
264 blame = clazz.blame
265 if detail is not None:
266 dump += "\n in " + repr(detail)
267 self.line = detail.line
268 blame = detail.blame
269 dump += "\n in " + repr(clazz)
270 dump += "\n in " + repr(clazz.pkg)
271 dump += "\n at line " + repr(self.line)
272 if blame is not None:
273 dump += "\n last modified by %s in %s" % (blame[1], blame[0])
274
275 self.dump = dump
276
277 def __repr__(self):
278 return self.dump
279
280
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700281failures = {}
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700282
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800283def _fail(clazz, detail, error, rule, msg):
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700284 """Records an API failure to be processed later."""
285 global failures
286
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700287 sig = "%s-%s-%s" % (clazz.fullname, repr(detail), msg)
288 sig = sig.replace(" deprecated ", " ")
289
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800290 failures[sig] = Failure(sig, clazz, detail, error, rule, msg)
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700291
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700292
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800293def warn(clazz, detail, rule, msg):
294 _fail(clazz, detail, False, rule, msg)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700295
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800296def error(clazz, detail, rule, msg):
297 _fail(clazz, detail, True, rule, msg)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700298
299
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -0700300noticed = {}
301
302def notice(clazz):
303 global noticed
304
305 noticed[clazz.fullname] = hash(clazz)
306
307
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700308def verify_constants(clazz):
309 """All static final constants must be FOO_NAME style."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700310 if re.match("android\.R\.[a-z]+", clazz.fullname): return
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600311 if clazz.fullname.startswith("android.os.Build"): return
312 if clazz.fullname == "android.system.OsConstants": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700313
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600314 req = ["java.lang.String","byte","short","int","long","float","double","boolean","char"]
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700315 for f in clazz.fields:
316 if "static" in f.split and "final" in f.split:
317 if re.match("[A-Z0-9_]+", f.name) is None:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800318 error(clazz, f, "C2", "Constant field names must be FOO_NAME")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600319 if f.typ != "java.lang.String":
Jeff Sharkey331279b2016-02-29 16:02:02 -0700320 if f.name.startswith("MIN_") or f.name.startswith("MAX_"):
321 warn(clazz, f, "C8", "If min/max could change in future, make them dynamic methods")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600322 if f.typ in req and f.value is None:
323 error(clazz, f, None, "All constants must be defined at compile time")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700324
325
326def verify_enums(clazz):
327 """Enums are bad, mmkay?"""
328 if "extends java.lang.Enum" in clazz.raw:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800329 error(clazz, None, "F5", "Enums are not allowed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700330
331
332def verify_class_names(clazz):
333 """Try catching malformed class names like myMtp or MTPUser."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700334 if clazz.fullname.startswith("android.opengl"): return
335 if clazz.fullname.startswith("android.renderscript"): return
336 if re.match("android\.R\.[a-z]+", clazz.fullname): return
337
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700338 if re.search("[A-Z]{2,}", clazz.name) is not None:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800339 warn(clazz, None, "S1", "Class names with acronyms should be Mtp not MTP")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700340 if re.match("[^A-Z]", clazz.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800341 error(clazz, None, "S1", "Class must start with uppercase char")
Jeff Sharkeyee21f692018-02-16 13:29:29 -0700342 if clazz.name.endswith("Impl"):
343 error(clazz, None, None, "Don't expose your implementation details")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700344
345
346def verify_method_names(clazz):
347 """Try catching malformed method names, like Foo() or getMTU()."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700348 if clazz.fullname.startswith("android.opengl"): return
349 if clazz.fullname.startswith("android.renderscript"): return
350 if clazz.fullname == "android.system.OsConstants": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700351
352 for m in clazz.methods:
353 if re.search("[A-Z]{2,}", m.name) is not None:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800354 warn(clazz, m, "S1", "Method names with acronyms should be getMtu() instead of getMTU()")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700355 if re.match("[^a-z]", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800356 error(clazz, m, "S1", "Method name must start with lowercase char")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700357
358
359def verify_callbacks(clazz):
360 """Verify Callback classes.
361 All callback classes must be abstract.
362 All methods must follow onFoo() naming style."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700363 if clazz.fullname == "android.speech.tts.SynthesisCallback": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700364
365 if clazz.name.endswith("Callbacks"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800366 error(clazz, None, "L1", "Callback class names should be singular")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700367 if clazz.name.endswith("Observer"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800368 warn(clazz, None, "L1", "Class should be named FooCallback")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700369
370 if clazz.name.endswith("Callback"):
371 if "interface" in clazz.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800372 error(clazz, None, "CL3", "Callbacks must be abstract class to enable extension in future API levels")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700373
374 for m in clazz.methods:
375 if not re.match("on[A-Z][a-z]*", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800376 error(clazz, m, "L1", "Callback method names must be onFoo() style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700377
378
379def verify_listeners(clazz):
380 """Verify Listener classes.
381 All Listener classes must be interface.
382 All methods must follow onFoo() naming style.
383 If only a single method, it must match class name:
384 interface OnFooListener { void onFoo() }"""
385
386 if clazz.name.endswith("Listener"):
387 if " abstract class " in clazz.raw:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800388 error(clazz, None, "L1", "Listeners should be an interface, or otherwise renamed Callback")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700389
390 for m in clazz.methods:
391 if not re.match("on[A-Z][a-z]*", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800392 error(clazz, m, "L1", "Listener method names must be onFoo() style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700393
394 if len(clazz.methods) == 1 and clazz.name.startswith("On"):
395 m = clazz.methods[0]
396 if (m.name + "Listener").lower() != clazz.name.lower():
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800397 error(clazz, m, "L1", "Single listener method name must match class name")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700398
399
400def verify_actions(clazz):
401 """Verify intent actions.
402 All action names must be named ACTION_FOO.
403 All action values must be scoped by package and match name:
404 package android.foo {
405 String ACTION_BAR = "android.foo.action.BAR";
406 }"""
407 for f in clazz.fields:
408 if f.value is None: continue
409 if f.name.startswith("EXTRA_"): continue
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700410 if f.name == "SERVICE_INTERFACE" or f.name == "PROVIDER_INTERFACE": continue
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600411 if "INTERACTION" in f.name: continue
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700412
413 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
414 if "_ACTION" in f.name or "ACTION_" in f.name or ".action." in f.value.lower():
415 if not f.name.startswith("ACTION_"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800416 error(clazz, f, "C3", "Intent action constant name must be ACTION_FOO")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700417 else:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700418 if clazz.fullname == "android.content.Intent":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700419 prefix = "android.intent.action"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700420 elif clazz.fullname == "android.provider.Settings":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700421 prefix = "android.settings"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700422 elif clazz.fullname == "android.app.admin.DevicePolicyManager" or clazz.fullname == "android.app.admin.DeviceAdminReceiver":
423 prefix = "android.app.action"
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700424 else:
425 prefix = clazz.pkg.name + ".action"
426 expected = prefix + "." + f.name[7:]
427 if f.value != expected:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700428 error(clazz, f, "C4", "Inconsistent action value; expected '%s'" % (expected))
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700429
430
431def verify_extras(clazz):
432 """Verify intent extras.
433 All extra names must be named EXTRA_FOO.
434 All extra values must be scoped by package and match name:
435 package android.foo {
436 String EXTRA_BAR = "android.foo.extra.BAR";
437 }"""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700438 if clazz.fullname == "android.app.Notification": return
439 if clazz.fullname == "android.appwidget.AppWidgetManager": return
440
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700441 for f in clazz.fields:
442 if f.value is None: continue
443 if f.name.startswith("ACTION_"): continue
444
445 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
446 if "_EXTRA" in f.name or "EXTRA_" in f.name or ".extra" in f.value.lower():
447 if not f.name.startswith("EXTRA_"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800448 error(clazz, f, "C3", "Intent extra must be EXTRA_FOO")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700449 else:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700450 if clazz.pkg.name == "android.content" and clazz.name == "Intent":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700451 prefix = "android.intent.extra"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700452 elif clazz.pkg.name == "android.app.admin":
453 prefix = "android.app.extra"
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700454 else:
455 prefix = clazz.pkg.name + ".extra"
456 expected = prefix + "." + f.name[6:]
457 if f.value != expected:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700458 error(clazz, f, "C4", "Inconsistent extra value; expected '%s'" % (expected))
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700459
460
461def verify_equals(clazz):
462 """Verify that equals() and hashCode() must be overridden together."""
Jeff Sharkey40d623e2016-12-21 13:46:33 -0700463 eq = False
464 hc = False
465 for m in clazz.methods:
466 if " static " in m.raw: continue
467 if "boolean equals(java.lang.Object)" in m.raw: eq = True
468 if "int hashCode()" in m.raw: hc = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700469 if eq != hc:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800470 error(clazz, None, "M8", "Must override both equals and hashCode; missing one")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700471
472
473def verify_parcelable(clazz):
474 """Verify that Parcelable objects aren't hiding required bits."""
475 if "implements android.os.Parcelable" in clazz.raw:
476 creator = [ i for i in clazz.fields if i.name == "CREATOR" ]
477 write = [ i for i in clazz.methods if i.name == "writeToParcel" ]
478 describe = [ i for i in clazz.methods if i.name == "describeContents" ]
479
480 if len(creator) == 0 or len(write) == 0 or len(describe) == 0:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800481 error(clazz, None, "FW3", "Parcelable requires CREATOR, writeToParcel, and describeContents; missing one")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700482
Joe LaPenna45380002017-04-20 12:49:48 -0700483 if ((" final class " not in clazz.raw) and
484 (" final deprecated class " not in clazz.raw)):
Jeff Sharkey331279b2016-02-29 16:02:02 -0700485 error(clazz, None, "FW8", "Parcelable classes must be final")
486
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700487 for c in clazz.ctors:
488 if c.args == ["android.os.Parcel"]:
489 error(clazz, c, "FW3", "Parcelable inflation is exposed through CREATOR, not raw constructors")
490
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700491
492def verify_protected(clazz):
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800493 """Verify that no protected methods or fields are allowed."""
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700494 for m in clazz.methods:
495 if "protected" in m.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800496 error(clazz, m, "M7", "Protected methods not allowed; must be public")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700497 for f in clazz.fields:
498 if "protected" in f.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800499 error(clazz, f, "M7", "Protected fields not allowed; must be public")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700500
501
502def verify_fields(clazz):
503 """Verify that all exposed fields are final.
504 Exposed fields must follow myName style.
505 Catch internal mFoo objects being exposed."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700506
507 IGNORE_BARE_FIELDS = [
508 "android.app.ActivityManager.RecentTaskInfo",
509 "android.app.Notification",
510 "android.content.pm.ActivityInfo",
511 "android.content.pm.ApplicationInfo",
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600512 "android.content.pm.ComponentInfo",
513 "android.content.pm.ResolveInfo",
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700514 "android.content.pm.FeatureGroupInfo",
515 "android.content.pm.InstrumentationInfo",
516 "android.content.pm.PackageInfo",
517 "android.content.pm.PackageItemInfo",
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600518 "android.content.res.Configuration",
519 "android.graphics.BitmapFactory.Options",
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700520 "android.os.Message",
521 "android.system.StructPollfd",
522 ]
523
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700524 for f in clazz.fields:
525 if not "final" in f.split:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700526 if clazz.fullname in IGNORE_BARE_FIELDS:
527 pass
528 elif clazz.fullname.endswith("LayoutParams"):
529 pass
530 elif clazz.fullname.startswith("android.util.Mutable"):
531 pass
532 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800533 error(clazz, f, "F2", "Bare fields must be marked final, or add accessors if mutable")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700534
535 if not "static" in f.split:
536 if not re.match("[a-z]([a-zA-Z]+)?", f.name):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800537 error(clazz, f, "S1", "Non-static fields must be named using myField style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700538
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700539 if re.match("[ms][A-Z]", f.name):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800540 error(clazz, f, "F1", "Internal objects must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700541
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700542 if re.match("[A-Z_]+", f.name):
543 if "static" not in f.split or "final" not in f.split:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800544 error(clazz, f, "C2", "Constants must be marked static final")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700545
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700546
547def verify_register(clazz):
548 """Verify parity of registration methods.
549 Callback objects use register/unregister methods.
550 Listener objects use add/remove methods."""
551 methods = [ m.name for m in clazz.methods ]
552 for m in clazz.methods:
553 if "Callback" in m.raw:
554 if m.name.startswith("register"):
555 other = "unregister" + m.name[8:]
556 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800557 error(clazz, m, "L2", "Missing unregister method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700558 if m.name.startswith("unregister"):
559 other = "register" + m.name[10:]
560 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800561 error(clazz, m, "L2", "Missing register method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700562
563 if m.name.startswith("add") or m.name.startswith("remove"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800564 error(clazz, m, "L3", "Callback methods should be named register/unregister")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700565
566 if "Listener" in m.raw:
567 if m.name.startswith("add"):
568 other = "remove" + m.name[3:]
569 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800570 error(clazz, m, "L2", "Missing remove method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700571 if m.name.startswith("remove") and not m.name.startswith("removeAll"):
572 other = "add" + m.name[6:]
573 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800574 error(clazz, m, "L2", "Missing add method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700575
576 if m.name.startswith("register") or m.name.startswith("unregister"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800577 error(clazz, m, "L3", "Listener methods should be named add/remove")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700578
579
580def verify_sync(clazz):
581 """Verify synchronized methods aren't exposed."""
582 for m in clazz.methods:
583 if "synchronized" in m.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800584 error(clazz, m, "M5", "Internal locks must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700585
586
587def verify_intent_builder(clazz):
588 """Verify that Intent builders are createFooIntent() style."""
589 if clazz.name == "Intent": return
590
591 for m in clazz.methods:
592 if m.typ == "android.content.Intent":
593 if m.name.startswith("create") and m.name.endswith("Intent"):
594 pass
595 else:
Adam Powell539ea122015-04-10 13:01:37 -0700596 warn(clazz, m, "FW1", "Methods creating an Intent should be named createFooIntent()")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700597
598
599def verify_helper_classes(clazz):
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700600 """Verify that helper classes are named consistently with what they extend.
601 All developer extendable methods should be named onFoo()."""
602 test_methods = False
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700603 if "extends android.app.Service" in clazz.raw:
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700604 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700605 if not clazz.name.endswith("Service"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800606 error(clazz, None, "CL4", "Inconsistent class name; should be FooService")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700607
608 found = False
609 for f in clazz.fields:
610 if f.name == "SERVICE_INTERFACE":
611 found = True
612 if f.value != clazz.fullname:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700613 error(clazz, f, "C4", "Inconsistent interface constant; expected '%s'" % (clazz.fullname))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700614
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700615 if "extends android.content.ContentProvider" in clazz.raw:
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700616 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700617 if not clazz.name.endswith("Provider"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800618 error(clazz, None, "CL4", "Inconsistent class name; should be FooProvider")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700619
620 found = False
621 for f in clazz.fields:
622 if f.name == "PROVIDER_INTERFACE":
623 found = True
624 if f.value != clazz.fullname:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700625 error(clazz, f, "C4", "Inconsistent interface constant; expected '%s'" % (clazz.fullname))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700626
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700627 if "extends android.content.BroadcastReceiver" in clazz.raw:
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700628 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700629 if not clazz.name.endswith("Receiver"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800630 error(clazz, None, "CL4", "Inconsistent class name; should be FooReceiver")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700631
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700632 if "extends android.app.Activity" in clazz.raw:
633 test_methods = True
634 if not clazz.name.endswith("Activity"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800635 error(clazz, None, "CL4", "Inconsistent class name; should be FooActivity")
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700636
637 if test_methods:
638 for m in clazz.methods:
639 if "final" in m.split: continue
640 if not re.match("on[A-Z]", m.name):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700641 if "abstract" in m.split:
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800642 warn(clazz, m, None, "Methods implemented by developers should be named onFoo()")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700643 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800644 warn(clazz, m, None, "If implemented by developer, should be named onFoo(); otherwise consider marking final")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700645
646
647def verify_builder(clazz):
648 """Verify builder classes.
649 Methods should return the builder to enable chaining."""
650 if " extends " in clazz.raw: return
651 if not clazz.name.endswith("Builder"): return
652
653 if clazz.name != "Builder":
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800654 warn(clazz, None, None, "Builder should be defined as inner class")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700655
656 has_build = False
657 for m in clazz.methods:
658 if m.name == "build":
659 has_build = True
660 continue
661
662 if m.name.startswith("get"): continue
663 if m.name.startswith("clear"): continue
664
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700665 if m.name.startswith("with"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800666 warn(clazz, m, None, "Builder methods names should use setFoo() style")
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700667
668 if m.name.startswith("set"):
669 if not m.typ.endswith(clazz.fullname):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800670 warn(clazz, m, "M4", "Methods must return the builder object")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700671
672 if not has_build:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800673 warn(clazz, None, None, "Missing build() method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700674
675
676def verify_aidl(clazz):
677 """Catch people exposing raw AIDL."""
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700678 if "extends android.os.Binder" in clazz.raw or "implements android.os.IInterface" in clazz.raw:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800679 error(clazz, None, None, "Raw AIDL interfaces must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700680
681
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700682def verify_internal(clazz):
683 """Catch people exposing internal classes."""
684 if clazz.pkg.name.startswith("com.android"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800685 error(clazz, None, None, "Internal classes must not be exposed")
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700686
687
688def verify_layering(clazz):
689 """Catch package layering violations.
690 For example, something in android.os depending on android.app."""
691 ranking = [
692 ["android.service","android.accessibilityservice","android.inputmethodservice","android.printservice","android.appwidget","android.webkit","android.preference","android.gesture","android.print"],
693 "android.app",
694 "android.widget",
695 "android.view",
696 "android.animation",
697 "android.provider",
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700698 ["android.content","android.graphics.drawable"],
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700699 "android.database",
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700700 "android.text",
Siyamed Sinir0a2e15d2018-09-13 16:06:59 -0700701 "android.graphics",
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700702 "android.os",
703 "android.util"
704 ]
705
706 def rank(p):
707 for i in range(len(ranking)):
708 if isinstance(ranking[i], list):
709 for j in ranking[i]:
710 if p.startswith(j): return i
711 else:
712 if p.startswith(ranking[i]): return i
713
714 cr = rank(clazz.pkg.name)
715 if cr is None: return
716
717 for f in clazz.fields:
718 ir = rank(f.typ)
719 if ir and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800720 warn(clazz, f, "FW6", "Field type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700721
722 for m in clazz.methods:
723 ir = rank(m.typ)
724 if ir and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800725 warn(clazz, m, "FW6", "Method return type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700726 for arg in m.args:
727 ir = rank(arg)
728 if ir and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800729 warn(clazz, m, "FW6", "Method argument type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -0700730
731
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800732def verify_boolean(clazz):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800733 """Verifies that boolean accessors are named correctly.
734 For example, hasFoo() and setHasFoo()."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700735
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800736 def is_get(m): return len(m.args) == 0 and m.typ == "boolean"
737 def is_set(m): return len(m.args) == 1 and m.args[0] == "boolean"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700738
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800739 gets = [ m for m in clazz.methods if is_get(m) ]
740 sets = [ m for m in clazz.methods if is_set(m) ]
741
742 def error_if_exists(methods, trigger, expected, actual):
743 for m in methods:
744 if m.name == actual:
745 error(clazz, m, "M6", "Symmetric method for %s must be named %s" % (trigger, expected))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700746
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700747 for m in clazz.methods:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800748 if is_get(m):
749 if re.match("is[A-Z]", m.name):
750 target = m.name[2:]
751 expected = "setIs" + target
752 error_if_exists(sets, m.name, expected, "setHas" + target)
753 elif re.match("has[A-Z]", m.name):
754 target = m.name[3:]
755 expected = "setHas" + target
756 error_if_exists(sets, m.name, expected, "setIs" + target)
757 error_if_exists(sets, m.name, expected, "set" + target)
758 elif re.match("get[A-Z]", m.name):
759 target = m.name[3:]
760 expected = "set" + target
761 error_if_exists(sets, m.name, expected, "setIs" + target)
762 error_if_exists(sets, m.name, expected, "setHas" + target)
763
764 if is_set(m):
765 if re.match("set[A-Z]", m.name):
766 target = m.name[3:]
767 expected = "get" + target
768 error_if_exists(sets, m.name, expected, "is" + target)
769 error_if_exists(sets, m.name, expected, "has" + target)
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700770
771
772def verify_collections(clazz):
773 """Verifies that collection types are interfaces."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700774 if clazz.fullname == "android.os.Bundle": return
775
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700776 bad = ["java.util.Vector", "java.util.LinkedList", "java.util.ArrayList", "java.util.Stack",
777 "java.util.HashMap", "java.util.HashSet", "android.util.ArraySet", "android.util.ArrayMap"]
778 for m in clazz.methods:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700779 if m.typ in bad:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800780 error(clazz, m, "CL2", "Return type is concrete collection; must be higher-level interface")
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700781 for arg in m.args:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700782 if arg in bad:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800783 error(clazz, m, "CL2", "Argument is concrete collection; must be higher-level interface")
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700784
785
786def verify_flags(clazz):
787 """Verifies that flags are non-overlapping."""
788 known = collections.defaultdict(int)
789 for f in clazz.fields:
790 if "FLAG_" in f.name:
791 try:
792 val = int(f.value)
793 except:
794 continue
795
796 scope = f.name[0:f.name.index("FLAG_")]
797 if val & known[scope]:
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800798 warn(clazz, f, "C1", "Found overlapping flag constant value")
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700799 known[scope] |= val
800
801
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800802def verify_exception(clazz):
803 """Verifies that methods don't throw generic exceptions."""
804 for m in clazz.methods:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700805 for t in m.throws:
806 if t in ["java.lang.Exception", "java.lang.Throwable", "java.lang.Error"]:
807 error(clazz, m, "S1", "Methods must not throw generic exceptions")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800808
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700809 if t in ["android.os.RemoteException"]:
810 if clazz.name == "android.content.ContentProviderClient": continue
811 if clazz.name == "android.os.Binder": continue
812 if clazz.name == "android.os.IBinder": continue
Jeff Sharkey331279b2016-02-29 16:02:02 -0700813
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700814 error(clazz, m, "FW9", "Methods calling into system server should rethrow RemoteException as RuntimeException")
815
816 if len(m.args) == 0 and t in ["java.lang.IllegalArgumentException", "java.lang.NullPointerException"]:
817 warn(clazz, m, "S1", "Methods taking no arguments should throw IllegalStateException")
Jeff Sharkey331279b2016-02-29 16:02:02 -0700818
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800819
820def verify_google(clazz):
821 """Verifies that APIs never reference Google."""
822
823 if re.search("google", clazz.raw, re.IGNORECASE):
824 error(clazz, None, None, "Must never reference Google")
825
826 test = []
827 test.extend(clazz.ctors)
828 test.extend(clazz.fields)
829 test.extend(clazz.methods)
830
831 for t in test:
832 if re.search("google", t.raw, re.IGNORECASE):
833 error(clazz, t, None, "Must never reference Google")
834
835
836def verify_bitset(clazz):
837 """Verifies that we avoid using heavy BitSet."""
838
839 for f in clazz.fields:
840 if f.typ == "java.util.BitSet":
841 error(clazz, f, None, "Field type must not be heavy BitSet")
842
843 for m in clazz.methods:
844 if m.typ == "java.util.BitSet":
845 error(clazz, m, None, "Return type must not be heavy BitSet")
846 for arg in m.args:
847 if arg == "java.util.BitSet":
848 error(clazz, m, None, "Argument type must not be heavy BitSet")
849
850
851def verify_manager(clazz):
852 """Verifies that FooManager is only obtained from Context."""
853
854 if not clazz.name.endswith("Manager"): return
855
856 for c in clazz.ctors:
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800857 error(clazz, c, None, "Managers must always be obtained from Context; no direct constructors")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800858
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600859 for m in clazz.methods:
860 if m.typ == clazz.fullname:
861 error(clazz, m, None, "Managers must always be obtained from Context")
862
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800863
864def verify_boxed(clazz):
865 """Verifies that methods avoid boxed primitives."""
866
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800867 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 -0800868
869 for c in clazz.ctors:
870 for arg in c.args:
871 if arg in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800872 error(clazz, c, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800873
874 for f in clazz.fields:
875 if f.typ in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800876 error(clazz, f, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800877
878 for m in clazz.methods:
879 if m.typ in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800880 error(clazz, m, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800881 for arg in m.args:
882 if arg in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800883 error(clazz, m, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800884
885
886def verify_static_utils(clazz):
887 """Verifies that helper classes can't be constructed."""
888 if clazz.fullname.startswith("android.opengl"): return
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600889 if clazz.fullname.startswith("android.R"): return
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800890
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600891 # Only care about classes with default constructors
892 if len(clazz.ctors) == 1 and len(clazz.ctors[0].args) == 0:
893 test = []
894 test.extend(clazz.fields)
895 test.extend(clazz.methods)
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800896
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600897 if len(test) == 0: return
898 for t in test:
899 if "static" not in t.split:
900 return
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800901
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800902 error(clazz, None, None, "Fully-static utility classes must not have constructor")
903
904
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800905def verify_overload_args(clazz):
906 """Verifies that method overloads add new arguments at the end."""
907 if clazz.fullname.startswith("android.opengl"): return
908
909 overloads = collections.defaultdict(list)
910 for m in clazz.methods:
911 if "deprecated" in m.split: continue
912 overloads[m.name].append(m)
913
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800914 for name, methods in overloads.items():
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800915 if len(methods) <= 1: continue
916
917 # Look for arguments common across all overloads
918 def cluster(args):
919 count = collections.defaultdict(int)
920 res = set()
921 for i in range(len(args)):
922 a = args[i]
923 res.add("%s#%d" % (a, count[a]))
924 count[a] += 1
925 return res
926
927 common_args = cluster(methods[0].args)
928 for m in methods:
929 common_args = common_args & cluster(m.args)
930
931 if len(common_args) == 0: continue
932
933 # Require that all common arguments are present at start of signature
934 locked_sig = None
935 for m in methods:
936 sig = m.args[0:len(common_args)]
937 if not common_args.issubset(cluster(sig)):
938 warn(clazz, m, "M2", "Expected common arguments [%s] at beginning of overloaded method" % (", ".join(common_args)))
939 elif not locked_sig:
940 locked_sig = sig
941 elif locked_sig != sig:
942 error(clazz, m, "M2", "Expected consistent argument ordering between overloads: %s..." % (", ".join(locked_sig)))
943
944
945def verify_callback_handlers(clazz):
946 """Verifies that methods adding listener/callback have overload
947 for specifying delivery thread."""
948
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800949 # Ignore UI packages which assume main thread
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800950 skip = [
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800951 "animation",
952 "view",
953 "graphics",
954 "transition",
955 "widget",
956 "webkit",
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800957 ]
958 for s in skip:
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800959 if s in clazz.pkg.name_path: return
960 if s in clazz.extends_path: return
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800961
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800962 # Ignore UI classes which assume main thread
963 if "app" in clazz.pkg.name_path or "app" in clazz.extends_path:
964 for s in ["ActionBar","Dialog","Application","Activity","Fragment","Loader"]:
965 if s in clazz.fullname: return
966 if "content" in clazz.pkg.name_path or "content" in clazz.extends_path:
967 for s in ["Loader"]:
968 if s in clazz.fullname: return
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800969
970 found = {}
971 by_name = collections.defaultdict(list)
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700972 examine = clazz.ctors + clazz.methods
973 for m in examine:
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800974 if m.name.startswith("unregister"): continue
975 if m.name.startswith("remove"): continue
976 if re.match("on[A-Z]+", m.name): continue
977
978 by_name[m.name].append(m)
979
980 for a in m.args:
981 if a.endswith("Listener") or a.endswith("Callback") or a.endswith("Callbacks"):
982 found[m.name] = m
983
984 for f in found.values():
985 takes_handler = False
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -0700986 takes_exec = False
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800987 for m in by_name[f.name]:
988 if "android.os.Handler" in m.args:
989 takes_handler = True
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -0700990 if "java.util.concurrent.Executor" in m.args:
991 takes_exec = True
992 if not takes_exec:
993 warn(clazz, f, "L1", "Registration methods should have overload that accepts delivery Executor")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800994
995
996def verify_context_first(clazz):
997 """Verifies that methods accepting a Context keep it the first argument."""
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800998 examine = clazz.ctors + clazz.methods
999 for m in examine:
1000 if len(m.args) > 1 and m.args[0] != "android.content.Context":
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001001 if "android.content.Context" in m.args[1:]:
1002 error(clazz, m, "M3", "Context is distinct, so it must be the first argument")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001003 if len(m.args) > 1 and m.args[0] != "android.content.ContentResolver":
1004 if "android.content.ContentResolver" in m.args[1:]:
1005 error(clazz, m, "M3", "ContentResolver is distinct, so it must be the first argument")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001006
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001007
1008def verify_listener_last(clazz):
1009 """Verifies that methods accepting a Listener or Callback keep them as last arguments."""
1010 examine = clazz.ctors + clazz.methods
1011 for m in examine:
1012 if "Listener" in m.name or "Callback" in m.name: continue
1013 found = False
1014 for a in m.args:
1015 if a.endswith("Callback") or a.endswith("Callbacks") or a.endswith("Listener"):
1016 found = True
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001017 elif found:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001018 warn(clazz, m, "M3", "Listeners should always be at end of argument list")
1019
1020
1021def verify_resource_names(clazz):
1022 """Verifies that resource names have consistent case."""
1023 if not re.match("android\.R\.[a-z]+", clazz.fullname): return
1024
1025 # Resources defined by files are foo_bar_baz
1026 if clazz.name in ["anim","animator","color","dimen","drawable","interpolator","layout","transition","menu","mipmap","string","plurals","raw","xml"]:
1027 for f in clazz.fields:
1028 if re.match("[a-z1-9_]+$", f.name): continue
1029 error(clazz, f, None, "Expected resource name in this class to be foo_bar_baz style")
1030
1031 # Resources defined inside files are fooBarBaz
1032 if clazz.name in ["array","attr","id","bool","fraction","integer"]:
1033 for f in clazz.fields:
1034 if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue
1035 if re.match("layout_[a-z][a-zA-Z1-9]*$", f.name): continue
1036 if re.match("state_[a-z_]*$", f.name): continue
1037
1038 if re.match("[a-z][a-zA-Z1-9]*$", f.name): continue
1039 error(clazz, f, "C7", "Expected resource name in this class to be fooBarBaz style")
1040
1041 # Styles are FooBar_Baz
1042 if clazz.name in ["style"]:
1043 for f in clazz.fields:
1044 if re.match("[A-Z][A-Za-z1-9]+(_[A-Z][A-Za-z1-9]+?)*$", f.name): continue
1045 error(clazz, f, "C7", "Expected resource name in this class to be FooBar_Baz style")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001046
1047
Jeff Sharkey331279b2016-02-29 16:02:02 -07001048def verify_files(clazz):
1049 """Verifies that methods accepting File also accept streams."""
1050
1051 has_file = set()
1052 has_stream = set()
1053
1054 test = []
1055 test.extend(clazz.ctors)
1056 test.extend(clazz.methods)
1057
1058 for m in test:
1059 if "java.io.File" in m.args:
1060 has_file.add(m)
1061 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:
1062 has_stream.add(m.name)
1063
1064 for m in has_file:
1065 if m.name not in has_stream:
1066 warn(clazz, m, "M10", "Methods accepting File should also accept FileDescriptor or streams")
1067
1068
Jeff Sharkey70168dd2016-03-30 21:47:16 -06001069def verify_manager_list(clazz):
1070 """Verifies that managers return List<? extends Parcelable> instead of arrays."""
1071
1072 if not clazz.name.endswith("Manager"): return
1073
1074 for m in clazz.methods:
1075 if m.typ.startswith("android.") and m.typ.endswith("[]"):
1076 warn(clazz, m, None, "Methods should return List<? extends Parcelable> instead of Parcelable[] to support ParceledListSlice under the hood")
1077
1078
Jeff Sharkey26c80902016-12-21 13:41:17 -07001079def verify_abstract_inner(clazz):
1080 """Verifies that abstract inner classes are static."""
1081
1082 if re.match(".+?\.[A-Z][^\.]+\.[A-Z]", clazz.fullname):
1083 if " abstract " in clazz.raw and " static " not in clazz.raw:
1084 warn(clazz, None, None, "Abstract inner classes should be static to improve testability")
1085
1086
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001087def verify_runtime_exceptions(clazz):
1088 """Verifies that runtime exceptions aren't listed in throws."""
1089
1090 banned = [
1091 "java.lang.NullPointerException",
1092 "java.lang.ClassCastException",
1093 "java.lang.IndexOutOfBoundsException",
1094 "java.lang.reflect.UndeclaredThrowableException",
1095 "java.lang.reflect.MalformedParametersException",
1096 "java.lang.reflect.MalformedParameterizedTypeException",
1097 "java.lang.invoke.WrongMethodTypeException",
1098 "java.lang.EnumConstantNotPresentException",
1099 "java.lang.IllegalMonitorStateException",
1100 "java.lang.SecurityException",
1101 "java.lang.UnsupportedOperationException",
1102 "java.lang.annotation.AnnotationTypeMismatchException",
1103 "java.lang.annotation.IncompleteAnnotationException",
1104 "java.lang.TypeNotPresentException",
1105 "java.lang.IllegalStateException",
1106 "java.lang.ArithmeticException",
1107 "java.lang.IllegalArgumentException",
1108 "java.lang.ArrayStoreException",
1109 "java.lang.NegativeArraySizeException",
1110 "java.util.MissingResourceException",
1111 "java.util.EmptyStackException",
1112 "java.util.concurrent.CompletionException",
1113 "java.util.concurrent.RejectedExecutionException",
1114 "java.util.IllformedLocaleException",
1115 "java.util.ConcurrentModificationException",
1116 "java.util.NoSuchElementException",
1117 "java.io.UncheckedIOException",
1118 "java.time.DateTimeException",
1119 "java.security.ProviderException",
1120 "java.nio.BufferUnderflowException",
1121 "java.nio.BufferOverflowException",
1122 ]
1123
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001124 examine = clazz.ctors + clazz.methods
1125 for m in examine:
1126 for t in m.throws:
1127 if t in banned:
1128 error(clazz, m, None, "Methods must not mention RuntimeException subclasses in throws clauses")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001129
1130
1131def verify_error(clazz):
1132 """Verifies that we always use Exception instead of Error."""
1133 if not clazz.extends: return
1134 if clazz.extends.endswith("Error"):
1135 error(clazz, None, None, "Trouble must be reported through an Exception, not Error")
1136 if clazz.extends.endswith("Exception") and not clazz.name.endswith("Exception"):
1137 error(clazz, None, None, "Exceptions must be named FooException")
1138
1139
1140def verify_units(clazz):
1141 """Verifies that we use consistent naming for units."""
1142
1143 # If we find K, recommend replacing with V
1144 bad = {
1145 "Ns": "Nanos",
1146 "Ms": "Millis or Micros",
1147 "Sec": "Seconds", "Secs": "Seconds",
1148 "Hr": "Hours", "Hrs": "Hours",
1149 "Mo": "Months", "Mos": "Months",
1150 "Yr": "Years", "Yrs": "Years",
1151 "Byte": "Bytes", "Space": "Bytes",
1152 }
1153
1154 for m in clazz.methods:
1155 if m.typ not in ["short","int","long"]: continue
1156 for k, v in bad.iteritems():
1157 if m.name.endswith(k):
1158 error(clazz, m, None, "Expected method name units to be " + v)
1159 if m.name.endswith("Nanos") or m.name.endswith("Micros"):
1160 warn(clazz, m, None, "Returned time values are strongly encouraged to be in milliseconds unless you need the extra precision")
1161 if m.name.endswith("Seconds"):
1162 error(clazz, m, None, "Returned time values must be in milliseconds")
1163
1164 for m in clazz.methods:
1165 typ = m.typ
1166 if typ == "void":
1167 if len(m.args) != 1: continue
1168 typ = m.args[0]
1169
1170 if m.name.endswith("Fraction") and typ != "float":
1171 error(clazz, m, None, "Fractions must use floats")
1172 if m.name.endswith("Percentage") and typ != "int":
1173 error(clazz, m, None, "Percentage must use ints")
1174
1175
1176def verify_closable(clazz):
1177 """Verifies that classes are AutoClosable."""
1178 if "implements java.lang.AutoCloseable" in clazz.raw: return
1179 if "implements java.io.Closeable" in clazz.raw: return
1180
1181 for m in clazz.methods:
1182 if len(m.args) > 0: continue
1183 if m.name in ["close","release","destroy","finish","finalize","disconnect","shutdown","stop","free","quit"]:
1184 warn(clazz, m, None, "Classes that release resources should implement AutoClosable and CloseGuard")
1185 return
1186
1187
Jake Wharton9e6738f2017-08-23 11:59:55 -04001188def verify_member_name_not_kotlin_keyword(clazz):
1189 """Prevent method names which are keywords in Kotlin."""
1190
1191 # https://kotlinlang.org/docs/reference/keyword-reference.html#hard-keywords
1192 # This list does not include Java keywords as those are already impossible to use.
1193 keywords = [
1194 'as',
1195 'fun',
1196 'in',
1197 'is',
1198 'object',
1199 'typealias',
1200 'val',
1201 'var',
1202 'when',
1203 ]
1204
1205 for m in clazz.methods:
1206 if m.name in keywords:
1207 error(clazz, m, None, "Method name must not be a Kotlin keyword")
1208 for f in clazz.fields:
1209 if f.name in keywords:
1210 error(clazz, f, None, "Field name must not be a Kotlin keyword")
1211
1212
1213def verify_method_name_not_kotlin_operator(clazz):
1214 """Warn about method names which become operators in Kotlin."""
1215
1216 binary = set()
1217
1218 def unique_binary_op(m, op):
1219 if op in binary:
1220 error(clazz, m, None, "Only one of '{0}' and '{0}Assign' methods should be present for Kotlin".format(op))
1221 binary.add(op)
1222
1223 for m in clazz.methods:
1224 if 'static' in m.split:
1225 continue
1226
1227 # https://kotlinlang.org/docs/reference/operator-overloading.html#unary-prefix-operators
1228 if m.name in ['unaryPlus', 'unaryMinus', 'not'] and len(m.args) == 0:
1229 warn(clazz, m, None, "Method can be invoked as a unary operator from Kotlin")
1230
1231 # https://kotlinlang.org/docs/reference/operator-overloading.html#increments-and-decrements
1232 if m.name in ['inc', 'dec'] and len(m.args) == 0 and m.typ != 'void':
1233 # This only applies if the return type is the same or a subtype of the enclosing class, but we have no
1234 # practical way of checking that relationship here.
1235 warn(clazz, m, None, "Method can be invoked as a pre/postfix inc/decrement operator from Kotlin")
1236
1237 # https://kotlinlang.org/docs/reference/operator-overloading.html#arithmetic
1238 if m.name in ['plus', 'minus', 'times', 'div', 'rem', 'mod', 'rangeTo'] and len(m.args) == 1:
1239 warn(clazz, m, None, "Method can be invoked as a binary operator from Kotlin")
1240 unique_binary_op(m, m.name)
1241
1242 # https://kotlinlang.org/docs/reference/operator-overloading.html#in
1243 if m.name == 'contains' and len(m.args) == 1 and m.typ == 'boolean':
1244 warn(clazz, m, None, "Method can be invoked as a 'in' operator from Kotlin")
1245
1246 # https://kotlinlang.org/docs/reference/operator-overloading.html#indexed
1247 if (m.name == 'get' and len(m.args) > 0) or (m.name == 'set' and len(m.args) > 1):
1248 warn(clazz, m, None, "Method can be invoked with an indexing operator from Kotlin")
1249
1250 # https://kotlinlang.org/docs/reference/operator-overloading.html#invoke
1251 if m.name == 'invoke':
1252 warn(clazz, m, None, "Method can be invoked with function call syntax from Kotlin")
1253
1254 # https://kotlinlang.org/docs/reference/operator-overloading.html#assignments
1255 if m.name in ['plusAssign', 'minusAssign', 'timesAssign', 'divAssign', 'remAssign', 'modAssign'] \
1256 and len(m.args) == 1 \
1257 and m.typ == 'void':
1258 warn(clazz, m, None, "Method can be invoked as a compound assignment operator from Kotlin")
1259 unique_binary_op(m, m.name[:-6]) # Remove 'Assign' suffix
1260
1261
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001262def verify_collections_over_arrays(clazz):
1263 """Warn that [] should be Collections."""
1264
1265 safe = ["java.lang.String[]","byte[]","short[]","int[]","long[]","float[]","double[]","boolean[]","char[]"]
1266 for m in clazz.methods:
1267 if m.typ.endswith("[]") and m.typ not in safe:
1268 warn(clazz, m, None, "Method should return Collection<> (or subclass) instead of raw array")
1269 for arg in m.args:
1270 if arg.endswith("[]") and arg not in safe:
1271 warn(clazz, m, None, "Method argument should be Collection<> (or subclass) instead of raw array")
1272
1273
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001274def verify_user_handle(clazz):
1275 """Methods taking UserHandle should be ForUser or AsUser."""
1276 if clazz.name.endswith("Listener") or clazz.name.endswith("Callback") or clazz.name.endswith("Callbacks"): return
1277 if clazz.fullname == "android.app.admin.DeviceAdminReceiver": return
1278 if clazz.fullname == "android.content.pm.LauncherApps": return
1279 if clazz.fullname == "android.os.UserHandle": return
1280 if clazz.fullname == "android.os.UserManager": return
1281
1282 for m in clazz.methods:
1283 if m.name.endswith("AsUser") or m.name.endswith("ForUser"): continue
1284 if re.match("on[A-Z]+", m.name): continue
1285 if "android.os.UserHandle" in m.args:
1286 warn(clazz, m, None, "Method taking UserHandle should be named 'doFooAsUser' or 'queryFooForUser'")
1287
1288
1289def verify_params(clazz):
1290 """Parameter classes should be 'Params'."""
1291 if clazz.name.endswith("Params"): return
1292 if clazz.fullname == "android.app.ActivityOptions": return
1293 if clazz.fullname == "android.app.BroadcastOptions": return
1294 if clazz.fullname == "android.os.Bundle": return
1295 if clazz.fullname == "android.os.BaseBundle": return
1296 if clazz.fullname == "android.os.PersistableBundle": return
1297
1298 bad = ["Param","Parameter","Parameters","Args","Arg","Argument","Arguments","Options","Bundle"]
1299 for b in bad:
1300 if clazz.name.endswith(b):
1301 error(clazz, None, None, "Classes holding a set of parameters should be called 'FooParams'")
1302
1303
1304def verify_services(clazz):
1305 """Service name should be FOO_BAR_SERVICE = 'foo_bar'."""
1306 if clazz.fullname != "android.content.Context": return
1307
1308 for f in clazz.fields:
1309 if f.typ != "java.lang.String": continue
1310 found = re.match(r"([A-Z_]+)_SERVICE", f.name)
1311 if found:
1312 expected = found.group(1).lower()
1313 if f.value != expected:
1314 error(clazz, f, "C4", "Inconsistent service value; expected '%s'" % (expected))
1315
1316
1317def verify_tense(clazz):
1318 """Verify tenses of method names."""
1319 if clazz.fullname.startswith("android.opengl"): return
1320
1321 for m in clazz.methods:
1322 if m.name.endswith("Enable"):
1323 warn(clazz, m, None, "Unexpected tense; probably meant 'enabled'")
1324
1325
Jeff Sharkeyee21f692018-02-16 13:29:29 -07001326def verify_icu(clazz):
1327 """Verifies that richer ICU replacements are used."""
1328 better = {
1329 "java.util.TimeZone": "android.icu.util.TimeZone",
1330 "java.util.Calendar": "android.icu.util.Calendar",
1331 "java.util.Locale": "android.icu.util.ULocale",
1332 "java.util.ResourceBundle": "android.icu.util.UResourceBundle",
1333 "java.util.SimpleTimeZone": "android.icu.util.SimpleTimeZone",
1334 "java.util.StringTokenizer": "android.icu.util.StringTokenizer",
1335 "java.util.GregorianCalendar": "android.icu.util.GregorianCalendar",
1336 "java.lang.Character": "android.icu.lang.UCharacter",
1337 "java.text.BreakIterator": "android.icu.text.BreakIterator",
1338 "java.text.Collator": "android.icu.text.Collator",
1339 "java.text.DecimalFormatSymbols": "android.icu.text.DecimalFormatSymbols",
1340 "java.text.NumberFormat": "android.icu.text.NumberFormat",
1341 "java.text.DateFormatSymbols": "android.icu.text.DateFormatSymbols",
1342 "java.text.DateFormat": "android.icu.text.DateFormat",
1343 "java.text.SimpleDateFormat": "android.icu.text.SimpleDateFormat",
1344 "java.text.MessageFormat": "android.icu.text.MessageFormat",
1345 "java.text.DecimalFormat": "android.icu.text.DecimalFormat",
1346 }
1347
1348 for m in clazz.ctors + clazz.methods:
1349 types = []
1350 types.extend(m.typ)
1351 types.extend(m.args)
1352 for arg in types:
1353 if arg in better:
1354 warn(clazz, m, None, "Type %s should be replaced with richer ICU type %s" % (arg, better[arg]))
1355
1356
1357def verify_clone(clazz):
1358 """Verify that clone() isn't implemented; see EJ page 61."""
1359 for m in clazz.methods:
1360 if m.name == "clone":
1361 error(clazz, m, None, "Provide an explicit copy constructor instead of implementing clone()")
1362
1363
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06001364def is_interesting(clazz):
1365 """Test if given class is interesting from an Android PoV."""
1366
1367 if clazz.pkg.name.startswith("java"): return False
1368 if clazz.pkg.name.startswith("junit"): return False
1369 if clazz.pkg.name.startswith("org.apache"): return False
1370 if clazz.pkg.name.startswith("org.xml"): return False
1371 if clazz.pkg.name.startswith("org.json"): return False
1372 if clazz.pkg.name.startswith("org.w3c"): return False
1373 if clazz.pkg.name.startswith("android.icu."): return False
1374 return True
1375
1376
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001377def examine_clazz(clazz):
1378 """Find all style issues in the given class."""
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001379
1380 notice(clazz)
1381
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06001382 if not is_interesting(clazz): return
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001383
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001384 verify_constants(clazz)
1385 verify_enums(clazz)
1386 verify_class_names(clazz)
1387 verify_method_names(clazz)
1388 verify_callbacks(clazz)
1389 verify_listeners(clazz)
1390 verify_actions(clazz)
1391 verify_extras(clazz)
1392 verify_equals(clazz)
1393 verify_parcelable(clazz)
1394 verify_protected(clazz)
1395 verify_fields(clazz)
1396 verify_register(clazz)
1397 verify_sync(clazz)
1398 verify_intent_builder(clazz)
1399 verify_helper_classes(clazz)
1400 verify_builder(clazz)
1401 verify_aidl(clazz)
1402 verify_internal(clazz)
1403 verify_layering(clazz)
1404 verify_boolean(clazz)
1405 verify_collections(clazz)
1406 verify_flags(clazz)
1407 verify_exception(clazz)
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07001408 if not ALLOW_GOOGLE: verify_google(clazz)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001409 verify_bitset(clazz)
1410 verify_manager(clazz)
1411 verify_boxed(clazz)
1412 verify_static_utils(clazz)
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001413 # verify_overload_args(clazz)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001414 verify_callback_handlers(clazz)
1415 verify_context_first(clazz)
1416 verify_listener_last(clazz)
1417 verify_resource_names(clazz)
Jeff Sharkey331279b2016-02-29 16:02:02 -07001418 verify_files(clazz)
Jeff Sharkey70168dd2016-03-30 21:47:16 -06001419 verify_manager_list(clazz)
Jeff Sharkey26c80902016-12-21 13:41:17 -07001420 verify_abstract_inner(clazz)
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001421 verify_runtime_exceptions(clazz)
1422 verify_error(clazz)
1423 verify_units(clazz)
1424 verify_closable(clazz)
Jake Wharton9e6738f2017-08-23 11:59:55 -04001425 verify_member_name_not_kotlin_keyword(clazz)
1426 verify_method_name_not_kotlin_operator(clazz)
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001427 verify_collections_over_arrays(clazz)
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001428 verify_user_handle(clazz)
1429 verify_params(clazz)
1430 verify_services(clazz)
1431 verify_tense(clazz)
Jeff Sharkeyee21f692018-02-16 13:29:29 -07001432 verify_icu(clazz)
1433 verify_clone(clazz)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001434
1435
1436def examine_stream(stream):
1437 """Find all style issues in the given API stream."""
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001438 global failures, noticed
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001439 failures = {}
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001440 noticed = {}
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001441 _parse_stream(stream, examine_clazz)
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001442 return (failures, noticed)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001443
1444
1445def examine_api(api):
1446 """Find all style issues in the given parsed API."""
1447 global failures
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001448 failures = {}
1449 for key in sorted(api.keys()):
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001450 examine_clazz(api[key])
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001451 return failures
1452
1453
Jeff Sharkey037458a2014-09-04 15:46:20 -07001454def verify_compat(cur, prev):
1455 """Find any incompatible API changes between two levels."""
1456 global failures
1457
1458 def class_exists(api, test):
1459 return test.fullname in api
1460
1461 def ctor_exists(api, clazz, test):
1462 for m in clazz.ctors:
1463 if m.ident == test.ident: return True
1464 return False
1465
1466 def all_methods(api, clazz):
1467 methods = list(clazz.methods)
1468 if clazz.extends is not None:
1469 methods.extend(all_methods(api, api[clazz.extends]))
1470 return methods
1471
1472 def method_exists(api, clazz, test):
1473 methods = all_methods(api, clazz)
1474 for m in methods:
1475 if m.ident == test.ident: return True
1476 return False
1477
1478 def field_exists(api, clazz, test):
1479 for f in clazz.fields:
1480 if f.ident == test.ident: return True
1481 return False
1482
1483 failures = {}
1484 for key in sorted(prev.keys()):
1485 prev_clazz = prev[key]
1486
1487 if not class_exists(cur, prev_clazz):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001488 error(prev_clazz, None, None, "Class removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07001489 continue
1490
1491 cur_clazz = cur[key]
1492
1493 for test in prev_clazz.ctors:
1494 if not ctor_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001495 error(prev_clazz, prev_ctor, None, "Constructor removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07001496
1497 methods = all_methods(prev, prev_clazz)
1498 for test in methods:
1499 if not method_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001500 error(prev_clazz, test, None, "Method removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07001501
1502 for test in prev_clazz.fields:
1503 if not field_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001504 error(prev_clazz, test, None, "Field removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07001505
1506 return failures
1507
1508
Jeff Sharkey8b141b92018-04-11 10:05:44 -06001509def show_deprecations_at_birth(cur, prev):
1510 """Show API deprecations at birth."""
1511 global failures
1512
1513 # Remove all existing things so we're left with new
1514 for prev_clazz in prev.values():
1515 cur_clazz = cur[prev_clazz.fullname]
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06001516 if not is_interesting(cur_clazz): continue
Jeff Sharkey8b141b92018-04-11 10:05:44 -06001517
1518 sigs = { i.ident: i for i in prev_clazz.ctors }
1519 cur_clazz.ctors = [ i for i in cur_clazz.ctors if i.ident not in sigs ]
1520 sigs = { i.ident: i for i in prev_clazz.methods }
1521 cur_clazz.methods = [ i for i in cur_clazz.methods if i.ident not in sigs ]
1522 sigs = { i.ident: i for i in prev_clazz.fields }
1523 cur_clazz.fields = [ i for i in cur_clazz.fields if i.ident not in sigs ]
1524
1525 # Forget about class entirely when nothing new
1526 if len(cur_clazz.ctors) == 0 and len(cur_clazz.methods) == 0 and len(cur_clazz.fields) == 0:
1527 del cur[prev_clazz.fullname]
1528
1529 for clazz in cur.values():
1530 if " deprecated " in clazz.raw and not clazz.fullname in prev:
1531 error(clazz, None, None, "Found API deprecation at birth")
1532
1533 for i in clazz.ctors + clazz.methods + clazz.fields:
1534 if " deprecated " in i.raw:
1535 error(clazz, i, None, "Found API deprecation at birth")
1536
1537 print "%s Deprecated at birth %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True),
1538 format(reset=True)))
1539 for f in sorted(failures):
1540 print failures[f]
1541 print
1542
1543
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06001544def show_stats(cur, prev):
1545 """Show API stats."""
1546
1547 stats = collections.defaultdict(int)
1548 for cur_clazz in cur.values():
1549 if not is_interesting(cur_clazz): continue
1550
1551 if cur_clazz.fullname not in prev:
1552 stats['new_classes'] += 1
1553 stats['new_ctors'] += len(cur_clazz.ctors)
1554 stats['new_methods'] += len(cur_clazz.methods)
1555 stats['new_fields'] += len(cur_clazz.fields)
1556 else:
1557 prev_clazz = prev[cur_clazz.fullname]
1558
1559 sigs = { i.ident: i for i in prev_clazz.ctors }
1560 ctors = len([ i for i in cur_clazz.ctors if i.ident not in sigs ])
1561 sigs = { i.ident: i for i in prev_clazz.methods }
1562 methods = len([ i for i in cur_clazz.methods if i.ident not in sigs ])
1563 sigs = { i.ident: i for i in prev_clazz.fields }
1564 fields = len([ i for i in cur_clazz.fields if i.ident not in sigs ])
1565
1566 if ctors + methods + fields > 0:
1567 stats['extend_classes'] += 1
1568 stats['extend_ctors'] += ctors
1569 stats['extend_methods'] += methods
1570 stats['extend_fields'] += fields
1571
1572 print "#", "".join([ k.ljust(20) for k in sorted(stats.keys()) ])
1573 print " ", "".join([ str(stats[k]).ljust(20) for k in sorted(stats.keys()) ])
1574
1575
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07001576if __name__ == "__main__":
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07001577 parser = argparse.ArgumentParser(description="Enforces common Android public API design \
1578 patterns. It ignores lint messages from a previous API level, if provided.")
1579 parser.add_argument("current.txt", type=argparse.FileType('r'), help="current.txt")
1580 parser.add_argument("previous.txt", nargs='?', type=argparse.FileType('r'), default=None,
1581 help="previous.txt")
1582 parser.add_argument("--no-color", action='store_const', const=True,
1583 help="Disable terminal colors")
1584 parser.add_argument("--allow-google", action='store_const', const=True,
1585 help="Allow references to Google")
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001586 parser.add_argument("--show-noticed", action='store_const', const=True,
1587 help="Show API changes noticed")
Jeff Sharkey8b141b92018-04-11 10:05:44 -06001588 parser.add_argument("--show-deprecations-at-birth", action='store_const', const=True,
1589 help="Show API deprecations at birth")
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06001590 parser.add_argument("--show-stats", action='store_const', const=True,
1591 help="Show API stats")
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07001592 args = vars(parser.parse_args())
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001593
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07001594 if args['no_color']:
1595 USE_COLOR = False
1596
1597 if args['allow_google']:
1598 ALLOW_GOOGLE = True
1599
1600 current_file = args['current.txt']
1601 previous_file = args['previous.txt']
1602
Jeff Sharkey8b141b92018-04-11 10:05:44 -06001603 if args['show_deprecations_at_birth']:
1604 with current_file as f:
1605 cur = _parse_stream(f)
1606 with previous_file as f:
1607 prev = _parse_stream(f)
1608 show_deprecations_at_birth(cur, prev)
1609 sys.exit()
1610
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06001611 if args['show_stats']:
1612 with current_file as f:
1613 cur = _parse_stream(f)
1614 with previous_file as f:
1615 prev = _parse_stream(f)
1616 show_stats(cur, prev)
1617 sys.exit()
1618
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07001619 with current_file as f:
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001620 cur_fail, cur_noticed = examine_stream(f)
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07001621 if not previous_file is None:
1622 with previous_file as f:
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001623 prev_fail, prev_noticed = examine_stream(f)
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001624
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07001625 # ignore errors from previous API level
1626 for p in prev_fail:
1627 if p in cur_fail:
1628 del cur_fail[p]
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001629
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001630 # ignore classes unchanged from previous API level
1631 for k, v in prev_noticed.iteritems():
1632 if k in cur_noticed and v == cur_noticed[k]:
1633 del cur_noticed[k]
1634
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001635 """
1636 # NOTE: disabled because of memory pressure
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07001637 # look for compatibility issues
1638 compat_fail = verify_compat(cur, prev)
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001639
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07001640 print "%s API compatibility issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
1641 for f in sorted(compat_fail):
1642 print compat_fail[f]
1643 print
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001644 """
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07001645
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001646 if args['show_noticed'] and len(cur_noticed) != 0:
1647 print "%s API changes noticed %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
1648 for f in sorted(cur_noticed.keys()):
1649 print f
1650 print
1651
Jason Monk53b2a732017-11-10 15:43:17 -05001652 if len(cur_fail) != 0:
1653 print "%s API style issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
1654 for f in sorted(cur_fail):
1655 print cur_fail[f]
1656 print
1657 sys.exit(77)