blob: acf1f1e9902e84dff22d940d613a46eed0cc126d [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
Adrian Roos1f1b6a82019-01-05 20:09:38 +010029import re, sys, collections, traceback, argparse, itertools
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():
Adrian Roosb787c182019-01-03 18:54:33 +010054 def __init__(self, clazz, line, raw, blame, sig_format = 1):
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
Adrian Roosb787c182019-01-03 18:54:33 +010060 if sig_format == 2:
61 V2LineParser(raw).parse_into_field(self)
62 elif sig_format == 1:
63 # drop generics for now; may need multiple passes
64 raw = re.sub("<[^<]+?>", "", raw)
65 raw = re.sub("<[^<]+?>", "", raw)
Jeff Sharkey40d67f42018-07-17 13:29:40 -060066
Adrian Roosb787c182019-01-03 18:54:33 +010067 raw = raw.split()
68 self.split = list(raw)
Jeff Sharkey8190f4882014-08-28 12:24:07 -070069
Adrian Roosb787c182019-01-03 18:54:33 +010070 for r in ["field", "volatile", "transient", "public", "protected", "static", "final", "deprecated"]:
71 while r in raw: raw.remove(r)
Jeff Sharkey8190f4882014-08-28 12:24:07 -070072
Adrian Roosb787c182019-01-03 18:54:33 +010073 # ignore annotations for now
74 raw = [ r for r in raw if not r.startswith("@") ]
Jeff Sharkey40d67f42018-07-17 13:29:40 -060075
Adrian Roosb787c182019-01-03 18:54:33 +010076 self.typ = raw[0]
77 self.name = raw[1].strip(";")
78 if len(raw) >= 4 and raw[2] == "=":
79 self.value = raw[3].strip(';"')
80 else:
81 self.value = None
82
83 self.ident = "-".join((self.typ, self.name, self.value or ""))
Jeff Sharkey037458a2014-09-04 15:46:20 -070084
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -070085 def __hash__(self):
86 return hash(self.raw)
87
Jeff Sharkey8190f4882014-08-28 12:24:07 -070088 def __repr__(self):
89 return self.raw
90
Jeff Sharkey8190f4882014-08-28 12:24:07 -070091class Method():
Adrian Roosb787c182019-01-03 18:54:33 +010092 def __init__(self, clazz, line, raw, blame, sig_format = 1):
Jeff Sharkey8190f4882014-08-28 12:24:07 -070093 self.clazz = clazz
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -070094 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -070095 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -070096 self.blame = blame
97
Adrian Roosb787c182019-01-03 18:54:33 +010098 if sig_format == 2:
99 V2LineParser(raw).parse_into_method(self)
100 elif sig_format == 1:
101 # drop generics for now; may need multiple passes
102 raw = re.sub("<[^<]+?>", "", raw)
103 raw = re.sub("<[^<]+?>", "", raw)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700104
Adrian Roosb787c182019-01-03 18:54:33 +0100105 # handle each clause differently
106 raw_prefix, raw_args, _, raw_throws = re.match(r"(.*?)\((.*?)\)( throws )?(.*?);$", raw).groups()
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600107
Adrian Roosb787c182019-01-03 18:54:33 +0100108 # parse prefixes
109 raw = re.split("[\s]+", raw_prefix)
110 for r in ["", ";"]:
111 while r in raw: raw.remove(r)
112 self.split = list(raw)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700113
Adrian Roosb787c182019-01-03 18:54:33 +0100114 for r in ["method", "public", "protected", "static", "final", "deprecated", "abstract", "default", "operator", "synchronized"]:
115 while r in raw: raw.remove(r)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700116
Adrian Roosb787c182019-01-03 18:54:33 +0100117 self.typ = raw[0]
118 self.name = raw[1]
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600119
Adrian Roosb787c182019-01-03 18:54:33 +0100120 # parse args
121 self.args = []
122 for arg in re.split(",\s*", raw_args):
123 arg = re.split("\s", arg)
124 # ignore annotations for now
125 arg = [ a for a in arg if not a.startswith("@") ]
126 if len(arg[0]) > 0:
127 self.args.append(arg[0])
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600128
Adrian Roosb787c182019-01-03 18:54:33 +0100129 # parse throws
130 self.throws = []
131 for throw in re.split(",\s*", raw_throws):
132 self.throws.append(throw)
133 else:
134 raise ValueError("Unknown signature format: " + sig_format)
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600135
Adrian Roosb787c182019-01-03 18:54:33 +0100136 self.ident = "-".join((self.typ, self.name, "-".join(self.args)))
137
138 def sig_matches(self, typ, name, args):
139 return typ == self.typ and name == self.name and args == self.args
Jeff Sharkey037458a2014-09-04 15:46:20 -0700140
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -0700141 def __hash__(self):
142 return hash(self.raw)
143
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700144 def __repr__(self):
145 return self.raw
146
147
148class Class():
Adrian Roosb787c182019-01-03 18:54:33 +0100149 def __init__(self, pkg, line, raw, blame, sig_format = 1):
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700150 self.pkg = pkg
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700151 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700152 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700153 self.blame = blame
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700154 self.ctors = []
155 self.fields = []
156 self.methods = []
157
Adrian Roosb787c182019-01-03 18:54:33 +0100158 if sig_format == 2:
159 V2LineParser(raw).parse_into_class(self)
160 elif sig_format == 1:
161 # drop generics for now; may need multiple passes
162 raw = re.sub("<[^<]+?>", "", raw)
163 raw = re.sub("<[^<]+?>", "", raw)
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600164
Adrian Roosb787c182019-01-03 18:54:33 +0100165 raw = raw.split()
166 self.split = list(raw)
167 if "class" in raw:
168 self.fullname = raw[raw.index("class")+1]
169 elif "interface" in raw:
170 self.fullname = raw[raw.index("interface")+1]
171 elif "@interface" in raw:
172 self.fullname = raw[raw.index("@interface")+1]
173 else:
174 raise ValueError("Funky class type %s" % (self.raw))
Jeff Sharkey037458a2014-09-04 15:46:20 -0700175
Adrian Roosb787c182019-01-03 18:54:33 +0100176 if "extends" in raw:
177 self.extends = raw[raw.index("extends")+1]
178 else:
179 self.extends = None
180
181 if "implements" in raw:
182 self.implements = raw[raw.index("implements")+1]
183 else:
184 self.implements = None
Jeff Sharkey037458a2014-09-04 15:46:20 -0700185 else:
Adrian Roosb787c182019-01-03 18:54:33 +0100186 raise ValueError("Unknown signature format: " + sig_format)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700187
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700188 self.fullname = self.pkg.name + "." + self.fullname
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800189 self.fullname_path = self.fullname.split(".")
190
Adrian Roosb787c182019-01-03 18:54:33 +0100191 if self.extends is not None:
192 self.extends_path = self.extends.split(".")
193 else:
194 self.extends_path = []
195
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700196 self.name = self.fullname[self.fullname.rindex(".")+1:]
197
Adrian Roos6eb57b02018-12-13 22:08:29 +0100198 def merge_from(self, other):
199 self.ctors.extend(other.ctors)
200 self.fields.extend(other.fields)
201 self.methods.extend(other.methods)
202
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -0700203 def __hash__(self):
204 return hash((self.raw, tuple(self.ctors), tuple(self.fields), tuple(self.methods)))
205
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700206 def __repr__(self):
207 return self.raw
208
209
210class Package():
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700211 def __init__(self, line, raw, blame):
212 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700213 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700214 self.blame = blame
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700215
216 raw = raw.split()
217 self.name = raw[raw.index("package")+1]
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800218 self.name_path = self.name.split(".")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700219
220 def __repr__(self):
221 return self.raw
222
Adrian Roose5eeae72019-01-04 20:10:06 +0100223class V2Tokenizer(object):
224 __slots__ = ["raw"]
225
Adrian Roos5cdfb692019-01-05 22:04:55 +0100226 DELIMITER = re.compile(r'\s+|[()@<>;,={}/"!?]|\[\]|\.\.\.')
Adrian Roosb787c182019-01-03 18:54:33 +0100227 STRING_SPECIAL = re.compile(r'["\\]')
228
229 def __init__(self, raw):
230 self.raw = raw
Adrian Roosb787c182019-01-03 18:54:33 +0100231
Adrian Roose5eeae72019-01-04 20:10:06 +0100232 def tokenize(self):
233 tokens = []
234 current = 0
235 raw = self.raw
236 length = len(raw)
Adrian Roosb787c182019-01-03 18:54:33 +0100237
Adrian Roose5eeae72019-01-04 20:10:06 +0100238 while current < length:
239 while current < length:
240 start = current
241 match = V2Tokenizer.DELIMITER.search(raw, start)
242 if match is not None:
243 match_start = match.start()
244 if match_start == current:
245 end = match.end()
246 else:
247 end = match_start
248 else:
249 end = length
Adrian Roosb787c182019-01-03 18:54:33 +0100250
Adrian Roose5eeae72019-01-04 20:10:06 +0100251 token = raw[start:end]
252 current = end
Adrian Roosb787c182019-01-03 18:54:33 +0100253
Adrian Roose5eeae72019-01-04 20:10:06 +0100254 if token == "" or token[0] == " ":
255 continue
256 else:
257 break
Adrian Roosb787c182019-01-03 18:54:33 +0100258
Adrian Roose5eeae72019-01-04 20:10:06 +0100259 if token == "@":
260 if raw[start:start+11] == "@interface ":
261 current = start + 11
262 tokens.append("@interface")
263 continue
264 elif token == '/':
265 if raw[start:start+2] == "//":
266 current = length
267 continue
268 elif token == '"':
269 current, string_token = self.tokenize_string(raw, length, current)
270 tokens.append(token + string_token)
271 continue
Adrian Roosb787c182019-01-03 18:54:33 +0100272
Adrian Roose5eeae72019-01-04 20:10:06 +0100273 tokens.append(token)
Adrian Roosb787c182019-01-03 18:54:33 +0100274
Adrian Roose5eeae72019-01-04 20:10:06 +0100275 return tokens
Adrian Roosb787c182019-01-03 18:54:33 +0100276
Adrian Roose5eeae72019-01-04 20:10:06 +0100277 def tokenize_string(self, raw, length, current):
278 start = current
279 end = length
Adrian Roosb787c182019-01-03 18:54:33 +0100280 while start < end:
Adrian Roose5eeae72019-01-04 20:10:06 +0100281 match = V2Tokenizer.STRING_SPECIAL.search(raw, start)
Adrian Roosb787c182019-01-03 18:54:33 +0100282 if match:
283 if match.group() == '"':
284 end = match.end()
285 break
286 elif match.group() == '\\':
287 # ignore whatever is after the slash
288 start += 2
289 else:
290 raise ValueError("Unexpected match: `%s`" % (match.group()))
291 else:
Adrian Roose5eeae72019-01-04 20:10:06 +0100292 raise ValueError("Unexpected EOF tokenizing string: `%s`" % (raw[current - 1:],))
Adrian Roosb787c182019-01-03 18:54:33 +0100293
Adrian Roose5eeae72019-01-04 20:10:06 +0100294 token = raw[current:end]
295 return end, token
Adrian Roosb787c182019-01-03 18:54:33 +0100296
Adrian Roose5eeae72019-01-04 20:10:06 +0100297class V2LineParser(object):
298 __slots__ = ["tokenized", "current", "len"]
299
Adrian Roosb787c182019-01-03 18:54:33 +0100300 MODIFIERS = set("public protected internal private abstract default static final transient volatile synchronized".split())
301 JAVA_LANG_TYPES = set("AbstractMethodError AbstractStringBuilder Appendable ArithmeticException ArrayIndexOutOfBoundsException ArrayStoreException AssertionError AutoCloseable Boolean BootstrapMethodError Byte Character CharSequence Class ClassCastException ClassCircularityError ClassFormatError ClassLoader ClassNotFoundException Cloneable CloneNotSupportedException Comparable Compiler Deprecated Double Enum EnumConstantNotPresentException Error Exception ExceptionInInitializerError Float FunctionalInterface IllegalAccessError IllegalAccessException IllegalArgumentException IllegalMonitorStateException IllegalStateException IllegalThreadStateException IncompatibleClassChangeError IndexOutOfBoundsException InheritableThreadLocal InstantiationError InstantiationException Integer InternalError InterruptedException Iterable LinkageError Long Math NegativeArraySizeException NoClassDefFoundError NoSuchFieldError NoSuchFieldException NoSuchMethodError NoSuchMethodException NullPointerException Number NumberFormatException Object OutOfMemoryError Override Package package-info.java Process ProcessBuilder ProcessEnvironment ProcessImpl Readable ReflectiveOperationException Runnable Runtime RuntimeException RuntimePermission SafeVarargs SecurityException SecurityManager Short StackOverflowError StackTraceElement StrictMath String StringBuffer StringBuilder StringIndexOutOfBoundsException SuppressWarnings System Thread ThreadDeath ThreadGroup ThreadLocal Throwable TypeNotPresentException UNIXProcess UnknownError UnsatisfiedLinkError UnsupportedClassVersionError UnsupportedOperationException VerifyError VirtualMachineError Void".split())
302
303 def __init__(self, raw):
Adrian Roose5eeae72019-01-04 20:10:06 +0100304 self.tokenized = V2Tokenizer(raw).tokenize()
Adrian Roosb787c182019-01-03 18:54:33 +0100305 self.current = 0
Adrian Roose5eeae72019-01-04 20:10:06 +0100306 self.len = len(self.tokenized)
Adrian Roosb787c182019-01-03 18:54:33 +0100307
308 def parse_into_method(self, method):
309 method.split = []
310 kind = self.parse_one_of("ctor", "method")
311 method.split.append(kind)
312 annotations = self.parse_annotations()
313 method.split.extend(self.parse_modifiers())
314 self.parse_matching_paren("<", ">")
315 if "@Deprecated" in annotations:
316 method.split.append("deprecated")
317 if kind == "ctor":
318 method.typ = "ctor"
319 else:
320 method.typ = self.parse_type()
321 method.split.append(method.typ)
322 method.name = self.parse_name()
323 method.split.append(method.name)
324 self.parse_token("(")
325 method.args = self.parse_args()
326 self.parse_token(")")
327 method.throws = self.parse_throws()
328 if "@interface" in method.clazz.split:
329 self.parse_annotation_default()
330 self.parse_token(";")
331 self.parse_eof()
332
333 def parse_into_class(self, clazz):
334 clazz.split = []
335 annotations = self.parse_annotations()
336 if "@Deprecated" in annotations:
337 clazz.split.append("deprecated")
338 clazz.split.extend(self.parse_modifiers())
339 kind = self.parse_one_of("class", "interface", "@interface", "enum")
340 if kind == "enum":
341 # enums are implicitly final
342 clazz.split.append("final")
343 clazz.split.append(kind)
344 clazz.fullname = self.parse_name()
345 self.parse_matching_paren("<", ">")
346 extends = self.parse_extends()
347 clazz.extends = extends[0] if extends else None
348 implements = self.parse_implements()
349 clazz.implements = implements[0] if implements else None
350 # The checks assume that interfaces are always found in implements, which isn't true for
351 # subinterfaces.
352 if not implements and "interface" in clazz.split:
353 clazz.implements = clazz.extends
354 self.parse_token("{")
355 self.parse_eof()
356
357 def parse_into_field(self, field):
358 kind = self.parse_token("field")
359 field.split = [kind]
360 annotations = self.parse_annotations()
361 if "@Deprecated" in annotations:
362 field.split.append("deprecated")
363 field.split.extend(self.parse_modifiers())
364 field.typ = self.parse_type()
365 field.split.append(field.typ)
366 field.name = self.parse_name()
367 field.split.append(field.name)
368 if self.parse_if("="):
369 field.value = self.parse_value_stripped()
370 else:
371 field.value = None
372
373 self.parse_token(";")
374 self.parse_eof()
375
376 def lookahead(self):
377 return self.tokenized[self.current]
378
379 def parse_one_of(self, *options):
380 found = self.lookahead()
381 if found not in options:
382 raise ValueError("Parsing failed, expected one of `%s` but found `%s` in %s" % (options, found, repr(self.tokenized)))
383 return self.parse_token()
384
385 def parse_token(self, tok = None):
386 found = self.lookahead()
387 if tok is not None and found != tok:
388 raise ValueError("Parsing failed, expected `%s` but found `%s` in %s" % (tok, found, repr(self.tokenized)))
389 self.current += 1
390 return found
391
392 def eof(self):
Adrian Roose5eeae72019-01-04 20:10:06 +0100393 return self.current == self.len
Adrian Roosb787c182019-01-03 18:54:33 +0100394
395 def parse_eof(self):
396 if not self.eof():
397 raise ValueError("Parsing failed, expected EOF, but %s has not been parsed in %s" % (self.tokenized[self.current:], self.tokenized))
398
399 def parse_if(self, tok):
400 if not self.eof() and self.lookahead() == tok:
401 self.parse_token()
402 return True
403 return False
404
405 def parse_annotations(self):
406 ret = []
407 while self.lookahead() == "@":
408 ret.append(self.parse_annotation())
409 return ret
410
411 def parse_annotation(self):
412 ret = self.parse_token("@") + self.parse_token()
413 self.parse_matching_paren("(", ")")
414 return ret
415
416 def parse_matching_paren(self, open, close):
417 start = self.current
418 if not self.parse_if(open):
419 return
420 length = len(self.tokenized)
421 count = 1
422 while count > 0:
423 if self.current == length:
424 raise ValueError("Unexpected EOF looking for closing paren: `%s`" % (self.tokenized[start:],))
425 t = self.parse_token()
426 if t == open:
427 count += 1
428 elif t == close:
429 count -= 1
430 return self.tokenized[start:self.current]
431
432 def parse_modifiers(self):
433 ret = []
434 while self.lookahead() in V2LineParser.MODIFIERS:
435 ret.append(self.parse_token())
436 return ret
437
Adrian Roos5cdfb692019-01-05 22:04:55 +0100438 def parse_kotlin_nullability(self):
439 t = self.lookahead()
440 if t == "?" or t == "!":
441 return self.parse_token()
442 return None
443
Adrian Roosb787c182019-01-03 18:54:33 +0100444 def parse_type(self):
445 type = self.parse_token()
446 if type in V2LineParser.JAVA_LANG_TYPES:
447 type = "java.lang." + type
448 self.parse_matching_paren("<", ">")
Adrian Roos5cdfb692019-01-05 22:04:55 +0100449 while True:
450 t = self.lookahead()
451 if t == "[]":
452 type += self.parse_token()
453 elif self.parse_kotlin_nullability() is not None:
454 pass # discard nullability for now
455 else:
456 break
Adrian Roosb787c182019-01-03 18:54:33 +0100457 return type
458
459 def parse_arg_type(self):
460 type = self.parse_type()
461 if self.parse_if("..."):
462 type += "..."
Adrian Roos5cdfb692019-01-05 22:04:55 +0100463 self.parse_kotlin_nullability() # discard nullability for now
Adrian Roosb787c182019-01-03 18:54:33 +0100464 return type
465
466 def parse_name(self):
467 return self.parse_token()
468
469 def parse_args(self):
470 args = []
471 if self.lookahead() == ")":
472 return args
473
474 while True:
475 args.append(self.parse_arg())
476 if self.lookahead() == ")":
477 return args
478 self.parse_token(",")
479
480 def parse_arg(self):
481 self.parse_annotations()
Adrian Roos5cdfb692019-01-05 22:04:55 +0100482 type = self.parse_arg_type()
483 l = self.lookahead()
484 if l != "," and l != ")":
485 self.parse_token() # kotlin argument name
486 if self.parse_if('='): # kotlin default value
487 (self.parse_matching_paren('(', ')') or
488 self.parse_matching_paren('{', '}') or
489 self.parse_token() and self.parse_matching_paren('(', ')'))
490 return type
Adrian Roosb787c182019-01-03 18:54:33 +0100491
492 def parse_throws(self):
493 ret = []
494 if self.parse_if("throws"):
495 ret.append(self.parse_type())
496 while self.parse_if(","):
497 ret.append(self.parse_type())
498 return ret
499
500 def parse_extends(self):
501 if self.parse_if("extends"):
502 return self.parse_space_delimited_type_list()
503 return []
504
505 def parse_implements(self):
506 if self.parse_if("implements"):
507 return self.parse_space_delimited_type_list()
508 return []
509
510 def parse_space_delimited_type_list(self, terminals = ["implements", "{"]):
511 types = []
512 while True:
513 types.append(self.parse_type())
514 if self.lookahead() in terminals:
515 return types
516
517 def parse_annotation_default(self):
518 if self.parse_if("default"):
519 self.parse_value()
520
521 def parse_value(self):
522 if self.lookahead() == "{":
523 return " ".join(self.parse_matching_paren("{", "}"))
524 elif self.lookahead() == "(":
525 return " ".join(self.parse_matching_paren("(", ")"))
526 else:
527 return self.parse_token()
528
529 def parse_value_stripped(self):
530 value = self.parse_value()
531 if value[0] == '"':
532 return value[1:-1]
533 return value
534
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700535
Adrian Roos038a0292018-12-19 17:11:21 +0100536def _parse_stream(f, clazz_cb=None, base_f=None, out_classes_with_base=None,
537 in_classes_with_base=[]):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700538 api = {}
Adrian Roos038a0292018-12-19 17:11:21 +0100539 in_classes_with_base = _retry_iterator(in_classes_with_base)
Adrian Roos6eb57b02018-12-13 22:08:29 +0100540
541 if base_f:
Adrian Roos038a0292018-12-19 17:11:21 +0100542 base_classes = _retry_iterator(_parse_stream_to_generator(base_f))
Adrian Roos6eb57b02018-12-13 22:08:29 +0100543 else:
544 base_classes = []
545
Adrian Roos038a0292018-12-19 17:11:21 +0100546 def handle_class(clazz):
Adrian Roos6eb57b02018-12-13 22:08:29 +0100547 if clazz_cb:
548 clazz_cb(clazz)
549 else: # In callback mode, don't keep track of the full API
550 api[clazz.fullname] = clazz
551
Adrian Roos038a0292018-12-19 17:11:21 +0100552 def handle_missed_classes_with_base(clazz):
553 for c in _yield_until_matching_class(in_classes_with_base, clazz):
554 base_class = _skip_to_matching_class(base_classes, c)
555 if base_class:
556 handle_class(base_class)
557
558 for clazz in _parse_stream_to_generator(f):
559 # Before looking at clazz, let's see if there's some classes that were not present, but
560 # may have an entry in the base stream.
561 handle_missed_classes_with_base(clazz)
562
563 base_class = _skip_to_matching_class(base_classes, clazz)
564 if base_class:
565 clazz.merge_from(base_class)
566 if out_classes_with_base is not None:
567 out_classes_with_base.append(clazz)
568 handle_class(clazz)
569
570 handle_missed_classes_with_base(None)
571
Adrian Roos6eb57b02018-12-13 22:08:29 +0100572 return api
573
574def _parse_stream_to_generator(f):
575 line = 0
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700576 pkg = None
577 clazz = None
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700578 blame = None
Adrian Roosb787c182019-01-03 18:54:33 +0100579 sig_format = 1
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700580
581 re_blame = re.compile("^([a-z0-9]{7,}) \(<([^>]+)>.+?\) (.+?)$")
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800582 for raw in f:
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700583 line += 1
584 raw = raw.rstrip()
585 match = re_blame.match(raw)
586 if match is not None:
587 blame = match.groups()[0:2]
588 raw = match.groups()[2]
589 else:
590 blame = None
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700591
Adrian Roosb787c182019-01-03 18:54:33 +0100592 if line == 1 and raw == "// Signature format: 2.0":
593 sig_format = 2
594 elif raw.startswith("package"):
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700595 pkg = Package(line, raw, blame)
596 elif raw.startswith(" ") and raw.endswith("{"):
Adrian Roosb787c182019-01-03 18:54:33 +0100597 clazz = Class(pkg, line, raw, blame, sig_format=sig_format)
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700598 elif raw.startswith(" ctor"):
Adrian Roosb787c182019-01-03 18:54:33 +0100599 clazz.ctors.append(Method(clazz, line, raw, blame, sig_format=sig_format))
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700600 elif raw.startswith(" method"):
Adrian Roosb787c182019-01-03 18:54:33 +0100601 clazz.methods.append(Method(clazz, line, raw, blame, sig_format=sig_format))
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700602 elif raw.startswith(" field"):
Adrian Roosb787c182019-01-03 18:54:33 +0100603 clazz.fields.append(Field(clazz, line, raw, blame, sig_format=sig_format))
Adrian Roos6eb57b02018-12-13 22:08:29 +0100604 elif raw.startswith(" }") and clazz:
Adrian Roos038a0292018-12-19 17:11:21 +0100605 yield clazz
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800606
Adrian Roos5ed42b62018-12-19 17:10:22 +0100607def _retry_iterator(it):
608 """Wraps an iterator, such that calling send(True) on it will redeliver the same element"""
609 for e in it:
610 while True:
611 retry = yield e
612 if not retry:
613 break
614 # send() was called, asking us to redeliver clazz on next(). Still need to yield
615 # a dummy value to the send() first though.
616 if (yield "Returning clazz on next()"):
617 raise TypeError("send() must be followed by next(), not send()")
618
Adrian Roos038a0292018-12-19 17:11:21 +0100619def _skip_to_matching_class(classes, needle):
620 """Takes a classes iterator and consumes entries until it returns the class we're looking for
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700621
Adrian Roos6eb57b02018-12-13 22:08:29 +0100622 This relies on classes being sorted by package and class name."""
623
624 for clazz in classes:
625 if clazz.pkg.name < needle.pkg.name:
626 # We haven't reached the right package yet
627 continue
Adrian Roos5ed42b62018-12-19 17:10:22 +0100628 if clazz.pkg.name == needle.pkg.name and clazz.fullname < needle.fullname:
629 # We're in the right package, but not the right class yet
Adrian Roos6eb57b02018-12-13 22:08:29 +0100630 continue
631 if clazz.fullname == needle.fullname:
632 return clazz
633 # We ran past the right class. Send it back into the generator, then report failure.
634 classes.send(clazz)
635 return None
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700636
Adrian Roos038a0292018-12-19 17:11:21 +0100637def _yield_until_matching_class(classes, needle):
638 """Takes a class iterator and yields entries it until it reaches the class we're looking for.
639
640 This relies on classes being sorted by package and class name."""
641
642 for clazz in classes:
643 if needle is None:
644 yield clazz
645 elif clazz.pkg.name < needle.pkg.name:
646 # We haven't reached the right package yet
647 yield clazz
648 elif clazz.pkg.name == needle.pkg.name and clazz.fullname < needle.fullname:
649 # We're in the right package, but not the right class yet
650 yield clazz
651 elif clazz.fullname == needle.fullname:
652 # Class found, abort.
653 return
654 else:
655 # We ran past the right class. Send it back into the iterator, then abort.
656 classes.send(clazz)
657 return
658
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700659class Failure():
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800660 def __init__(self, sig, clazz, detail, error, rule, msg):
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700661 self.sig = sig
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700662 self.error = error
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800663 self.rule = rule
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700664 self.msg = msg
665
666 if error:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800667 self.head = "Error %s" % (rule) if rule else "Error"
668 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 -0700669 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800670 self.head = "Warning %s" % (rule) if rule else "Warning"
671 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 -0700672
673 self.line = clazz.line
674 blame = clazz.blame
675 if detail is not None:
676 dump += "\n in " + repr(detail)
677 self.line = detail.line
678 blame = detail.blame
679 dump += "\n in " + repr(clazz)
680 dump += "\n in " + repr(clazz.pkg)
681 dump += "\n at line " + repr(self.line)
682 if blame is not None:
683 dump += "\n last modified by %s in %s" % (blame[1], blame[0])
684
685 self.dump = dump
686
687 def __repr__(self):
688 return self.dump
689
690
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700691failures = {}
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700692
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800693def _fail(clazz, detail, error, rule, msg):
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700694 """Records an API failure to be processed later."""
695 global failures
696
Adrian Roosb787c182019-01-03 18:54:33 +0100697 sig = "%s-%s-%s" % (clazz.fullname, detail.ident if detail else None, msg)
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700698 sig = sig.replace(" deprecated ", " ")
699
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800700 failures[sig] = Failure(sig, clazz, detail, error, rule, msg)
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700701
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700702
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800703def warn(clazz, detail, rule, msg):
704 _fail(clazz, detail, False, rule, msg)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700705
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800706def error(clazz, detail, rule, msg):
707 _fail(clazz, detail, True, rule, msg)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700708
709
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -0700710noticed = {}
711
712def notice(clazz):
713 global noticed
714
715 noticed[clazz.fullname] = hash(clazz)
716
717
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700718def verify_constants(clazz):
719 """All static final constants must be FOO_NAME style."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700720 if re.match("android\.R\.[a-z]+", clazz.fullname): return
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600721 if clazz.fullname.startswith("android.os.Build"): return
722 if clazz.fullname == "android.system.OsConstants": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700723
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600724 req = ["java.lang.String","byte","short","int","long","float","double","boolean","char"]
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700725 for f in clazz.fields:
726 if "static" in f.split and "final" in f.split:
727 if re.match("[A-Z0-9_]+", f.name) is None:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800728 error(clazz, f, "C2", "Constant field names must be FOO_NAME")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600729 if f.typ != "java.lang.String":
Jeff Sharkey331279b2016-02-29 16:02:02 -0700730 if f.name.startswith("MIN_") or f.name.startswith("MAX_"):
731 warn(clazz, f, "C8", "If min/max could change in future, make them dynamic methods")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600732 if f.typ in req and f.value is None:
733 error(clazz, f, None, "All constants must be defined at compile time")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700734
735
736def verify_enums(clazz):
737 """Enums are bad, mmkay?"""
Adrian Roosb787c182019-01-03 18:54:33 +0100738 if clazz.extends == "java.lang.Enum" or "enum" in clazz.split:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800739 error(clazz, None, "F5", "Enums are not allowed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700740
741
742def verify_class_names(clazz):
743 """Try catching malformed class names like myMtp or MTPUser."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700744 if clazz.fullname.startswith("android.opengl"): return
745 if clazz.fullname.startswith("android.renderscript"): return
746 if re.match("android\.R\.[a-z]+", clazz.fullname): return
747
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700748 if re.search("[A-Z]{2,}", clazz.name) is not None:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800749 warn(clazz, None, "S1", "Class names with acronyms should be Mtp not MTP")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700750 if re.match("[^A-Z]", clazz.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800751 error(clazz, None, "S1", "Class must start with uppercase char")
Jeff Sharkeyee21f692018-02-16 13:29:29 -0700752 if clazz.name.endswith("Impl"):
753 error(clazz, None, None, "Don't expose your implementation details")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700754
755
756def verify_method_names(clazz):
757 """Try catching malformed method names, like Foo() or getMTU()."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700758 if clazz.fullname.startswith("android.opengl"): return
759 if clazz.fullname.startswith("android.renderscript"): return
760 if clazz.fullname == "android.system.OsConstants": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700761
762 for m in clazz.methods:
763 if re.search("[A-Z]{2,}", m.name) is not None:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800764 warn(clazz, m, "S1", "Method names with acronyms should be getMtu() instead of getMTU()")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700765 if re.match("[^a-z]", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800766 error(clazz, m, "S1", "Method name must start with lowercase char")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700767
768
769def verify_callbacks(clazz):
770 """Verify Callback classes.
771 All callback classes must be abstract.
772 All methods must follow onFoo() naming style."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700773 if clazz.fullname == "android.speech.tts.SynthesisCallback": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700774
775 if clazz.name.endswith("Callbacks"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800776 error(clazz, None, "L1", "Callback class names should be singular")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700777 if clazz.name.endswith("Observer"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800778 warn(clazz, None, "L1", "Class should be named FooCallback")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700779
780 if clazz.name.endswith("Callback"):
781 if "interface" in clazz.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800782 error(clazz, None, "CL3", "Callbacks must be abstract class to enable extension in future API levels")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700783
784 for m in clazz.methods:
785 if not re.match("on[A-Z][a-z]*", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800786 error(clazz, m, "L1", "Callback method names must be onFoo() style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700787
788
789def verify_listeners(clazz):
790 """Verify Listener classes.
791 All Listener classes must be interface.
792 All methods must follow onFoo() naming style.
793 If only a single method, it must match class name:
794 interface OnFooListener { void onFoo() }"""
795
796 if clazz.name.endswith("Listener"):
Adrian Roosb787c182019-01-03 18:54:33 +0100797 if "abstract" in clazz.split and "class" in clazz.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800798 error(clazz, None, "L1", "Listeners should be an interface, or otherwise renamed Callback")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700799
800 for m in clazz.methods:
801 if not re.match("on[A-Z][a-z]*", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800802 error(clazz, m, "L1", "Listener method names must be onFoo() style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700803
804 if len(clazz.methods) == 1 and clazz.name.startswith("On"):
805 m = clazz.methods[0]
806 if (m.name + "Listener").lower() != clazz.name.lower():
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800807 error(clazz, m, "L1", "Single listener method name must match class name")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700808
809
810def verify_actions(clazz):
811 """Verify intent actions.
812 All action names must be named ACTION_FOO.
813 All action values must be scoped by package and match name:
814 package android.foo {
815 String ACTION_BAR = "android.foo.action.BAR";
816 }"""
817 for f in clazz.fields:
818 if f.value is None: continue
819 if f.name.startswith("EXTRA_"): continue
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700820 if f.name == "SERVICE_INTERFACE" or f.name == "PROVIDER_INTERFACE": continue
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600821 if "INTERACTION" in f.name: continue
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700822
823 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
824 if "_ACTION" in f.name or "ACTION_" in f.name or ".action." in f.value.lower():
825 if not f.name.startswith("ACTION_"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800826 error(clazz, f, "C3", "Intent action constant name must be ACTION_FOO")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700827 else:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700828 if clazz.fullname == "android.content.Intent":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700829 prefix = "android.intent.action"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700830 elif clazz.fullname == "android.provider.Settings":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700831 prefix = "android.settings"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700832 elif clazz.fullname == "android.app.admin.DevicePolicyManager" or clazz.fullname == "android.app.admin.DeviceAdminReceiver":
833 prefix = "android.app.action"
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700834 else:
835 prefix = clazz.pkg.name + ".action"
836 expected = prefix + "." + f.name[7:]
837 if f.value != expected:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700838 error(clazz, f, "C4", "Inconsistent action value; expected '%s'" % (expected))
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700839
840
841def verify_extras(clazz):
842 """Verify intent extras.
843 All extra names must be named EXTRA_FOO.
844 All extra values must be scoped by package and match name:
845 package android.foo {
846 String EXTRA_BAR = "android.foo.extra.BAR";
847 }"""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700848 if clazz.fullname == "android.app.Notification": return
849 if clazz.fullname == "android.appwidget.AppWidgetManager": return
850
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700851 for f in clazz.fields:
852 if f.value is None: continue
853 if f.name.startswith("ACTION_"): continue
854
855 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
856 if "_EXTRA" in f.name or "EXTRA_" in f.name or ".extra" in f.value.lower():
857 if not f.name.startswith("EXTRA_"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800858 error(clazz, f, "C3", "Intent extra must be EXTRA_FOO")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700859 else:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700860 if clazz.pkg.name == "android.content" and clazz.name == "Intent":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700861 prefix = "android.intent.extra"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700862 elif clazz.pkg.name == "android.app.admin":
863 prefix = "android.app.extra"
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700864 else:
865 prefix = clazz.pkg.name + ".extra"
866 expected = prefix + "." + f.name[6:]
867 if f.value != expected:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700868 error(clazz, f, "C4", "Inconsistent extra value; expected '%s'" % (expected))
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700869
870
871def verify_equals(clazz):
872 """Verify that equals() and hashCode() must be overridden together."""
Jeff Sharkey40d623e2016-12-21 13:46:33 -0700873 eq = False
874 hc = False
875 for m in clazz.methods:
Adrian Roosb787c182019-01-03 18:54:33 +0100876 if "static" in m.split: continue
877 if m.sig_matches("boolean", "equals", ["java.lang.Object"]): eq = True
878 if m.sig_matches("int", "hashCode", []): hc = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700879 if eq != hc:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800880 error(clazz, None, "M8", "Must override both equals and hashCode; missing one")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700881
882
883def verify_parcelable(clazz):
884 """Verify that Parcelable objects aren't hiding required bits."""
Adrian Roosb787c182019-01-03 18:54:33 +0100885 if clazz.implements == "android.os.Parcelable":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700886 creator = [ i for i in clazz.fields if i.name == "CREATOR" ]
887 write = [ i for i in clazz.methods if i.name == "writeToParcel" ]
888 describe = [ i for i in clazz.methods if i.name == "describeContents" ]
889
890 if len(creator) == 0 or len(write) == 0 or len(describe) == 0:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800891 error(clazz, None, "FW3", "Parcelable requires CREATOR, writeToParcel, and describeContents; missing one")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700892
Adrian Roosb787c182019-01-03 18:54:33 +0100893 if "final" not in clazz.split:
Jeff Sharkey331279b2016-02-29 16:02:02 -0700894 error(clazz, None, "FW8", "Parcelable classes must be final")
895
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700896 for c in clazz.ctors:
897 if c.args == ["android.os.Parcel"]:
898 error(clazz, c, "FW3", "Parcelable inflation is exposed through CREATOR, not raw constructors")
899
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700900
901def verify_protected(clazz):
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800902 """Verify that no protected methods or fields are allowed."""
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700903 for m in clazz.methods:
Jeff Sharkeyeff9e222018-09-27 16:29:25 -0600904 if m.name == "finalize": continue
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700905 if "protected" in m.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800906 error(clazz, m, "M7", "Protected methods not allowed; must be public")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700907 for f in clazz.fields:
908 if "protected" in f.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800909 error(clazz, f, "M7", "Protected fields not allowed; must be public")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700910
911
912def verify_fields(clazz):
913 """Verify that all exposed fields are final.
914 Exposed fields must follow myName style.
915 Catch internal mFoo objects being exposed."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700916
917 IGNORE_BARE_FIELDS = [
918 "android.app.ActivityManager.RecentTaskInfo",
919 "android.app.Notification",
920 "android.content.pm.ActivityInfo",
921 "android.content.pm.ApplicationInfo",
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600922 "android.content.pm.ComponentInfo",
923 "android.content.pm.ResolveInfo",
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700924 "android.content.pm.FeatureGroupInfo",
925 "android.content.pm.InstrumentationInfo",
926 "android.content.pm.PackageInfo",
927 "android.content.pm.PackageItemInfo",
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600928 "android.content.res.Configuration",
929 "android.graphics.BitmapFactory.Options",
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700930 "android.os.Message",
931 "android.system.StructPollfd",
932 ]
933
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700934 for f in clazz.fields:
935 if not "final" in f.split:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700936 if clazz.fullname in IGNORE_BARE_FIELDS:
937 pass
938 elif clazz.fullname.endswith("LayoutParams"):
939 pass
940 elif clazz.fullname.startswith("android.util.Mutable"):
941 pass
942 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800943 error(clazz, f, "F2", "Bare fields must be marked final, or add accessors if mutable")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700944
945 if not "static" in f.split:
946 if not re.match("[a-z]([a-zA-Z]+)?", f.name):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800947 error(clazz, f, "S1", "Non-static fields must be named using myField style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700948
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700949 if re.match("[ms][A-Z]", f.name):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800950 error(clazz, f, "F1", "Internal objects must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700951
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700952 if re.match("[A-Z_]+", f.name):
953 if "static" not in f.split or "final" not in f.split:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800954 error(clazz, f, "C2", "Constants must be marked static final")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700955
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700956
957def verify_register(clazz):
958 """Verify parity of registration methods.
959 Callback objects use register/unregister methods.
960 Listener objects use add/remove methods."""
961 methods = [ m.name for m in clazz.methods ]
962 for m in clazz.methods:
963 if "Callback" in m.raw:
964 if m.name.startswith("register"):
965 other = "unregister" + m.name[8:]
966 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800967 error(clazz, m, "L2", "Missing unregister method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700968 if m.name.startswith("unregister"):
969 other = "register" + m.name[10:]
970 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800971 error(clazz, m, "L2", "Missing register method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700972
973 if m.name.startswith("add") or m.name.startswith("remove"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800974 error(clazz, m, "L3", "Callback methods should be named register/unregister")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700975
976 if "Listener" in m.raw:
977 if m.name.startswith("add"):
978 other = "remove" + m.name[3:]
979 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800980 error(clazz, m, "L2", "Missing remove method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700981 if m.name.startswith("remove") and not m.name.startswith("removeAll"):
982 other = "add" + m.name[6:]
983 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800984 error(clazz, m, "L2", "Missing add method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700985
986 if m.name.startswith("register") or m.name.startswith("unregister"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800987 error(clazz, m, "L3", "Listener methods should be named add/remove")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700988
989
990def verify_sync(clazz):
991 """Verify synchronized methods aren't exposed."""
992 for m in clazz.methods:
993 if "synchronized" in m.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800994 error(clazz, m, "M5", "Internal locks must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700995
996
997def verify_intent_builder(clazz):
998 """Verify that Intent builders are createFooIntent() style."""
999 if clazz.name == "Intent": return
1000
1001 for m in clazz.methods:
1002 if m.typ == "android.content.Intent":
1003 if m.name.startswith("create") and m.name.endswith("Intent"):
1004 pass
1005 else:
Adam Powell539ea122015-04-10 13:01:37 -07001006 warn(clazz, m, "FW1", "Methods creating an Intent should be named createFooIntent()")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001007
1008
1009def verify_helper_classes(clazz):
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001010 """Verify that helper classes are named consistently with what they extend.
1011 All developer extendable methods should be named onFoo()."""
1012 test_methods = False
Adrian Roosb787c182019-01-03 18:54:33 +01001013 if clazz.extends == "android.app.Service":
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001014 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001015 if not clazz.name.endswith("Service"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001016 error(clazz, None, "CL4", "Inconsistent class name; should be FooService")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001017
1018 found = False
1019 for f in clazz.fields:
1020 if f.name == "SERVICE_INTERFACE":
1021 found = True
1022 if f.value != clazz.fullname:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001023 error(clazz, f, "C4", "Inconsistent interface constant; expected '%s'" % (clazz.fullname))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001024
Adrian Roosb787c182019-01-03 18:54:33 +01001025 if clazz.extends == "android.content.ContentProvider":
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001026 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001027 if not clazz.name.endswith("Provider"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001028 error(clazz, None, "CL4", "Inconsistent class name; should be FooProvider")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001029
1030 found = False
1031 for f in clazz.fields:
1032 if f.name == "PROVIDER_INTERFACE":
1033 found = True
1034 if f.value != clazz.fullname:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001035 error(clazz, f, "C4", "Inconsistent interface constant; expected '%s'" % (clazz.fullname))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001036
Adrian Roosb787c182019-01-03 18:54:33 +01001037 if clazz.extends == "android.content.BroadcastReceiver":
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001038 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001039 if not clazz.name.endswith("Receiver"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001040 error(clazz, None, "CL4", "Inconsistent class name; should be FooReceiver")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001041
Adrian Roosb787c182019-01-03 18:54:33 +01001042 if clazz.extends == "android.app.Activity":
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001043 test_methods = True
1044 if not clazz.name.endswith("Activity"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001045 error(clazz, None, "CL4", "Inconsistent class name; should be FooActivity")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001046
1047 if test_methods:
1048 for m in clazz.methods:
1049 if "final" in m.split: continue
1050 if not re.match("on[A-Z]", m.name):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001051 if "abstract" in m.split:
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001052 warn(clazz, m, None, "Methods implemented by developers should be named onFoo()")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001053 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001054 warn(clazz, m, None, "If implemented by developer, should be named onFoo(); otherwise consider marking final")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001055
1056
1057def verify_builder(clazz):
1058 """Verify builder classes.
1059 Methods should return the builder to enable chaining."""
Adrian Roosb787c182019-01-03 18:54:33 +01001060 if clazz.extends: return
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001061 if not clazz.name.endswith("Builder"): return
1062
1063 if clazz.name != "Builder":
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001064 warn(clazz, None, None, "Builder should be defined as inner class")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001065
1066 has_build = False
1067 for m in clazz.methods:
1068 if m.name == "build":
1069 has_build = True
1070 continue
1071
1072 if m.name.startswith("get"): continue
1073 if m.name.startswith("clear"): continue
1074
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001075 if m.name.startswith("with"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001076 warn(clazz, m, None, "Builder methods names should use setFoo() style")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001077
1078 if m.name.startswith("set"):
1079 if not m.typ.endswith(clazz.fullname):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001080 warn(clazz, m, "M4", "Methods must return the builder object")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001081
1082 if not has_build:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001083 warn(clazz, None, None, "Missing build() method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001084
1085
1086def verify_aidl(clazz):
1087 """Catch people exposing raw AIDL."""
Adrian Roosb787c182019-01-03 18:54:33 +01001088 if clazz.extends == "android.os.Binder" or clazz.implements == "android.os.IInterface":
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001089 error(clazz, None, None, "Raw AIDL interfaces must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001090
1091
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001092def verify_internal(clazz):
1093 """Catch people exposing internal classes."""
1094 if clazz.pkg.name.startswith("com.android"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001095 error(clazz, None, None, "Internal classes must not be exposed")
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001096
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001097def layering_build_ranking(ranking_list):
1098 r = {}
1099 for rank, ps in enumerate(ranking_list):
1100 if not isinstance(ps, list):
1101 ps = [ps]
1102 for p in ps:
1103 rs = r
1104 for n in p.split('.'):
1105 if n not in rs:
1106 rs[n] = {}
1107 rs = rs[n]
1108 rs['-rank'] = rank
1109 return r
1110
1111LAYERING_PACKAGE_RANKING = layering_build_ranking([
1112 ["android.service","android.accessibilityservice","android.inputmethodservice","android.printservice","android.appwidget","android.webkit","android.preference","android.gesture","android.print"],
1113 "android.app",
1114 "android.widget",
1115 "android.view",
1116 "android.animation",
1117 "android.provider",
1118 ["android.content","android.graphics.drawable"],
1119 "android.database",
1120 "android.text",
1121 "android.graphics",
1122 "android.os",
1123 "android.util"
1124])
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001125
1126def verify_layering(clazz):
1127 """Catch package layering violations.
1128 For example, something in android.os depending on android.app."""
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001129
1130 def rank(p):
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001131 r = None
1132 l = LAYERING_PACKAGE_RANKING
1133 for n in p.split('.'):
1134 if n in l:
1135 l = l[n]
1136 if '-rank' in l:
1137 r = l['-rank']
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001138 else:
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001139 break
1140 return r
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001141
1142 cr = rank(clazz.pkg.name)
1143 if cr is None: return
1144
1145 for f in clazz.fields:
1146 ir = rank(f.typ)
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001147 if ir is not None and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001148 warn(clazz, f, "FW6", "Field type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001149
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001150 for m in itertools.chain(clazz.methods, clazz.ctors):
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001151 ir = rank(m.typ)
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001152 if ir is not None and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001153 warn(clazz, m, "FW6", "Method return type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001154 for arg in m.args:
1155 ir = rank(arg)
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001156 if ir is not None and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001157 warn(clazz, m, "FW6", "Method argument type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001158
1159
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001160def verify_boolean(clazz):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001161 """Verifies that boolean accessors are named correctly.
1162 For example, hasFoo() and setHasFoo()."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001163
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001164 def is_get(m): return len(m.args) == 0 and m.typ == "boolean"
1165 def is_set(m): return len(m.args) == 1 and m.args[0] == "boolean"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001166
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001167 gets = [ m for m in clazz.methods if is_get(m) ]
1168 sets = [ m for m in clazz.methods if is_set(m) ]
1169
1170 def error_if_exists(methods, trigger, expected, actual):
1171 for m in methods:
1172 if m.name == actual:
1173 error(clazz, m, "M6", "Symmetric method for %s must be named %s" % (trigger, expected))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001174
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001175 for m in clazz.methods:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001176 if is_get(m):
1177 if re.match("is[A-Z]", m.name):
1178 target = m.name[2:]
1179 expected = "setIs" + target
1180 error_if_exists(sets, m.name, expected, "setHas" + target)
1181 elif re.match("has[A-Z]", m.name):
1182 target = m.name[3:]
1183 expected = "setHas" + target
1184 error_if_exists(sets, m.name, expected, "setIs" + target)
1185 error_if_exists(sets, m.name, expected, "set" + target)
1186 elif re.match("get[A-Z]", m.name):
1187 target = m.name[3:]
1188 expected = "set" + target
1189 error_if_exists(sets, m.name, expected, "setIs" + target)
1190 error_if_exists(sets, m.name, expected, "setHas" + target)
1191
1192 if is_set(m):
1193 if re.match("set[A-Z]", m.name):
1194 target = m.name[3:]
1195 expected = "get" + target
1196 error_if_exists(sets, m.name, expected, "is" + target)
1197 error_if_exists(sets, m.name, expected, "has" + target)
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001198
1199
1200def verify_collections(clazz):
1201 """Verifies that collection types are interfaces."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001202 if clazz.fullname == "android.os.Bundle": return
1203
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001204 bad = ["java.util.Vector", "java.util.LinkedList", "java.util.ArrayList", "java.util.Stack",
1205 "java.util.HashMap", "java.util.HashSet", "android.util.ArraySet", "android.util.ArrayMap"]
1206 for m in clazz.methods:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001207 if m.typ in bad:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001208 error(clazz, m, "CL2", "Return type is concrete collection; must be higher-level interface")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001209 for arg in m.args:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001210 if arg in bad:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001211 error(clazz, m, "CL2", "Argument is concrete collection; must be higher-level interface")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001212
1213
1214def verify_flags(clazz):
1215 """Verifies that flags are non-overlapping."""
1216 known = collections.defaultdict(int)
1217 for f in clazz.fields:
1218 if "FLAG_" in f.name:
1219 try:
1220 val = int(f.value)
1221 except:
1222 continue
1223
1224 scope = f.name[0:f.name.index("FLAG_")]
1225 if val & known[scope]:
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001226 warn(clazz, f, "C1", "Found overlapping flag constant value")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001227 known[scope] |= val
1228
1229
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001230def verify_exception(clazz):
1231 """Verifies that methods don't throw generic exceptions."""
1232 for m in clazz.methods:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001233 for t in m.throws:
1234 if t in ["java.lang.Exception", "java.lang.Throwable", "java.lang.Error"]:
1235 error(clazz, m, "S1", "Methods must not throw generic exceptions")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001236
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001237 if t in ["android.os.RemoteException"]:
1238 if clazz.name == "android.content.ContentProviderClient": continue
1239 if clazz.name == "android.os.Binder": continue
1240 if clazz.name == "android.os.IBinder": continue
Jeff Sharkey331279b2016-02-29 16:02:02 -07001241
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001242 error(clazz, m, "FW9", "Methods calling into system server should rethrow RemoteException as RuntimeException")
1243
1244 if len(m.args) == 0 and t in ["java.lang.IllegalArgumentException", "java.lang.NullPointerException"]:
1245 warn(clazz, m, "S1", "Methods taking no arguments should throw IllegalStateException")
Jeff Sharkey331279b2016-02-29 16:02:02 -07001246
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001247GOOGLE_IGNORECASE = re.compile("google", re.IGNORECASE)
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001248
1249def verify_google(clazz):
1250 """Verifies that APIs never reference Google."""
1251
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001252 if GOOGLE_IGNORECASE.search(clazz.raw) is not None:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001253 error(clazz, None, None, "Must never reference Google")
1254
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001255 for test in clazz.ctors, clazz.fields, clazz.methods:
1256 for t in test:
1257 if GOOGLE_IGNORECASE.search(t.raw) is not None:
1258 error(clazz, t, None, "Must never reference Google")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001259
1260
1261def verify_bitset(clazz):
1262 """Verifies that we avoid using heavy BitSet."""
1263
1264 for f in clazz.fields:
1265 if f.typ == "java.util.BitSet":
1266 error(clazz, f, None, "Field type must not be heavy BitSet")
1267
1268 for m in clazz.methods:
1269 if m.typ == "java.util.BitSet":
1270 error(clazz, m, None, "Return type must not be heavy BitSet")
1271 for arg in m.args:
1272 if arg == "java.util.BitSet":
1273 error(clazz, m, None, "Argument type must not be heavy BitSet")
1274
1275
1276def verify_manager(clazz):
1277 """Verifies that FooManager is only obtained from Context."""
1278
1279 if not clazz.name.endswith("Manager"): return
1280
1281 for c in clazz.ctors:
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001282 error(clazz, c, None, "Managers must always be obtained from Context; no direct constructors")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001283
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001284 for m in clazz.methods:
1285 if m.typ == clazz.fullname:
1286 error(clazz, m, None, "Managers must always be obtained from Context")
1287
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001288
1289def verify_boxed(clazz):
1290 """Verifies that methods avoid boxed primitives."""
1291
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001292 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 -08001293
1294 for c in clazz.ctors:
1295 for arg in c.args:
1296 if arg in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001297 error(clazz, c, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001298
1299 for f in clazz.fields:
1300 if f.typ in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001301 error(clazz, f, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001302
1303 for m in clazz.methods:
1304 if m.typ in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001305 error(clazz, m, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001306 for arg in m.args:
1307 if arg in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001308 error(clazz, m, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001309
1310
1311def verify_static_utils(clazz):
1312 """Verifies that helper classes can't be constructed."""
1313 if clazz.fullname.startswith("android.opengl"): return
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001314 if clazz.fullname.startswith("android.R"): return
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001315
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001316 # Only care about classes with default constructors
1317 if len(clazz.ctors) == 1 and len(clazz.ctors[0].args) == 0:
1318 test = []
1319 test.extend(clazz.fields)
1320 test.extend(clazz.methods)
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001321
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001322 if len(test) == 0: return
1323 for t in test:
1324 if "static" not in t.split:
1325 return
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001326
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001327 error(clazz, None, None, "Fully-static utility classes must not have constructor")
1328
1329
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001330def verify_overload_args(clazz):
1331 """Verifies that method overloads add new arguments at the end."""
1332 if clazz.fullname.startswith("android.opengl"): return
1333
1334 overloads = collections.defaultdict(list)
1335 for m in clazz.methods:
1336 if "deprecated" in m.split: continue
1337 overloads[m.name].append(m)
1338
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001339 for name, methods in overloads.items():
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001340 if len(methods) <= 1: continue
1341
1342 # Look for arguments common across all overloads
1343 def cluster(args):
1344 count = collections.defaultdict(int)
1345 res = set()
1346 for i in range(len(args)):
1347 a = args[i]
1348 res.add("%s#%d" % (a, count[a]))
1349 count[a] += 1
1350 return res
1351
1352 common_args = cluster(methods[0].args)
1353 for m in methods:
1354 common_args = common_args & cluster(m.args)
1355
1356 if len(common_args) == 0: continue
1357
1358 # Require that all common arguments are present at start of signature
1359 locked_sig = None
1360 for m in methods:
1361 sig = m.args[0:len(common_args)]
1362 if not common_args.issubset(cluster(sig)):
1363 warn(clazz, m, "M2", "Expected common arguments [%s] at beginning of overloaded method" % (", ".join(common_args)))
1364 elif not locked_sig:
1365 locked_sig = sig
1366 elif locked_sig != sig:
1367 error(clazz, m, "M2", "Expected consistent argument ordering between overloads: %s..." % (", ".join(locked_sig)))
1368
1369
1370def verify_callback_handlers(clazz):
1371 """Verifies that methods adding listener/callback have overload
1372 for specifying delivery thread."""
1373
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001374 # Ignore UI packages which assume main thread
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001375 skip = [
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001376 "animation",
1377 "view",
1378 "graphics",
1379 "transition",
1380 "widget",
1381 "webkit",
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001382 ]
1383 for s in skip:
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001384 if s in clazz.pkg.name_path: return
1385 if s in clazz.extends_path: return
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001386
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001387 # Ignore UI classes which assume main thread
1388 if "app" in clazz.pkg.name_path or "app" in clazz.extends_path:
1389 for s in ["ActionBar","Dialog","Application","Activity","Fragment","Loader"]:
1390 if s in clazz.fullname: return
1391 if "content" in clazz.pkg.name_path or "content" in clazz.extends_path:
1392 for s in ["Loader"]:
1393 if s in clazz.fullname: return
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001394
1395 found = {}
1396 by_name = collections.defaultdict(list)
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001397 examine = clazz.ctors + clazz.methods
1398 for m in examine:
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001399 if m.name.startswith("unregister"): continue
1400 if m.name.startswith("remove"): continue
1401 if re.match("on[A-Z]+", m.name): continue
1402
1403 by_name[m.name].append(m)
1404
1405 for a in m.args:
1406 if a.endswith("Listener") or a.endswith("Callback") or a.endswith("Callbacks"):
1407 found[m.name] = m
1408
1409 for f in found.values():
1410 takes_handler = False
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001411 takes_exec = False
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001412 for m in by_name[f.name]:
1413 if "android.os.Handler" in m.args:
1414 takes_handler = True
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001415 if "java.util.concurrent.Executor" in m.args:
1416 takes_exec = True
1417 if not takes_exec:
1418 warn(clazz, f, "L1", "Registration methods should have overload that accepts delivery Executor")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001419
1420
1421def verify_context_first(clazz):
1422 """Verifies that methods accepting a Context keep it the first argument."""
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001423 examine = clazz.ctors + clazz.methods
1424 for m in examine:
1425 if len(m.args) > 1 and m.args[0] != "android.content.Context":
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001426 if "android.content.Context" in m.args[1:]:
1427 error(clazz, m, "M3", "Context is distinct, so it must be the first argument")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001428 if len(m.args) > 1 and m.args[0] != "android.content.ContentResolver":
1429 if "android.content.ContentResolver" in m.args[1:]:
1430 error(clazz, m, "M3", "ContentResolver is distinct, so it must be the first argument")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001431
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001432
1433def verify_listener_last(clazz):
1434 """Verifies that methods accepting a Listener or Callback keep them as last arguments."""
1435 examine = clazz.ctors + clazz.methods
1436 for m in examine:
1437 if "Listener" in m.name or "Callback" in m.name: continue
1438 found = False
1439 for a in m.args:
1440 if a.endswith("Callback") or a.endswith("Callbacks") or a.endswith("Listener"):
1441 found = True
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001442 elif found:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001443 warn(clazz, m, "M3", "Listeners should always be at end of argument list")
1444
1445
1446def verify_resource_names(clazz):
1447 """Verifies that resource names have consistent case."""
1448 if not re.match("android\.R\.[a-z]+", clazz.fullname): return
1449
1450 # Resources defined by files are foo_bar_baz
1451 if clazz.name in ["anim","animator","color","dimen","drawable","interpolator","layout","transition","menu","mipmap","string","plurals","raw","xml"]:
1452 for f in clazz.fields:
Jeff Sharkeyeff9e222018-09-27 16:29:25 -06001453 if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue
1454 if f.name.startswith("config_"):
1455 error(clazz, f, None, "Expected config name to be config_fooBarBaz style")
1456
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001457 if re.match("[a-z1-9_]+$", f.name): continue
1458 error(clazz, f, None, "Expected resource name in this class to be foo_bar_baz style")
1459
1460 # Resources defined inside files are fooBarBaz
1461 if clazz.name in ["array","attr","id","bool","fraction","integer"]:
1462 for f in clazz.fields:
1463 if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue
1464 if re.match("layout_[a-z][a-zA-Z1-9]*$", f.name): continue
1465 if re.match("state_[a-z_]*$", f.name): continue
1466
1467 if re.match("[a-z][a-zA-Z1-9]*$", f.name): continue
1468 error(clazz, f, "C7", "Expected resource name in this class to be fooBarBaz style")
1469
1470 # Styles are FooBar_Baz
1471 if clazz.name in ["style"]:
1472 for f in clazz.fields:
1473 if re.match("[A-Z][A-Za-z1-9]+(_[A-Z][A-Za-z1-9]+?)*$", f.name): continue
1474 error(clazz, f, "C7", "Expected resource name in this class to be FooBar_Baz style")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001475
1476
Jeff Sharkey331279b2016-02-29 16:02:02 -07001477def verify_files(clazz):
1478 """Verifies that methods accepting File also accept streams."""
1479
1480 has_file = set()
1481 has_stream = set()
1482
1483 test = []
1484 test.extend(clazz.ctors)
1485 test.extend(clazz.methods)
1486
1487 for m in test:
1488 if "java.io.File" in m.args:
1489 has_file.add(m)
1490 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:
1491 has_stream.add(m.name)
1492
1493 for m in has_file:
1494 if m.name not in has_stream:
1495 warn(clazz, m, "M10", "Methods accepting File should also accept FileDescriptor or streams")
1496
1497
Jeff Sharkey70168dd2016-03-30 21:47:16 -06001498def verify_manager_list(clazz):
1499 """Verifies that managers return List<? extends Parcelable> instead of arrays."""
1500
1501 if not clazz.name.endswith("Manager"): return
1502
1503 for m in clazz.methods:
1504 if m.typ.startswith("android.") and m.typ.endswith("[]"):
1505 warn(clazz, m, None, "Methods should return List<? extends Parcelable> instead of Parcelable[] to support ParceledListSlice under the hood")
1506
1507
Jeff Sharkey26c80902016-12-21 13:41:17 -07001508def verify_abstract_inner(clazz):
1509 """Verifies that abstract inner classes are static."""
1510
1511 if re.match(".+?\.[A-Z][^\.]+\.[A-Z]", clazz.fullname):
Adrian Roosb787c182019-01-03 18:54:33 +01001512 if "abstract" in clazz.split and "static" not in clazz.split:
Jeff Sharkey26c80902016-12-21 13:41:17 -07001513 warn(clazz, None, None, "Abstract inner classes should be static to improve testability")
1514
1515
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001516def verify_runtime_exceptions(clazz):
1517 """Verifies that runtime exceptions aren't listed in throws."""
1518
1519 banned = [
1520 "java.lang.NullPointerException",
1521 "java.lang.ClassCastException",
1522 "java.lang.IndexOutOfBoundsException",
1523 "java.lang.reflect.UndeclaredThrowableException",
1524 "java.lang.reflect.MalformedParametersException",
1525 "java.lang.reflect.MalformedParameterizedTypeException",
1526 "java.lang.invoke.WrongMethodTypeException",
1527 "java.lang.EnumConstantNotPresentException",
1528 "java.lang.IllegalMonitorStateException",
1529 "java.lang.SecurityException",
1530 "java.lang.UnsupportedOperationException",
1531 "java.lang.annotation.AnnotationTypeMismatchException",
1532 "java.lang.annotation.IncompleteAnnotationException",
1533 "java.lang.TypeNotPresentException",
1534 "java.lang.IllegalStateException",
1535 "java.lang.ArithmeticException",
1536 "java.lang.IllegalArgumentException",
1537 "java.lang.ArrayStoreException",
1538 "java.lang.NegativeArraySizeException",
1539 "java.util.MissingResourceException",
1540 "java.util.EmptyStackException",
1541 "java.util.concurrent.CompletionException",
1542 "java.util.concurrent.RejectedExecutionException",
1543 "java.util.IllformedLocaleException",
1544 "java.util.ConcurrentModificationException",
1545 "java.util.NoSuchElementException",
1546 "java.io.UncheckedIOException",
1547 "java.time.DateTimeException",
1548 "java.security.ProviderException",
1549 "java.nio.BufferUnderflowException",
1550 "java.nio.BufferOverflowException",
1551 ]
1552
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001553 examine = clazz.ctors + clazz.methods
1554 for m in examine:
1555 for t in m.throws:
1556 if t in banned:
1557 error(clazz, m, None, "Methods must not mention RuntimeException subclasses in throws clauses")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001558
1559
1560def verify_error(clazz):
1561 """Verifies that we always use Exception instead of Error."""
1562 if not clazz.extends: return
1563 if clazz.extends.endswith("Error"):
1564 error(clazz, None, None, "Trouble must be reported through an Exception, not Error")
1565 if clazz.extends.endswith("Exception") and not clazz.name.endswith("Exception"):
1566 error(clazz, None, None, "Exceptions must be named FooException")
1567
1568
1569def verify_units(clazz):
1570 """Verifies that we use consistent naming for units."""
1571
1572 # If we find K, recommend replacing with V
1573 bad = {
1574 "Ns": "Nanos",
1575 "Ms": "Millis or Micros",
1576 "Sec": "Seconds", "Secs": "Seconds",
1577 "Hr": "Hours", "Hrs": "Hours",
1578 "Mo": "Months", "Mos": "Months",
1579 "Yr": "Years", "Yrs": "Years",
1580 "Byte": "Bytes", "Space": "Bytes",
1581 }
1582
1583 for m in clazz.methods:
1584 if m.typ not in ["short","int","long"]: continue
1585 for k, v in bad.iteritems():
1586 if m.name.endswith(k):
1587 error(clazz, m, None, "Expected method name units to be " + v)
1588 if m.name.endswith("Nanos") or m.name.endswith("Micros"):
1589 warn(clazz, m, None, "Returned time values are strongly encouraged to be in milliseconds unless you need the extra precision")
1590 if m.name.endswith("Seconds"):
1591 error(clazz, m, None, "Returned time values must be in milliseconds")
1592
1593 for m in clazz.methods:
1594 typ = m.typ
1595 if typ == "void":
1596 if len(m.args) != 1: continue
1597 typ = m.args[0]
1598
1599 if m.name.endswith("Fraction") and typ != "float":
1600 error(clazz, m, None, "Fractions must use floats")
1601 if m.name.endswith("Percentage") and typ != "int":
1602 error(clazz, m, None, "Percentage must use ints")
1603
1604
1605def verify_closable(clazz):
1606 """Verifies that classes are AutoClosable."""
Adrian Roosb787c182019-01-03 18:54:33 +01001607 if clazz.implements == "java.lang.AutoCloseable": return
1608 if clazz.implements == "java.io.Closeable": return
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001609
1610 for m in clazz.methods:
1611 if len(m.args) > 0: continue
1612 if m.name in ["close","release","destroy","finish","finalize","disconnect","shutdown","stop","free","quit"]:
1613 warn(clazz, m, None, "Classes that release resources should implement AutoClosable and CloseGuard")
1614 return
1615
1616
Jake Wharton9e6738f2017-08-23 11:59:55 -04001617def verify_member_name_not_kotlin_keyword(clazz):
1618 """Prevent method names which are keywords in Kotlin."""
1619
1620 # https://kotlinlang.org/docs/reference/keyword-reference.html#hard-keywords
1621 # This list does not include Java keywords as those are already impossible to use.
1622 keywords = [
1623 'as',
1624 'fun',
1625 'in',
1626 'is',
1627 'object',
1628 'typealias',
1629 'val',
1630 'var',
1631 'when',
1632 ]
1633
1634 for m in clazz.methods:
1635 if m.name in keywords:
1636 error(clazz, m, None, "Method name must not be a Kotlin keyword")
1637 for f in clazz.fields:
1638 if f.name in keywords:
1639 error(clazz, f, None, "Field name must not be a Kotlin keyword")
1640
1641
1642def verify_method_name_not_kotlin_operator(clazz):
1643 """Warn about method names which become operators in Kotlin."""
1644
1645 binary = set()
1646
1647 def unique_binary_op(m, op):
1648 if op in binary:
1649 error(clazz, m, None, "Only one of '{0}' and '{0}Assign' methods should be present for Kotlin".format(op))
1650 binary.add(op)
1651
1652 for m in clazz.methods:
1653 if 'static' in m.split:
1654 continue
1655
1656 # https://kotlinlang.org/docs/reference/operator-overloading.html#unary-prefix-operators
1657 if m.name in ['unaryPlus', 'unaryMinus', 'not'] and len(m.args) == 0:
1658 warn(clazz, m, None, "Method can be invoked as a unary operator from Kotlin")
1659
1660 # https://kotlinlang.org/docs/reference/operator-overloading.html#increments-and-decrements
1661 if m.name in ['inc', 'dec'] and len(m.args) == 0 and m.typ != 'void':
1662 # This only applies if the return type is the same or a subtype of the enclosing class, but we have no
1663 # practical way of checking that relationship here.
1664 warn(clazz, m, None, "Method can be invoked as a pre/postfix inc/decrement operator from Kotlin")
1665
1666 # https://kotlinlang.org/docs/reference/operator-overloading.html#arithmetic
1667 if m.name in ['plus', 'minus', 'times', 'div', 'rem', 'mod', 'rangeTo'] and len(m.args) == 1:
1668 warn(clazz, m, None, "Method can be invoked as a binary operator from Kotlin")
1669 unique_binary_op(m, m.name)
1670
1671 # https://kotlinlang.org/docs/reference/operator-overloading.html#in
1672 if m.name == 'contains' and len(m.args) == 1 and m.typ == 'boolean':
1673 warn(clazz, m, None, "Method can be invoked as a 'in' operator from Kotlin")
1674
1675 # https://kotlinlang.org/docs/reference/operator-overloading.html#indexed
1676 if (m.name == 'get' and len(m.args) > 0) or (m.name == 'set' and len(m.args) > 1):
1677 warn(clazz, m, None, "Method can be invoked with an indexing operator from Kotlin")
1678
1679 # https://kotlinlang.org/docs/reference/operator-overloading.html#invoke
1680 if m.name == 'invoke':
1681 warn(clazz, m, None, "Method can be invoked with function call syntax from Kotlin")
1682
1683 # https://kotlinlang.org/docs/reference/operator-overloading.html#assignments
1684 if m.name in ['plusAssign', 'minusAssign', 'timesAssign', 'divAssign', 'remAssign', 'modAssign'] \
1685 and len(m.args) == 1 \
1686 and m.typ == 'void':
1687 warn(clazz, m, None, "Method can be invoked as a compound assignment operator from Kotlin")
1688 unique_binary_op(m, m.name[:-6]) # Remove 'Assign' suffix
1689
1690
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001691def verify_collections_over_arrays(clazz):
1692 """Warn that [] should be Collections."""
1693
Adrian Roosb787c182019-01-03 18:54:33 +01001694 if "@interface" in clazz.split:
1695 return
1696
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001697 safe = ["java.lang.String[]","byte[]","short[]","int[]","long[]","float[]","double[]","boolean[]","char[]"]
1698 for m in clazz.methods:
1699 if m.typ.endswith("[]") and m.typ not in safe:
1700 warn(clazz, m, None, "Method should return Collection<> (or subclass) instead of raw array")
1701 for arg in m.args:
1702 if arg.endswith("[]") and arg not in safe:
1703 warn(clazz, m, None, "Method argument should be Collection<> (or subclass) instead of raw array")
1704
1705
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001706def verify_user_handle(clazz):
1707 """Methods taking UserHandle should be ForUser or AsUser."""
1708 if clazz.name.endswith("Listener") or clazz.name.endswith("Callback") or clazz.name.endswith("Callbacks"): return
1709 if clazz.fullname == "android.app.admin.DeviceAdminReceiver": return
1710 if clazz.fullname == "android.content.pm.LauncherApps": return
1711 if clazz.fullname == "android.os.UserHandle": return
1712 if clazz.fullname == "android.os.UserManager": return
1713
1714 for m in clazz.methods:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001715 if re.match("on[A-Z]+", m.name): continue
Jeff Sharkeya8e5df02018-11-27 17:33:42 -07001716
1717 has_arg = "android.os.UserHandle" in m.args
1718 has_name = m.name.endswith("AsUser") or m.name.endswith("ForUser")
1719
1720 if clazz.fullname.endswith("Manager") and has_arg:
1721 warn(clazz, m, None, "When a method overload is needed to target a specific "
1722 "UserHandle, callers should be directed to use "
1723 "Context.createPackageContextAsUser() and re-obtain the relevant "
1724 "Manager, and no new API should be added")
1725 elif has_arg and not has_name:
1726 warn(clazz, m, None, "Method taking UserHandle should be named 'doFooAsUser' "
1727 "or 'queryFooForUser'")
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001728
1729
1730def verify_params(clazz):
1731 """Parameter classes should be 'Params'."""
1732 if clazz.name.endswith("Params"): return
1733 if clazz.fullname == "android.app.ActivityOptions": return
1734 if clazz.fullname == "android.app.BroadcastOptions": return
1735 if clazz.fullname == "android.os.Bundle": return
1736 if clazz.fullname == "android.os.BaseBundle": return
1737 if clazz.fullname == "android.os.PersistableBundle": return
1738
1739 bad = ["Param","Parameter","Parameters","Args","Arg","Argument","Arguments","Options","Bundle"]
1740 for b in bad:
1741 if clazz.name.endswith(b):
1742 error(clazz, None, None, "Classes holding a set of parameters should be called 'FooParams'")
1743
1744
1745def verify_services(clazz):
1746 """Service name should be FOO_BAR_SERVICE = 'foo_bar'."""
1747 if clazz.fullname != "android.content.Context": return
1748
1749 for f in clazz.fields:
1750 if f.typ != "java.lang.String": continue
1751 found = re.match(r"([A-Z_]+)_SERVICE", f.name)
1752 if found:
1753 expected = found.group(1).lower()
1754 if f.value != expected:
1755 error(clazz, f, "C4", "Inconsistent service value; expected '%s'" % (expected))
1756
1757
1758def verify_tense(clazz):
1759 """Verify tenses of method names."""
1760 if clazz.fullname.startswith("android.opengl"): return
1761
1762 for m in clazz.methods:
1763 if m.name.endswith("Enable"):
1764 warn(clazz, m, None, "Unexpected tense; probably meant 'enabled'")
1765
1766
Jeff Sharkeyee21f692018-02-16 13:29:29 -07001767def verify_icu(clazz):
1768 """Verifies that richer ICU replacements are used."""
1769 better = {
1770 "java.util.TimeZone": "android.icu.util.TimeZone",
1771 "java.util.Calendar": "android.icu.util.Calendar",
1772 "java.util.Locale": "android.icu.util.ULocale",
1773 "java.util.ResourceBundle": "android.icu.util.UResourceBundle",
1774 "java.util.SimpleTimeZone": "android.icu.util.SimpleTimeZone",
1775 "java.util.StringTokenizer": "android.icu.util.StringTokenizer",
1776 "java.util.GregorianCalendar": "android.icu.util.GregorianCalendar",
1777 "java.lang.Character": "android.icu.lang.UCharacter",
1778 "java.text.BreakIterator": "android.icu.text.BreakIterator",
1779 "java.text.Collator": "android.icu.text.Collator",
1780 "java.text.DecimalFormatSymbols": "android.icu.text.DecimalFormatSymbols",
1781 "java.text.NumberFormat": "android.icu.text.NumberFormat",
1782 "java.text.DateFormatSymbols": "android.icu.text.DateFormatSymbols",
1783 "java.text.DateFormat": "android.icu.text.DateFormat",
1784 "java.text.SimpleDateFormat": "android.icu.text.SimpleDateFormat",
1785 "java.text.MessageFormat": "android.icu.text.MessageFormat",
1786 "java.text.DecimalFormat": "android.icu.text.DecimalFormat",
1787 }
1788
1789 for m in clazz.ctors + clazz.methods:
1790 types = []
1791 types.extend(m.typ)
1792 types.extend(m.args)
1793 for arg in types:
1794 if arg in better:
1795 warn(clazz, m, None, "Type %s should be replaced with richer ICU type %s" % (arg, better[arg]))
1796
1797
1798def verify_clone(clazz):
1799 """Verify that clone() isn't implemented; see EJ page 61."""
1800 for m in clazz.methods:
1801 if m.name == "clone":
1802 error(clazz, m, None, "Provide an explicit copy constructor instead of implementing clone()")
1803
1804
Jeff Sharkeyeff9e222018-09-27 16:29:25 -06001805def verify_pfd(clazz):
1806 """Verify that android APIs use PFD over FD."""
1807 examine = clazz.ctors + clazz.methods
1808 for m in examine:
1809 if m.typ == "java.io.FileDescriptor":
1810 error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
1811 if m.typ == "int":
1812 if "Fd" in m.name or "FD" in m.name or "FileDescriptor" in m.name:
1813 error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
1814 for arg in m.args:
1815 if arg == "java.io.FileDescriptor":
1816 error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
1817
1818 for f in clazz.fields:
1819 if f.typ == "java.io.FileDescriptor":
1820 error(clazz, f, "FW11", "Must use ParcelFileDescriptor")
1821
1822
1823def verify_numbers(clazz):
1824 """Discourage small numbers types like short and byte."""
1825
1826 discouraged = ["short","byte"]
1827
1828 for c in clazz.ctors:
1829 for arg in c.args:
1830 if arg in discouraged:
1831 warn(clazz, c, "FW12", "Should avoid odd sized primitives; use int instead")
1832
1833 for f in clazz.fields:
1834 if f.typ in discouraged:
1835 warn(clazz, f, "FW12", "Should avoid odd sized primitives; use int instead")
1836
1837 for m in clazz.methods:
1838 if m.typ in discouraged:
1839 warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead")
1840 for arg in m.args:
1841 if arg in discouraged:
1842 warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead")
1843
1844
1845def verify_singleton(clazz):
1846 """Catch singleton objects with constructors."""
1847
1848 singleton = False
1849 for m in clazz.methods:
1850 if m.name.startswith("get") and m.name.endswith("Instance") and " static " in m.raw:
1851 singleton = True
1852
1853 if singleton:
1854 for c in clazz.ctors:
1855 error(clazz, c, None, "Singleton classes should use getInstance() methods")
1856
1857
1858
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06001859def is_interesting(clazz):
1860 """Test if given class is interesting from an Android PoV."""
1861
1862 if clazz.pkg.name.startswith("java"): return False
1863 if clazz.pkg.name.startswith("junit"): return False
1864 if clazz.pkg.name.startswith("org.apache"): return False
1865 if clazz.pkg.name.startswith("org.xml"): return False
1866 if clazz.pkg.name.startswith("org.json"): return False
1867 if clazz.pkg.name.startswith("org.w3c"): return False
1868 if clazz.pkg.name.startswith("android.icu."): return False
1869 return True
1870
1871
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001872def examine_clazz(clazz):
1873 """Find all style issues in the given class."""
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001874
1875 notice(clazz)
1876
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06001877 if not is_interesting(clazz): return
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001878
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001879 verify_constants(clazz)
1880 verify_enums(clazz)
1881 verify_class_names(clazz)
1882 verify_method_names(clazz)
1883 verify_callbacks(clazz)
1884 verify_listeners(clazz)
1885 verify_actions(clazz)
1886 verify_extras(clazz)
1887 verify_equals(clazz)
1888 verify_parcelable(clazz)
1889 verify_protected(clazz)
1890 verify_fields(clazz)
1891 verify_register(clazz)
1892 verify_sync(clazz)
1893 verify_intent_builder(clazz)
1894 verify_helper_classes(clazz)
1895 verify_builder(clazz)
1896 verify_aidl(clazz)
1897 verify_internal(clazz)
1898 verify_layering(clazz)
1899 verify_boolean(clazz)
1900 verify_collections(clazz)
1901 verify_flags(clazz)
1902 verify_exception(clazz)
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07001903 if not ALLOW_GOOGLE: verify_google(clazz)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001904 verify_bitset(clazz)
1905 verify_manager(clazz)
1906 verify_boxed(clazz)
1907 verify_static_utils(clazz)
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001908 # verify_overload_args(clazz)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001909 verify_callback_handlers(clazz)
1910 verify_context_first(clazz)
1911 verify_listener_last(clazz)
1912 verify_resource_names(clazz)
Jeff Sharkey331279b2016-02-29 16:02:02 -07001913 verify_files(clazz)
Jeff Sharkey70168dd2016-03-30 21:47:16 -06001914 verify_manager_list(clazz)
Jeff Sharkey26c80902016-12-21 13:41:17 -07001915 verify_abstract_inner(clazz)
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001916 verify_runtime_exceptions(clazz)
1917 verify_error(clazz)
1918 verify_units(clazz)
1919 verify_closable(clazz)
Jake Wharton9e6738f2017-08-23 11:59:55 -04001920 verify_member_name_not_kotlin_keyword(clazz)
1921 verify_method_name_not_kotlin_operator(clazz)
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001922 verify_collections_over_arrays(clazz)
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001923 verify_user_handle(clazz)
1924 verify_params(clazz)
1925 verify_services(clazz)
1926 verify_tense(clazz)
Jeff Sharkeyee21f692018-02-16 13:29:29 -07001927 verify_icu(clazz)
1928 verify_clone(clazz)
Jeff Sharkeyeff9e222018-09-27 16:29:25 -06001929 verify_pfd(clazz)
1930 verify_numbers(clazz)
1931 verify_singleton(clazz)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001932
1933
Adrian Roos038a0292018-12-19 17:11:21 +01001934def examine_stream(stream, base_stream=None, in_classes_with_base=[], out_classes_with_base=None):
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001935 """Find all style issues in the given API stream."""
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001936 global failures, noticed
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001937 failures = {}
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001938 noticed = {}
Adrian Roos038a0292018-12-19 17:11:21 +01001939 _parse_stream(stream, examine_clazz, base_f=base_stream,
1940 in_classes_with_base=in_classes_with_base,
1941 out_classes_with_base=out_classes_with_base)
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001942 return (failures, noticed)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001943
1944
1945def examine_api(api):
1946 """Find all style issues in the given parsed API."""
1947 global failures
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001948 failures = {}
1949 for key in sorted(api.keys()):
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001950 examine_clazz(api[key])
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001951 return failures
1952
1953
Jeff Sharkey037458a2014-09-04 15:46:20 -07001954def verify_compat(cur, prev):
1955 """Find any incompatible API changes between two levels."""
1956 global failures
1957
1958 def class_exists(api, test):
1959 return test.fullname in api
1960
1961 def ctor_exists(api, clazz, test):
1962 for m in clazz.ctors:
1963 if m.ident == test.ident: return True
1964 return False
1965
1966 def all_methods(api, clazz):
1967 methods = list(clazz.methods)
1968 if clazz.extends is not None:
1969 methods.extend(all_methods(api, api[clazz.extends]))
1970 return methods
1971
1972 def method_exists(api, clazz, test):
1973 methods = all_methods(api, clazz)
1974 for m in methods:
1975 if m.ident == test.ident: return True
1976 return False
1977
1978 def field_exists(api, clazz, test):
1979 for f in clazz.fields:
1980 if f.ident == test.ident: return True
1981 return False
1982
1983 failures = {}
1984 for key in sorted(prev.keys()):
1985 prev_clazz = prev[key]
1986
1987 if not class_exists(cur, prev_clazz):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001988 error(prev_clazz, None, None, "Class removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07001989 continue
1990
1991 cur_clazz = cur[key]
1992
1993 for test in prev_clazz.ctors:
1994 if not ctor_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001995 error(prev_clazz, prev_ctor, None, "Constructor removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07001996
1997 methods = all_methods(prev, prev_clazz)
1998 for test in methods:
1999 if not method_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08002000 error(prev_clazz, test, None, "Method removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07002001
2002 for test in prev_clazz.fields:
2003 if not field_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08002004 error(prev_clazz, test, None, "Field removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07002005
2006 return failures
2007
2008
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002009def show_deprecations_at_birth(cur, prev):
2010 """Show API deprecations at birth."""
2011 global failures
2012
2013 # Remove all existing things so we're left with new
2014 for prev_clazz in prev.values():
2015 cur_clazz = cur[prev_clazz.fullname]
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06002016 if not is_interesting(cur_clazz): continue
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002017
2018 sigs = { i.ident: i for i in prev_clazz.ctors }
2019 cur_clazz.ctors = [ i for i in cur_clazz.ctors if i.ident not in sigs ]
2020 sigs = { i.ident: i for i in prev_clazz.methods }
2021 cur_clazz.methods = [ i for i in cur_clazz.methods if i.ident not in sigs ]
2022 sigs = { i.ident: i for i in prev_clazz.fields }
2023 cur_clazz.fields = [ i for i in cur_clazz.fields if i.ident not in sigs ]
2024
2025 # Forget about class entirely when nothing new
2026 if len(cur_clazz.ctors) == 0 and len(cur_clazz.methods) == 0 and len(cur_clazz.fields) == 0:
2027 del cur[prev_clazz.fullname]
2028
2029 for clazz in cur.values():
Adrian Roosb787c182019-01-03 18:54:33 +01002030 if "deprecated" in clazz.split and not clazz.fullname in prev:
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002031 error(clazz, None, None, "Found API deprecation at birth")
2032
2033 for i in clazz.ctors + clazz.methods + clazz.fields:
Adrian Roosb787c182019-01-03 18:54:33 +01002034 if "deprecated" in i.split:
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002035 error(clazz, i, None, "Found API deprecation at birth")
2036
2037 print "%s Deprecated at birth %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True),
2038 format(reset=True)))
2039 for f in sorted(failures):
2040 print failures[f]
2041 print
2042
2043
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06002044def show_stats(cur, prev):
2045 """Show API stats."""
2046
2047 stats = collections.defaultdict(int)
2048 for cur_clazz in cur.values():
2049 if not is_interesting(cur_clazz): continue
2050
2051 if cur_clazz.fullname not in prev:
2052 stats['new_classes'] += 1
2053 stats['new_ctors'] += len(cur_clazz.ctors)
2054 stats['new_methods'] += len(cur_clazz.methods)
2055 stats['new_fields'] += len(cur_clazz.fields)
2056 else:
2057 prev_clazz = prev[cur_clazz.fullname]
2058
2059 sigs = { i.ident: i for i in prev_clazz.ctors }
2060 ctors = len([ i for i in cur_clazz.ctors if i.ident not in sigs ])
2061 sigs = { i.ident: i for i in prev_clazz.methods }
2062 methods = len([ i for i in cur_clazz.methods if i.ident not in sigs ])
2063 sigs = { i.ident: i for i in prev_clazz.fields }
2064 fields = len([ i for i in cur_clazz.fields if i.ident not in sigs ])
2065
2066 if ctors + methods + fields > 0:
2067 stats['extend_classes'] += 1
2068 stats['extend_ctors'] += ctors
2069 stats['extend_methods'] += methods
2070 stats['extend_fields'] += fields
2071
2072 print "#", "".join([ k.ljust(20) for k in sorted(stats.keys()) ])
2073 print " ", "".join([ str(stats[k]).ljust(20) for k in sorted(stats.keys()) ])
2074
2075
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002076if __name__ == "__main__":
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002077 parser = argparse.ArgumentParser(description="Enforces common Android public API design \
2078 patterns. It ignores lint messages from a previous API level, if provided.")
2079 parser.add_argument("current.txt", type=argparse.FileType('r'), help="current.txt")
2080 parser.add_argument("previous.txt", nargs='?', type=argparse.FileType('r'), default=None,
2081 help="previous.txt")
Adrian Roos6eb57b02018-12-13 22:08:29 +01002082 parser.add_argument("--base-current", nargs='?', type=argparse.FileType('r'), default=None,
2083 help="The base current.txt to use when examining system-current.txt or"
2084 " test-current.txt")
2085 parser.add_argument("--base-previous", nargs='?', type=argparse.FileType('r'), default=None,
2086 help="The base previous.txt to use when examining system-previous.txt or"
2087 " test-previous.txt")
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002088 parser.add_argument("--no-color", action='store_const', const=True,
2089 help="Disable terminal colors")
2090 parser.add_argument("--allow-google", action='store_const', const=True,
2091 help="Allow references to Google")
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002092 parser.add_argument("--show-noticed", action='store_const', const=True,
2093 help="Show API changes noticed")
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002094 parser.add_argument("--show-deprecations-at-birth", action='store_const', const=True,
2095 help="Show API deprecations at birth")
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06002096 parser.add_argument("--show-stats", action='store_const', const=True,
2097 help="Show API stats")
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002098 args = vars(parser.parse_args())
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002099
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002100 if args['no_color']:
2101 USE_COLOR = False
2102
2103 if args['allow_google']:
2104 ALLOW_GOOGLE = True
2105
2106 current_file = args['current.txt']
Adrian Roos6eb57b02018-12-13 22:08:29 +01002107 base_current_file = args['base_current']
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002108 previous_file = args['previous.txt']
Adrian Roos6eb57b02018-12-13 22:08:29 +01002109 base_previous_file = args['base_previous']
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002110
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002111 if args['show_deprecations_at_birth']:
2112 with current_file as f:
2113 cur = _parse_stream(f)
2114 with previous_file as f:
2115 prev = _parse_stream(f)
2116 show_deprecations_at_birth(cur, prev)
2117 sys.exit()
2118
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06002119 if args['show_stats']:
2120 with current_file as f:
2121 cur = _parse_stream(f)
2122 with previous_file as f:
2123 prev = _parse_stream(f)
2124 show_stats(cur, prev)
2125 sys.exit()
2126
Adrian Roos038a0292018-12-19 17:11:21 +01002127 classes_with_base = []
2128
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002129 with current_file as f:
Adrian Roos6eb57b02018-12-13 22:08:29 +01002130 if base_current_file:
2131 with base_current_file as base_f:
Adrian Roos038a0292018-12-19 17:11:21 +01002132 cur_fail, cur_noticed = examine_stream(f, base_f,
2133 out_classes_with_base=classes_with_base)
Adrian Roos6eb57b02018-12-13 22:08:29 +01002134 else:
Adrian Roos038a0292018-12-19 17:11:21 +01002135 cur_fail, cur_noticed = examine_stream(f, out_classes_with_base=classes_with_base)
2136
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002137 if not previous_file is None:
2138 with previous_file as f:
Adrian Roos6eb57b02018-12-13 22:08:29 +01002139 if base_previous_file:
2140 with base_previous_file as base_f:
Adrian Roos038a0292018-12-19 17:11:21 +01002141 prev_fail, prev_noticed = examine_stream(f, base_f,
2142 in_classes_with_base=classes_with_base)
Adrian Roos6eb57b02018-12-13 22:08:29 +01002143 else:
Adrian Roos038a0292018-12-19 17:11:21 +01002144 prev_fail, prev_noticed = examine_stream(f, in_classes_with_base=classes_with_base)
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002145
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002146 # ignore errors from previous API level
2147 for p in prev_fail:
2148 if p in cur_fail:
2149 del cur_fail[p]
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002150
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002151 # ignore classes unchanged from previous API level
2152 for k, v in prev_noticed.iteritems():
2153 if k in cur_noticed and v == cur_noticed[k]:
2154 del cur_noticed[k]
2155
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002156 """
2157 # NOTE: disabled because of memory pressure
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002158 # look for compatibility issues
2159 compat_fail = verify_compat(cur, prev)
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002160
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002161 print "%s API compatibility issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
2162 for f in sorted(compat_fail):
2163 print compat_fail[f]
2164 print
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002165 """
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002166
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002167 if args['show_noticed'] and len(cur_noticed) != 0:
2168 print "%s API changes noticed %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
2169 for f in sorted(cur_noticed.keys()):
2170 print f
2171 print
2172
Jason Monk53b2a732017-11-10 15:43:17 -05002173 if len(cur_fail) != 0:
2174 print "%s API style issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
2175 for f in sorted(cur_fail):
2176 print cur_fail[f]
2177 print
2178 sys.exit(77)