blob: 5680c8ca3a6acce3e6f6759fbeec6d1295b680dd [file] [log] [blame]
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001#!/usr/bin/env python
2
3# Copyright (C) 2014 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the 'License');
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an 'AS IS' BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""
18Enforces common Android public API design patterns. It ignores lint messages from
19a previous API level, if provided.
20
21Usage: apilint.py current.txt
22Usage: apilint.py current.txt previous.txt
Jeff Sharkey1498f9c2014-09-04 12:45:33 -070023
24You can also splice in blame details like this:
25$ git blame api/current.txt -t -e > /tmp/currentblame.txt
26$ apilint.py /tmp/currentblame.txt previous.txt --no-color
Jeff Sharkey8190f4882014-08-28 12:24:07 -070027"""
28
Adam Metcalf1c7e70a2015-03-27 16:11:23 -070029import re, sys, collections, traceback, argparse
Jeff Sharkey8190f4882014-08-28 12:24:07 -070030
31
32BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
33
Adam Metcalf1c7e70a2015-03-27 16:11:23 -070034ALLOW_GOOGLE = False
35USE_COLOR = True
36
Jeff Sharkey8190f4882014-08-28 12:24:07 -070037def format(fg=None, bg=None, bright=False, bold=False, dim=False, reset=False):
38 # manually derived from http://en.wikipedia.org/wiki/ANSI_escape_code#Codes
Adam Metcalf1c7e70a2015-03-27 16:11:23 -070039 if not USE_COLOR: return ""
Jeff Sharkey8190f4882014-08-28 12:24:07 -070040 codes = []
41 if reset: codes.append("0")
42 else:
43 if not fg is None: codes.append("3%d" % (fg))
44 if not bg is None:
45 if not bright: codes.append("4%d" % (bg))
46 else: codes.append("10%d" % (bg))
47 if bold: codes.append("1")
48 elif dim: codes.append("2")
49 else: codes.append("22")
50 return "\033[%sm" % (";".join(codes))
51
52
53class Field():
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 Roosb787c182019-01-03 18:54:33 +0100226 DELIMITER = re.compile(r'\s+|[()@<>;,={}/"]|\[\]')
227 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
438 def parse_type(self):
439 type = self.parse_token()
440 if type in V2LineParser.JAVA_LANG_TYPES:
441 type = "java.lang." + type
442 self.parse_matching_paren("<", ">")
443 while self.parse_if("[]"):
444 type += "[]"
445 return type
446
447 def parse_arg_type(self):
448 type = self.parse_type()
449 if self.parse_if("..."):
450 type += "..."
451 return type
452
453 def parse_name(self):
454 return self.parse_token()
455
456 def parse_args(self):
457 args = []
458 if self.lookahead() == ")":
459 return args
460
461 while True:
462 args.append(self.parse_arg())
463 if self.lookahead() == ")":
464 return args
465 self.parse_token(",")
466
467 def parse_arg(self):
468 self.parse_annotations()
469 return self.parse_arg_type()
470
471 def parse_throws(self):
472 ret = []
473 if self.parse_if("throws"):
474 ret.append(self.parse_type())
475 while self.parse_if(","):
476 ret.append(self.parse_type())
477 return ret
478
479 def parse_extends(self):
480 if self.parse_if("extends"):
481 return self.parse_space_delimited_type_list()
482 return []
483
484 def parse_implements(self):
485 if self.parse_if("implements"):
486 return self.parse_space_delimited_type_list()
487 return []
488
489 def parse_space_delimited_type_list(self, terminals = ["implements", "{"]):
490 types = []
491 while True:
492 types.append(self.parse_type())
493 if self.lookahead() in terminals:
494 return types
495
496 def parse_annotation_default(self):
497 if self.parse_if("default"):
498 self.parse_value()
499
500 def parse_value(self):
501 if self.lookahead() == "{":
502 return " ".join(self.parse_matching_paren("{", "}"))
503 elif self.lookahead() == "(":
504 return " ".join(self.parse_matching_paren("(", ")"))
505 else:
506 return self.parse_token()
507
508 def parse_value_stripped(self):
509 value = self.parse_value()
510 if value[0] == '"':
511 return value[1:-1]
512 return value
513
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700514
Adrian Roos038a0292018-12-19 17:11:21 +0100515def _parse_stream(f, clazz_cb=None, base_f=None, out_classes_with_base=None,
516 in_classes_with_base=[]):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700517 api = {}
Adrian Roos038a0292018-12-19 17:11:21 +0100518 in_classes_with_base = _retry_iterator(in_classes_with_base)
Adrian Roos6eb57b02018-12-13 22:08:29 +0100519
520 if base_f:
Adrian Roos038a0292018-12-19 17:11:21 +0100521 base_classes = _retry_iterator(_parse_stream_to_generator(base_f))
Adrian Roos6eb57b02018-12-13 22:08:29 +0100522 else:
523 base_classes = []
524
Adrian Roos038a0292018-12-19 17:11:21 +0100525 def handle_class(clazz):
Adrian Roos6eb57b02018-12-13 22:08:29 +0100526 if clazz_cb:
527 clazz_cb(clazz)
528 else: # In callback mode, don't keep track of the full API
529 api[clazz.fullname] = clazz
530
Adrian Roos038a0292018-12-19 17:11:21 +0100531 def handle_missed_classes_with_base(clazz):
532 for c in _yield_until_matching_class(in_classes_with_base, clazz):
533 base_class = _skip_to_matching_class(base_classes, c)
534 if base_class:
535 handle_class(base_class)
536
537 for clazz in _parse_stream_to_generator(f):
538 # Before looking at clazz, let's see if there's some classes that were not present, but
539 # may have an entry in the base stream.
540 handle_missed_classes_with_base(clazz)
541
542 base_class = _skip_to_matching_class(base_classes, clazz)
543 if base_class:
544 clazz.merge_from(base_class)
545 if out_classes_with_base is not None:
546 out_classes_with_base.append(clazz)
547 handle_class(clazz)
548
549 handle_missed_classes_with_base(None)
550
Adrian Roos6eb57b02018-12-13 22:08:29 +0100551 return api
552
553def _parse_stream_to_generator(f):
554 line = 0
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700555 pkg = None
556 clazz = None
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700557 blame = None
Adrian Roosb787c182019-01-03 18:54:33 +0100558 sig_format = 1
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700559
560 re_blame = re.compile("^([a-z0-9]{7,}) \(<([^>]+)>.+?\) (.+?)$")
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800561 for raw in f:
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700562 line += 1
563 raw = raw.rstrip()
564 match = re_blame.match(raw)
565 if match is not None:
566 blame = match.groups()[0:2]
567 raw = match.groups()[2]
568 else:
569 blame = None
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700570
Adrian Roosb787c182019-01-03 18:54:33 +0100571 if line == 1 and raw == "// Signature format: 2.0":
572 sig_format = 2
573 elif raw.startswith("package"):
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700574 pkg = Package(line, raw, blame)
575 elif raw.startswith(" ") and raw.endswith("{"):
Adrian Roosb787c182019-01-03 18:54:33 +0100576 clazz = Class(pkg, line, raw, blame, sig_format=sig_format)
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700577 elif raw.startswith(" ctor"):
Adrian Roosb787c182019-01-03 18:54:33 +0100578 clazz.ctors.append(Method(clazz, line, raw, blame, sig_format=sig_format))
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700579 elif raw.startswith(" method"):
Adrian Roosb787c182019-01-03 18:54:33 +0100580 clazz.methods.append(Method(clazz, line, raw, blame, sig_format=sig_format))
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700581 elif raw.startswith(" field"):
Adrian Roosb787c182019-01-03 18:54:33 +0100582 clazz.fields.append(Field(clazz, line, raw, blame, sig_format=sig_format))
Adrian Roos6eb57b02018-12-13 22:08:29 +0100583 elif raw.startswith(" }") and clazz:
Adrian Roos038a0292018-12-19 17:11:21 +0100584 yield clazz
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800585
Adrian Roos5ed42b62018-12-19 17:10:22 +0100586def _retry_iterator(it):
587 """Wraps an iterator, such that calling send(True) on it will redeliver the same element"""
588 for e in it:
589 while True:
590 retry = yield e
591 if not retry:
592 break
593 # send() was called, asking us to redeliver clazz on next(). Still need to yield
594 # a dummy value to the send() first though.
595 if (yield "Returning clazz on next()"):
596 raise TypeError("send() must be followed by next(), not send()")
597
Adrian Roos038a0292018-12-19 17:11:21 +0100598def _skip_to_matching_class(classes, needle):
599 """Takes a classes iterator and consumes entries until it returns the class we're looking for
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700600
Adrian Roos6eb57b02018-12-13 22:08:29 +0100601 This relies on classes being sorted by package and class name."""
602
603 for clazz in classes:
604 if clazz.pkg.name < needle.pkg.name:
605 # We haven't reached the right package yet
606 continue
Adrian Roos5ed42b62018-12-19 17:10:22 +0100607 if clazz.pkg.name == needle.pkg.name and clazz.fullname < needle.fullname:
608 # We're in the right package, but not the right class yet
Adrian Roos6eb57b02018-12-13 22:08:29 +0100609 continue
610 if clazz.fullname == needle.fullname:
611 return clazz
612 # We ran past the right class. Send it back into the generator, then report failure.
613 classes.send(clazz)
614 return None
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700615
Adrian Roos038a0292018-12-19 17:11:21 +0100616def _yield_until_matching_class(classes, needle):
617 """Takes a class iterator and yields entries it until it reaches the class we're looking for.
618
619 This relies on classes being sorted by package and class name."""
620
621 for clazz in classes:
622 if needle is None:
623 yield clazz
624 elif clazz.pkg.name < needle.pkg.name:
625 # We haven't reached the right package yet
626 yield clazz
627 elif clazz.pkg.name == needle.pkg.name and clazz.fullname < needle.fullname:
628 # We're in the right package, but not the right class yet
629 yield clazz
630 elif clazz.fullname == needle.fullname:
631 # Class found, abort.
632 return
633 else:
634 # We ran past the right class. Send it back into the iterator, then abort.
635 classes.send(clazz)
636 return
637
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700638class Failure():
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800639 def __init__(self, sig, clazz, detail, error, rule, msg):
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700640 self.sig = sig
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700641 self.error = error
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800642 self.rule = rule
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700643 self.msg = msg
644
645 if error:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800646 self.head = "Error %s" % (rule) if rule else "Error"
647 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 -0700648 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800649 self.head = "Warning %s" % (rule) if rule else "Warning"
650 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 -0700651
652 self.line = clazz.line
653 blame = clazz.blame
654 if detail is not None:
655 dump += "\n in " + repr(detail)
656 self.line = detail.line
657 blame = detail.blame
658 dump += "\n in " + repr(clazz)
659 dump += "\n in " + repr(clazz.pkg)
660 dump += "\n at line " + repr(self.line)
661 if blame is not None:
662 dump += "\n last modified by %s in %s" % (blame[1], blame[0])
663
664 self.dump = dump
665
666 def __repr__(self):
667 return self.dump
668
669
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700670failures = {}
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700671
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800672def _fail(clazz, detail, error, rule, msg):
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700673 """Records an API failure to be processed later."""
674 global failures
675
Adrian Roosb787c182019-01-03 18:54:33 +0100676 sig = "%s-%s-%s" % (clazz.fullname, detail.ident if detail else None, msg)
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700677 sig = sig.replace(" deprecated ", " ")
678
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800679 failures[sig] = Failure(sig, clazz, detail, error, rule, msg)
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700680
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700681
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800682def warn(clazz, detail, rule, msg):
683 _fail(clazz, detail, False, rule, msg)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700684
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800685def error(clazz, detail, rule, msg):
686 _fail(clazz, detail, True, rule, msg)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700687
688
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -0700689noticed = {}
690
691def notice(clazz):
692 global noticed
693
694 noticed[clazz.fullname] = hash(clazz)
695
696
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700697def verify_constants(clazz):
698 """All static final constants must be FOO_NAME style."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700699 if re.match("android\.R\.[a-z]+", clazz.fullname): return
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600700 if clazz.fullname.startswith("android.os.Build"): return
701 if clazz.fullname == "android.system.OsConstants": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700702
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600703 req = ["java.lang.String","byte","short","int","long","float","double","boolean","char"]
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700704 for f in clazz.fields:
705 if "static" in f.split and "final" in f.split:
706 if re.match("[A-Z0-9_]+", f.name) is None:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800707 error(clazz, f, "C2", "Constant field names must be FOO_NAME")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600708 if f.typ != "java.lang.String":
Jeff Sharkey331279b2016-02-29 16:02:02 -0700709 if f.name.startswith("MIN_") or f.name.startswith("MAX_"):
710 warn(clazz, f, "C8", "If min/max could change in future, make them dynamic methods")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600711 if f.typ in req and f.value is None:
712 error(clazz, f, None, "All constants must be defined at compile time")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700713
714
715def verify_enums(clazz):
716 """Enums are bad, mmkay?"""
Adrian Roosb787c182019-01-03 18:54:33 +0100717 if clazz.extends == "java.lang.Enum" or "enum" in clazz.split:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800718 error(clazz, None, "F5", "Enums are not allowed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700719
720
721def verify_class_names(clazz):
722 """Try catching malformed class names like myMtp or MTPUser."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700723 if clazz.fullname.startswith("android.opengl"): return
724 if clazz.fullname.startswith("android.renderscript"): return
725 if re.match("android\.R\.[a-z]+", clazz.fullname): return
726
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700727 if re.search("[A-Z]{2,}", clazz.name) is not None:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800728 warn(clazz, None, "S1", "Class names with acronyms should be Mtp not MTP")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700729 if re.match("[^A-Z]", clazz.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800730 error(clazz, None, "S1", "Class must start with uppercase char")
Jeff Sharkeyee21f692018-02-16 13:29:29 -0700731 if clazz.name.endswith("Impl"):
732 error(clazz, None, None, "Don't expose your implementation details")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700733
734
735def verify_method_names(clazz):
736 """Try catching malformed method names, like Foo() or getMTU()."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700737 if clazz.fullname.startswith("android.opengl"): return
738 if clazz.fullname.startswith("android.renderscript"): return
739 if clazz.fullname == "android.system.OsConstants": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700740
741 for m in clazz.methods:
742 if re.search("[A-Z]{2,}", m.name) is not None:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800743 warn(clazz, m, "S1", "Method names with acronyms should be getMtu() instead of getMTU()")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700744 if re.match("[^a-z]", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800745 error(clazz, m, "S1", "Method name must start with lowercase char")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700746
747
748def verify_callbacks(clazz):
749 """Verify Callback classes.
750 All callback classes must be abstract.
751 All methods must follow onFoo() naming style."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700752 if clazz.fullname == "android.speech.tts.SynthesisCallback": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700753
754 if clazz.name.endswith("Callbacks"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800755 error(clazz, None, "L1", "Callback class names should be singular")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700756 if clazz.name.endswith("Observer"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800757 warn(clazz, None, "L1", "Class should be named FooCallback")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700758
759 if clazz.name.endswith("Callback"):
760 if "interface" in clazz.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800761 error(clazz, None, "CL3", "Callbacks must be abstract class to enable extension in future API levels")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700762
763 for m in clazz.methods:
764 if not re.match("on[A-Z][a-z]*", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800765 error(clazz, m, "L1", "Callback method names must be onFoo() style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700766
767
768def verify_listeners(clazz):
769 """Verify Listener classes.
770 All Listener classes must be interface.
771 All methods must follow onFoo() naming style.
772 If only a single method, it must match class name:
773 interface OnFooListener { void onFoo() }"""
774
775 if clazz.name.endswith("Listener"):
Adrian Roosb787c182019-01-03 18:54:33 +0100776 if "abstract" in clazz.split and "class" in clazz.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800777 error(clazz, None, "L1", "Listeners should be an interface, or otherwise renamed Callback")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700778
779 for m in clazz.methods:
780 if not re.match("on[A-Z][a-z]*", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800781 error(clazz, m, "L1", "Listener method names must be onFoo() style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700782
783 if len(clazz.methods) == 1 and clazz.name.startswith("On"):
784 m = clazz.methods[0]
785 if (m.name + "Listener").lower() != clazz.name.lower():
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800786 error(clazz, m, "L1", "Single listener method name must match class name")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700787
788
789def verify_actions(clazz):
790 """Verify intent actions.
791 All action names must be named ACTION_FOO.
792 All action values must be scoped by package and match name:
793 package android.foo {
794 String ACTION_BAR = "android.foo.action.BAR";
795 }"""
796 for f in clazz.fields:
797 if f.value is None: continue
798 if f.name.startswith("EXTRA_"): continue
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700799 if f.name == "SERVICE_INTERFACE" or f.name == "PROVIDER_INTERFACE": continue
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600800 if "INTERACTION" in f.name: continue
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700801
802 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
803 if "_ACTION" in f.name or "ACTION_" in f.name or ".action." in f.value.lower():
804 if not f.name.startswith("ACTION_"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800805 error(clazz, f, "C3", "Intent action constant name must be ACTION_FOO")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700806 else:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700807 if clazz.fullname == "android.content.Intent":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700808 prefix = "android.intent.action"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700809 elif clazz.fullname == "android.provider.Settings":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700810 prefix = "android.settings"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700811 elif clazz.fullname == "android.app.admin.DevicePolicyManager" or clazz.fullname == "android.app.admin.DeviceAdminReceiver":
812 prefix = "android.app.action"
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700813 else:
814 prefix = clazz.pkg.name + ".action"
815 expected = prefix + "." + f.name[7:]
816 if f.value != expected:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700817 error(clazz, f, "C4", "Inconsistent action value; expected '%s'" % (expected))
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700818
819
820def verify_extras(clazz):
821 """Verify intent extras.
822 All extra names must be named EXTRA_FOO.
823 All extra values must be scoped by package and match name:
824 package android.foo {
825 String EXTRA_BAR = "android.foo.extra.BAR";
826 }"""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700827 if clazz.fullname == "android.app.Notification": return
828 if clazz.fullname == "android.appwidget.AppWidgetManager": return
829
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700830 for f in clazz.fields:
831 if f.value is None: continue
832 if f.name.startswith("ACTION_"): continue
833
834 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
835 if "_EXTRA" in f.name or "EXTRA_" in f.name or ".extra" in f.value.lower():
836 if not f.name.startswith("EXTRA_"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800837 error(clazz, f, "C3", "Intent extra must be EXTRA_FOO")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700838 else:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700839 if clazz.pkg.name == "android.content" and clazz.name == "Intent":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700840 prefix = "android.intent.extra"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700841 elif clazz.pkg.name == "android.app.admin":
842 prefix = "android.app.extra"
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700843 else:
844 prefix = clazz.pkg.name + ".extra"
845 expected = prefix + "." + f.name[6:]
846 if f.value != expected:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700847 error(clazz, f, "C4", "Inconsistent extra value; expected '%s'" % (expected))
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700848
849
850def verify_equals(clazz):
851 """Verify that equals() and hashCode() must be overridden together."""
Jeff Sharkey40d623e2016-12-21 13:46:33 -0700852 eq = False
853 hc = False
854 for m in clazz.methods:
Adrian Roosb787c182019-01-03 18:54:33 +0100855 if "static" in m.split: continue
856 if m.sig_matches("boolean", "equals", ["java.lang.Object"]): eq = True
857 if m.sig_matches("int", "hashCode", []): hc = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700858 if eq != hc:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800859 error(clazz, None, "M8", "Must override both equals and hashCode; missing one")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700860
861
862def verify_parcelable(clazz):
863 """Verify that Parcelable objects aren't hiding required bits."""
Adrian Roosb787c182019-01-03 18:54:33 +0100864 if clazz.implements == "android.os.Parcelable":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700865 creator = [ i for i in clazz.fields if i.name == "CREATOR" ]
866 write = [ i for i in clazz.methods if i.name == "writeToParcel" ]
867 describe = [ i for i in clazz.methods if i.name == "describeContents" ]
868
869 if len(creator) == 0 or len(write) == 0 or len(describe) == 0:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800870 error(clazz, None, "FW3", "Parcelable requires CREATOR, writeToParcel, and describeContents; missing one")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700871
Adrian Roosb787c182019-01-03 18:54:33 +0100872 if "final" not in clazz.split:
Jeff Sharkey331279b2016-02-29 16:02:02 -0700873 error(clazz, None, "FW8", "Parcelable classes must be final")
874
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700875 for c in clazz.ctors:
876 if c.args == ["android.os.Parcel"]:
877 error(clazz, c, "FW3", "Parcelable inflation is exposed through CREATOR, not raw constructors")
878
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700879
880def verify_protected(clazz):
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800881 """Verify that no protected methods or fields are allowed."""
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700882 for m in clazz.methods:
Jeff Sharkeyeff9e222018-09-27 16:29:25 -0600883 if m.name == "finalize": continue
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700884 if "protected" in m.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800885 error(clazz, m, "M7", "Protected methods not allowed; must be public")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700886 for f in clazz.fields:
887 if "protected" in f.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800888 error(clazz, f, "M7", "Protected fields not allowed; must be public")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700889
890
891def verify_fields(clazz):
892 """Verify that all exposed fields are final.
893 Exposed fields must follow myName style.
894 Catch internal mFoo objects being exposed."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700895
896 IGNORE_BARE_FIELDS = [
897 "android.app.ActivityManager.RecentTaskInfo",
898 "android.app.Notification",
899 "android.content.pm.ActivityInfo",
900 "android.content.pm.ApplicationInfo",
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600901 "android.content.pm.ComponentInfo",
902 "android.content.pm.ResolveInfo",
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700903 "android.content.pm.FeatureGroupInfo",
904 "android.content.pm.InstrumentationInfo",
905 "android.content.pm.PackageInfo",
906 "android.content.pm.PackageItemInfo",
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600907 "android.content.res.Configuration",
908 "android.graphics.BitmapFactory.Options",
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700909 "android.os.Message",
910 "android.system.StructPollfd",
911 ]
912
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700913 for f in clazz.fields:
914 if not "final" in f.split:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700915 if clazz.fullname in IGNORE_BARE_FIELDS:
916 pass
917 elif clazz.fullname.endswith("LayoutParams"):
918 pass
919 elif clazz.fullname.startswith("android.util.Mutable"):
920 pass
921 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800922 error(clazz, f, "F2", "Bare fields must be marked final, or add accessors if mutable")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700923
924 if not "static" in f.split:
925 if not re.match("[a-z]([a-zA-Z]+)?", f.name):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800926 error(clazz, f, "S1", "Non-static fields must be named using myField style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700927
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700928 if re.match("[ms][A-Z]", f.name):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800929 error(clazz, f, "F1", "Internal objects must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700930
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700931 if re.match("[A-Z_]+", f.name):
932 if "static" not in f.split or "final" not in f.split:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800933 error(clazz, f, "C2", "Constants must be marked static final")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700934
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700935
936def verify_register(clazz):
937 """Verify parity of registration methods.
938 Callback objects use register/unregister methods.
939 Listener objects use add/remove methods."""
940 methods = [ m.name for m in clazz.methods ]
941 for m in clazz.methods:
942 if "Callback" in m.raw:
943 if m.name.startswith("register"):
944 other = "unregister" + m.name[8:]
945 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800946 error(clazz, m, "L2", "Missing unregister method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700947 if m.name.startswith("unregister"):
948 other = "register" + m.name[10:]
949 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800950 error(clazz, m, "L2", "Missing register method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700951
952 if m.name.startswith("add") or m.name.startswith("remove"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800953 error(clazz, m, "L3", "Callback methods should be named register/unregister")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700954
955 if "Listener" in m.raw:
956 if m.name.startswith("add"):
957 other = "remove" + m.name[3:]
958 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800959 error(clazz, m, "L2", "Missing remove method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700960 if m.name.startswith("remove") and not m.name.startswith("removeAll"):
961 other = "add" + m.name[6:]
962 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800963 error(clazz, m, "L2", "Missing add method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700964
965 if m.name.startswith("register") or m.name.startswith("unregister"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800966 error(clazz, m, "L3", "Listener methods should be named add/remove")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700967
968
969def verify_sync(clazz):
970 """Verify synchronized methods aren't exposed."""
971 for m in clazz.methods:
972 if "synchronized" in m.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800973 error(clazz, m, "M5", "Internal locks must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700974
975
976def verify_intent_builder(clazz):
977 """Verify that Intent builders are createFooIntent() style."""
978 if clazz.name == "Intent": return
979
980 for m in clazz.methods:
981 if m.typ == "android.content.Intent":
982 if m.name.startswith("create") and m.name.endswith("Intent"):
983 pass
984 else:
Adam Powell539ea122015-04-10 13:01:37 -0700985 warn(clazz, m, "FW1", "Methods creating an Intent should be named createFooIntent()")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700986
987
988def verify_helper_classes(clazz):
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700989 """Verify that helper classes are named consistently with what they extend.
990 All developer extendable methods should be named onFoo()."""
991 test_methods = False
Adrian Roosb787c182019-01-03 18:54:33 +0100992 if clazz.extends == "android.app.Service":
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700993 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700994 if not clazz.name.endswith("Service"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800995 error(clazz, None, "CL4", "Inconsistent class name; should be FooService")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700996
997 found = False
998 for f in clazz.fields:
999 if f.name == "SERVICE_INTERFACE":
1000 found = True
1001 if f.value != clazz.fullname:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001002 error(clazz, f, "C4", "Inconsistent interface constant; expected '%s'" % (clazz.fullname))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001003
Adrian Roosb787c182019-01-03 18:54:33 +01001004 if clazz.extends == "android.content.ContentProvider":
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001005 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001006 if not clazz.name.endswith("Provider"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001007 error(clazz, None, "CL4", "Inconsistent class name; should be FooProvider")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001008
1009 found = False
1010 for f in clazz.fields:
1011 if f.name == "PROVIDER_INTERFACE":
1012 found = True
1013 if f.value != clazz.fullname:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001014 error(clazz, f, "C4", "Inconsistent interface constant; expected '%s'" % (clazz.fullname))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001015
Adrian Roosb787c182019-01-03 18:54:33 +01001016 if clazz.extends == "android.content.BroadcastReceiver":
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001017 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001018 if not clazz.name.endswith("Receiver"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001019 error(clazz, None, "CL4", "Inconsistent class name; should be FooReceiver")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001020
Adrian Roosb787c182019-01-03 18:54:33 +01001021 if clazz.extends == "android.app.Activity":
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001022 test_methods = True
1023 if not clazz.name.endswith("Activity"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001024 error(clazz, None, "CL4", "Inconsistent class name; should be FooActivity")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001025
1026 if test_methods:
1027 for m in clazz.methods:
1028 if "final" in m.split: continue
1029 if not re.match("on[A-Z]", m.name):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001030 if "abstract" in m.split:
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001031 warn(clazz, m, None, "Methods implemented by developers should be named onFoo()")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001032 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001033 warn(clazz, m, None, "If implemented by developer, should be named onFoo(); otherwise consider marking final")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001034
1035
1036def verify_builder(clazz):
1037 """Verify builder classes.
1038 Methods should return the builder to enable chaining."""
Adrian Roosb787c182019-01-03 18:54:33 +01001039 if clazz.extends: return
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001040 if not clazz.name.endswith("Builder"): return
1041
1042 if clazz.name != "Builder":
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001043 warn(clazz, None, None, "Builder should be defined as inner class")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001044
1045 has_build = False
1046 for m in clazz.methods:
1047 if m.name == "build":
1048 has_build = True
1049 continue
1050
1051 if m.name.startswith("get"): continue
1052 if m.name.startswith("clear"): continue
1053
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001054 if m.name.startswith("with"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001055 warn(clazz, m, None, "Builder methods names should use setFoo() style")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001056
1057 if m.name.startswith("set"):
1058 if not m.typ.endswith(clazz.fullname):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001059 warn(clazz, m, "M4", "Methods must return the builder object")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001060
1061 if not has_build:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001062 warn(clazz, None, None, "Missing build() method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001063
1064
1065def verify_aidl(clazz):
1066 """Catch people exposing raw AIDL."""
Adrian Roosb787c182019-01-03 18:54:33 +01001067 if clazz.extends == "android.os.Binder" or clazz.implements == "android.os.IInterface":
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001068 error(clazz, None, None, "Raw AIDL interfaces must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001069
1070
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001071def verify_internal(clazz):
1072 """Catch people exposing internal classes."""
1073 if clazz.pkg.name.startswith("com.android"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001074 error(clazz, None, None, "Internal classes must not be exposed")
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001075
1076
1077def verify_layering(clazz):
1078 """Catch package layering violations.
1079 For example, something in android.os depending on android.app."""
1080 ranking = [
1081 ["android.service","android.accessibilityservice","android.inputmethodservice","android.printservice","android.appwidget","android.webkit","android.preference","android.gesture","android.print"],
1082 "android.app",
1083 "android.widget",
1084 "android.view",
1085 "android.animation",
1086 "android.provider",
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001087 ["android.content","android.graphics.drawable"],
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001088 "android.database",
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001089 "android.text",
Siyamed Sinir0a2e15d2018-09-13 16:06:59 -07001090 "android.graphics",
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001091 "android.os",
1092 "android.util"
1093 ]
1094
1095 def rank(p):
1096 for i in range(len(ranking)):
1097 if isinstance(ranking[i], list):
1098 for j in ranking[i]:
1099 if p.startswith(j): return i
1100 else:
1101 if p.startswith(ranking[i]): return i
1102
1103 cr = rank(clazz.pkg.name)
1104 if cr is None: return
1105
1106 for f in clazz.fields:
1107 ir = rank(f.typ)
1108 if ir and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001109 warn(clazz, f, "FW6", "Field type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001110
1111 for m in clazz.methods:
1112 ir = rank(m.typ)
1113 if ir and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001114 warn(clazz, m, "FW6", "Method return type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001115 for arg in m.args:
1116 ir = rank(arg)
1117 if ir and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001118 warn(clazz, m, "FW6", "Method argument type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001119
1120
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001121def verify_boolean(clazz):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001122 """Verifies that boolean accessors are named correctly.
1123 For example, hasFoo() and setHasFoo()."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001124
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001125 def is_get(m): return len(m.args) == 0 and m.typ == "boolean"
1126 def is_set(m): return len(m.args) == 1 and m.args[0] == "boolean"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001127
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001128 gets = [ m for m in clazz.methods if is_get(m) ]
1129 sets = [ m for m in clazz.methods if is_set(m) ]
1130
1131 def error_if_exists(methods, trigger, expected, actual):
1132 for m in methods:
1133 if m.name == actual:
1134 error(clazz, m, "M6", "Symmetric method for %s must be named %s" % (trigger, expected))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001135
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001136 for m in clazz.methods:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001137 if is_get(m):
1138 if re.match("is[A-Z]", m.name):
1139 target = m.name[2:]
1140 expected = "setIs" + target
1141 error_if_exists(sets, m.name, expected, "setHas" + target)
1142 elif re.match("has[A-Z]", m.name):
1143 target = m.name[3:]
1144 expected = "setHas" + target
1145 error_if_exists(sets, m.name, expected, "setIs" + target)
1146 error_if_exists(sets, m.name, expected, "set" + target)
1147 elif re.match("get[A-Z]", m.name):
1148 target = m.name[3:]
1149 expected = "set" + target
1150 error_if_exists(sets, m.name, expected, "setIs" + target)
1151 error_if_exists(sets, m.name, expected, "setHas" + target)
1152
1153 if is_set(m):
1154 if re.match("set[A-Z]", m.name):
1155 target = m.name[3:]
1156 expected = "get" + target
1157 error_if_exists(sets, m.name, expected, "is" + target)
1158 error_if_exists(sets, m.name, expected, "has" + target)
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001159
1160
1161def verify_collections(clazz):
1162 """Verifies that collection types are interfaces."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001163 if clazz.fullname == "android.os.Bundle": return
1164
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001165 bad = ["java.util.Vector", "java.util.LinkedList", "java.util.ArrayList", "java.util.Stack",
1166 "java.util.HashMap", "java.util.HashSet", "android.util.ArraySet", "android.util.ArrayMap"]
1167 for m in clazz.methods:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001168 if m.typ in bad:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001169 error(clazz, m, "CL2", "Return type is concrete collection; must be higher-level interface")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001170 for arg in m.args:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001171 if arg in bad:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001172 error(clazz, m, "CL2", "Argument is concrete collection; must be higher-level interface")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001173
1174
1175def verify_flags(clazz):
1176 """Verifies that flags are non-overlapping."""
1177 known = collections.defaultdict(int)
1178 for f in clazz.fields:
1179 if "FLAG_" in f.name:
1180 try:
1181 val = int(f.value)
1182 except:
1183 continue
1184
1185 scope = f.name[0:f.name.index("FLAG_")]
1186 if val & known[scope]:
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001187 warn(clazz, f, "C1", "Found overlapping flag constant value")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001188 known[scope] |= val
1189
1190
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001191def verify_exception(clazz):
1192 """Verifies that methods don't throw generic exceptions."""
1193 for m in clazz.methods:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001194 for t in m.throws:
1195 if t in ["java.lang.Exception", "java.lang.Throwable", "java.lang.Error"]:
1196 error(clazz, m, "S1", "Methods must not throw generic exceptions")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001197
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001198 if t in ["android.os.RemoteException"]:
1199 if clazz.name == "android.content.ContentProviderClient": continue
1200 if clazz.name == "android.os.Binder": continue
1201 if clazz.name == "android.os.IBinder": continue
Jeff Sharkey331279b2016-02-29 16:02:02 -07001202
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001203 error(clazz, m, "FW9", "Methods calling into system server should rethrow RemoteException as RuntimeException")
1204
1205 if len(m.args) == 0 and t in ["java.lang.IllegalArgumentException", "java.lang.NullPointerException"]:
1206 warn(clazz, m, "S1", "Methods taking no arguments should throw IllegalStateException")
Jeff Sharkey331279b2016-02-29 16:02:02 -07001207
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001208
1209def verify_google(clazz):
1210 """Verifies that APIs never reference Google."""
1211
1212 if re.search("google", clazz.raw, re.IGNORECASE):
1213 error(clazz, None, None, "Must never reference Google")
1214
1215 test = []
1216 test.extend(clazz.ctors)
1217 test.extend(clazz.fields)
1218 test.extend(clazz.methods)
1219
1220 for t in test:
1221 if re.search("google", t.raw, re.IGNORECASE):
1222 error(clazz, t, None, "Must never reference Google")
1223
1224
1225def verify_bitset(clazz):
1226 """Verifies that we avoid using heavy BitSet."""
1227
1228 for f in clazz.fields:
1229 if f.typ == "java.util.BitSet":
1230 error(clazz, f, None, "Field type must not be heavy BitSet")
1231
1232 for m in clazz.methods:
1233 if m.typ == "java.util.BitSet":
1234 error(clazz, m, None, "Return type must not be heavy BitSet")
1235 for arg in m.args:
1236 if arg == "java.util.BitSet":
1237 error(clazz, m, None, "Argument type must not be heavy BitSet")
1238
1239
1240def verify_manager(clazz):
1241 """Verifies that FooManager is only obtained from Context."""
1242
1243 if not clazz.name.endswith("Manager"): return
1244
1245 for c in clazz.ctors:
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001246 error(clazz, c, None, "Managers must always be obtained from Context; no direct constructors")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001247
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001248 for m in clazz.methods:
1249 if m.typ == clazz.fullname:
1250 error(clazz, m, None, "Managers must always be obtained from Context")
1251
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001252
1253def verify_boxed(clazz):
1254 """Verifies that methods avoid boxed primitives."""
1255
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001256 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 -08001257
1258 for c in clazz.ctors:
1259 for arg in c.args:
1260 if arg in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001261 error(clazz, c, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001262
1263 for f in clazz.fields:
1264 if f.typ in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001265 error(clazz, f, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001266
1267 for m in clazz.methods:
1268 if m.typ in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001269 error(clazz, m, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001270 for arg in m.args:
1271 if arg in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001272 error(clazz, m, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001273
1274
1275def verify_static_utils(clazz):
1276 """Verifies that helper classes can't be constructed."""
1277 if clazz.fullname.startswith("android.opengl"): return
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001278 if clazz.fullname.startswith("android.R"): return
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001279
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001280 # Only care about classes with default constructors
1281 if len(clazz.ctors) == 1 and len(clazz.ctors[0].args) == 0:
1282 test = []
1283 test.extend(clazz.fields)
1284 test.extend(clazz.methods)
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001285
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001286 if len(test) == 0: return
1287 for t in test:
1288 if "static" not in t.split:
1289 return
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001290
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001291 error(clazz, None, None, "Fully-static utility classes must not have constructor")
1292
1293
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001294def verify_overload_args(clazz):
1295 """Verifies that method overloads add new arguments at the end."""
1296 if clazz.fullname.startswith("android.opengl"): return
1297
1298 overloads = collections.defaultdict(list)
1299 for m in clazz.methods:
1300 if "deprecated" in m.split: continue
1301 overloads[m.name].append(m)
1302
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001303 for name, methods in overloads.items():
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001304 if len(methods) <= 1: continue
1305
1306 # Look for arguments common across all overloads
1307 def cluster(args):
1308 count = collections.defaultdict(int)
1309 res = set()
1310 for i in range(len(args)):
1311 a = args[i]
1312 res.add("%s#%d" % (a, count[a]))
1313 count[a] += 1
1314 return res
1315
1316 common_args = cluster(methods[0].args)
1317 for m in methods:
1318 common_args = common_args & cluster(m.args)
1319
1320 if len(common_args) == 0: continue
1321
1322 # Require that all common arguments are present at start of signature
1323 locked_sig = None
1324 for m in methods:
1325 sig = m.args[0:len(common_args)]
1326 if not common_args.issubset(cluster(sig)):
1327 warn(clazz, m, "M2", "Expected common arguments [%s] at beginning of overloaded method" % (", ".join(common_args)))
1328 elif not locked_sig:
1329 locked_sig = sig
1330 elif locked_sig != sig:
1331 error(clazz, m, "M2", "Expected consistent argument ordering between overloads: %s..." % (", ".join(locked_sig)))
1332
1333
1334def verify_callback_handlers(clazz):
1335 """Verifies that methods adding listener/callback have overload
1336 for specifying delivery thread."""
1337
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001338 # Ignore UI packages which assume main thread
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001339 skip = [
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001340 "animation",
1341 "view",
1342 "graphics",
1343 "transition",
1344 "widget",
1345 "webkit",
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001346 ]
1347 for s in skip:
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001348 if s in clazz.pkg.name_path: return
1349 if s in clazz.extends_path: return
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001350
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001351 # Ignore UI classes which assume main thread
1352 if "app" in clazz.pkg.name_path or "app" in clazz.extends_path:
1353 for s in ["ActionBar","Dialog","Application","Activity","Fragment","Loader"]:
1354 if s in clazz.fullname: return
1355 if "content" in clazz.pkg.name_path or "content" in clazz.extends_path:
1356 for s in ["Loader"]:
1357 if s in clazz.fullname: return
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001358
1359 found = {}
1360 by_name = collections.defaultdict(list)
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001361 examine = clazz.ctors + clazz.methods
1362 for m in examine:
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001363 if m.name.startswith("unregister"): continue
1364 if m.name.startswith("remove"): continue
1365 if re.match("on[A-Z]+", m.name): continue
1366
1367 by_name[m.name].append(m)
1368
1369 for a in m.args:
1370 if a.endswith("Listener") or a.endswith("Callback") or a.endswith("Callbacks"):
1371 found[m.name] = m
1372
1373 for f in found.values():
1374 takes_handler = False
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001375 takes_exec = False
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001376 for m in by_name[f.name]:
1377 if "android.os.Handler" in m.args:
1378 takes_handler = True
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001379 if "java.util.concurrent.Executor" in m.args:
1380 takes_exec = True
1381 if not takes_exec:
1382 warn(clazz, f, "L1", "Registration methods should have overload that accepts delivery Executor")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001383
1384
1385def verify_context_first(clazz):
1386 """Verifies that methods accepting a Context keep it the first argument."""
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001387 examine = clazz.ctors + clazz.methods
1388 for m in examine:
1389 if len(m.args) > 1 and m.args[0] != "android.content.Context":
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001390 if "android.content.Context" in m.args[1:]:
1391 error(clazz, m, "M3", "Context is distinct, so it must be the first argument")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001392 if len(m.args) > 1 and m.args[0] != "android.content.ContentResolver":
1393 if "android.content.ContentResolver" in m.args[1:]:
1394 error(clazz, m, "M3", "ContentResolver is distinct, so it must be the first argument")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001395
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001396
1397def verify_listener_last(clazz):
1398 """Verifies that methods accepting a Listener or Callback keep them as last arguments."""
1399 examine = clazz.ctors + clazz.methods
1400 for m in examine:
1401 if "Listener" in m.name or "Callback" in m.name: continue
1402 found = False
1403 for a in m.args:
1404 if a.endswith("Callback") or a.endswith("Callbacks") or a.endswith("Listener"):
1405 found = True
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001406 elif found:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001407 warn(clazz, m, "M3", "Listeners should always be at end of argument list")
1408
1409
1410def verify_resource_names(clazz):
1411 """Verifies that resource names have consistent case."""
1412 if not re.match("android\.R\.[a-z]+", clazz.fullname): return
1413
1414 # Resources defined by files are foo_bar_baz
1415 if clazz.name in ["anim","animator","color","dimen","drawable","interpolator","layout","transition","menu","mipmap","string","plurals","raw","xml"]:
1416 for f in clazz.fields:
Jeff Sharkeyeff9e222018-09-27 16:29:25 -06001417 if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue
1418 if f.name.startswith("config_"):
1419 error(clazz, f, None, "Expected config name to be config_fooBarBaz style")
1420
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001421 if re.match("[a-z1-9_]+$", f.name): continue
1422 error(clazz, f, None, "Expected resource name in this class to be foo_bar_baz style")
1423
1424 # Resources defined inside files are fooBarBaz
1425 if clazz.name in ["array","attr","id","bool","fraction","integer"]:
1426 for f in clazz.fields:
1427 if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue
1428 if re.match("layout_[a-z][a-zA-Z1-9]*$", f.name): continue
1429 if re.match("state_[a-z_]*$", f.name): continue
1430
1431 if re.match("[a-z][a-zA-Z1-9]*$", f.name): continue
1432 error(clazz, f, "C7", "Expected resource name in this class to be fooBarBaz style")
1433
1434 # Styles are FooBar_Baz
1435 if clazz.name in ["style"]:
1436 for f in clazz.fields:
1437 if re.match("[A-Z][A-Za-z1-9]+(_[A-Z][A-Za-z1-9]+?)*$", f.name): continue
1438 error(clazz, f, "C7", "Expected resource name in this class to be FooBar_Baz style")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001439
1440
Jeff Sharkey331279b2016-02-29 16:02:02 -07001441def verify_files(clazz):
1442 """Verifies that methods accepting File also accept streams."""
1443
1444 has_file = set()
1445 has_stream = set()
1446
1447 test = []
1448 test.extend(clazz.ctors)
1449 test.extend(clazz.methods)
1450
1451 for m in test:
1452 if "java.io.File" in m.args:
1453 has_file.add(m)
1454 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:
1455 has_stream.add(m.name)
1456
1457 for m in has_file:
1458 if m.name not in has_stream:
1459 warn(clazz, m, "M10", "Methods accepting File should also accept FileDescriptor or streams")
1460
1461
Jeff Sharkey70168dd2016-03-30 21:47:16 -06001462def verify_manager_list(clazz):
1463 """Verifies that managers return List<? extends Parcelable> instead of arrays."""
1464
1465 if not clazz.name.endswith("Manager"): return
1466
1467 for m in clazz.methods:
1468 if m.typ.startswith("android.") and m.typ.endswith("[]"):
1469 warn(clazz, m, None, "Methods should return List<? extends Parcelable> instead of Parcelable[] to support ParceledListSlice under the hood")
1470
1471
Jeff Sharkey26c80902016-12-21 13:41:17 -07001472def verify_abstract_inner(clazz):
1473 """Verifies that abstract inner classes are static."""
1474
1475 if re.match(".+?\.[A-Z][^\.]+\.[A-Z]", clazz.fullname):
Adrian Roosb787c182019-01-03 18:54:33 +01001476 if "abstract" in clazz.split and "static" not in clazz.split:
Jeff Sharkey26c80902016-12-21 13:41:17 -07001477 warn(clazz, None, None, "Abstract inner classes should be static to improve testability")
1478
1479
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001480def verify_runtime_exceptions(clazz):
1481 """Verifies that runtime exceptions aren't listed in throws."""
1482
1483 banned = [
1484 "java.lang.NullPointerException",
1485 "java.lang.ClassCastException",
1486 "java.lang.IndexOutOfBoundsException",
1487 "java.lang.reflect.UndeclaredThrowableException",
1488 "java.lang.reflect.MalformedParametersException",
1489 "java.lang.reflect.MalformedParameterizedTypeException",
1490 "java.lang.invoke.WrongMethodTypeException",
1491 "java.lang.EnumConstantNotPresentException",
1492 "java.lang.IllegalMonitorStateException",
1493 "java.lang.SecurityException",
1494 "java.lang.UnsupportedOperationException",
1495 "java.lang.annotation.AnnotationTypeMismatchException",
1496 "java.lang.annotation.IncompleteAnnotationException",
1497 "java.lang.TypeNotPresentException",
1498 "java.lang.IllegalStateException",
1499 "java.lang.ArithmeticException",
1500 "java.lang.IllegalArgumentException",
1501 "java.lang.ArrayStoreException",
1502 "java.lang.NegativeArraySizeException",
1503 "java.util.MissingResourceException",
1504 "java.util.EmptyStackException",
1505 "java.util.concurrent.CompletionException",
1506 "java.util.concurrent.RejectedExecutionException",
1507 "java.util.IllformedLocaleException",
1508 "java.util.ConcurrentModificationException",
1509 "java.util.NoSuchElementException",
1510 "java.io.UncheckedIOException",
1511 "java.time.DateTimeException",
1512 "java.security.ProviderException",
1513 "java.nio.BufferUnderflowException",
1514 "java.nio.BufferOverflowException",
1515 ]
1516
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001517 examine = clazz.ctors + clazz.methods
1518 for m in examine:
1519 for t in m.throws:
1520 if t in banned:
1521 error(clazz, m, None, "Methods must not mention RuntimeException subclasses in throws clauses")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001522
1523
1524def verify_error(clazz):
1525 """Verifies that we always use Exception instead of Error."""
1526 if not clazz.extends: return
1527 if clazz.extends.endswith("Error"):
1528 error(clazz, None, None, "Trouble must be reported through an Exception, not Error")
1529 if clazz.extends.endswith("Exception") and not clazz.name.endswith("Exception"):
1530 error(clazz, None, None, "Exceptions must be named FooException")
1531
1532
1533def verify_units(clazz):
1534 """Verifies that we use consistent naming for units."""
1535
1536 # If we find K, recommend replacing with V
1537 bad = {
1538 "Ns": "Nanos",
1539 "Ms": "Millis or Micros",
1540 "Sec": "Seconds", "Secs": "Seconds",
1541 "Hr": "Hours", "Hrs": "Hours",
1542 "Mo": "Months", "Mos": "Months",
1543 "Yr": "Years", "Yrs": "Years",
1544 "Byte": "Bytes", "Space": "Bytes",
1545 }
1546
1547 for m in clazz.methods:
1548 if m.typ not in ["short","int","long"]: continue
1549 for k, v in bad.iteritems():
1550 if m.name.endswith(k):
1551 error(clazz, m, None, "Expected method name units to be " + v)
1552 if m.name.endswith("Nanos") or m.name.endswith("Micros"):
1553 warn(clazz, m, None, "Returned time values are strongly encouraged to be in milliseconds unless you need the extra precision")
1554 if m.name.endswith("Seconds"):
1555 error(clazz, m, None, "Returned time values must be in milliseconds")
1556
1557 for m in clazz.methods:
1558 typ = m.typ
1559 if typ == "void":
1560 if len(m.args) != 1: continue
1561 typ = m.args[0]
1562
1563 if m.name.endswith("Fraction") and typ != "float":
1564 error(clazz, m, None, "Fractions must use floats")
1565 if m.name.endswith("Percentage") and typ != "int":
1566 error(clazz, m, None, "Percentage must use ints")
1567
1568
1569def verify_closable(clazz):
1570 """Verifies that classes are AutoClosable."""
Adrian Roosb787c182019-01-03 18:54:33 +01001571 if clazz.implements == "java.lang.AutoCloseable": return
1572 if clazz.implements == "java.io.Closeable": return
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001573
1574 for m in clazz.methods:
1575 if len(m.args) > 0: continue
1576 if m.name in ["close","release","destroy","finish","finalize","disconnect","shutdown","stop","free","quit"]:
1577 warn(clazz, m, None, "Classes that release resources should implement AutoClosable and CloseGuard")
1578 return
1579
1580
Jake Wharton9e6738f2017-08-23 11:59:55 -04001581def verify_member_name_not_kotlin_keyword(clazz):
1582 """Prevent method names which are keywords in Kotlin."""
1583
1584 # https://kotlinlang.org/docs/reference/keyword-reference.html#hard-keywords
1585 # This list does not include Java keywords as those are already impossible to use.
1586 keywords = [
1587 'as',
1588 'fun',
1589 'in',
1590 'is',
1591 'object',
1592 'typealias',
1593 'val',
1594 'var',
1595 'when',
1596 ]
1597
1598 for m in clazz.methods:
1599 if m.name in keywords:
1600 error(clazz, m, None, "Method name must not be a Kotlin keyword")
1601 for f in clazz.fields:
1602 if f.name in keywords:
1603 error(clazz, f, None, "Field name must not be a Kotlin keyword")
1604
1605
1606def verify_method_name_not_kotlin_operator(clazz):
1607 """Warn about method names which become operators in Kotlin."""
1608
1609 binary = set()
1610
1611 def unique_binary_op(m, op):
1612 if op in binary:
1613 error(clazz, m, None, "Only one of '{0}' and '{0}Assign' methods should be present for Kotlin".format(op))
1614 binary.add(op)
1615
1616 for m in clazz.methods:
1617 if 'static' in m.split:
1618 continue
1619
1620 # https://kotlinlang.org/docs/reference/operator-overloading.html#unary-prefix-operators
1621 if m.name in ['unaryPlus', 'unaryMinus', 'not'] and len(m.args) == 0:
1622 warn(clazz, m, None, "Method can be invoked as a unary operator from Kotlin")
1623
1624 # https://kotlinlang.org/docs/reference/operator-overloading.html#increments-and-decrements
1625 if m.name in ['inc', 'dec'] and len(m.args) == 0 and m.typ != 'void':
1626 # This only applies if the return type is the same or a subtype of the enclosing class, but we have no
1627 # practical way of checking that relationship here.
1628 warn(clazz, m, None, "Method can be invoked as a pre/postfix inc/decrement operator from Kotlin")
1629
1630 # https://kotlinlang.org/docs/reference/operator-overloading.html#arithmetic
1631 if m.name in ['plus', 'minus', 'times', 'div', 'rem', 'mod', 'rangeTo'] and len(m.args) == 1:
1632 warn(clazz, m, None, "Method can be invoked as a binary operator from Kotlin")
1633 unique_binary_op(m, m.name)
1634
1635 # https://kotlinlang.org/docs/reference/operator-overloading.html#in
1636 if m.name == 'contains' and len(m.args) == 1 and m.typ == 'boolean':
1637 warn(clazz, m, None, "Method can be invoked as a 'in' operator from Kotlin")
1638
1639 # https://kotlinlang.org/docs/reference/operator-overloading.html#indexed
1640 if (m.name == 'get' and len(m.args) > 0) or (m.name == 'set' and len(m.args) > 1):
1641 warn(clazz, m, None, "Method can be invoked with an indexing operator from Kotlin")
1642
1643 # https://kotlinlang.org/docs/reference/operator-overloading.html#invoke
1644 if m.name == 'invoke':
1645 warn(clazz, m, None, "Method can be invoked with function call syntax from Kotlin")
1646
1647 # https://kotlinlang.org/docs/reference/operator-overloading.html#assignments
1648 if m.name in ['plusAssign', 'minusAssign', 'timesAssign', 'divAssign', 'remAssign', 'modAssign'] \
1649 and len(m.args) == 1 \
1650 and m.typ == 'void':
1651 warn(clazz, m, None, "Method can be invoked as a compound assignment operator from Kotlin")
1652 unique_binary_op(m, m.name[:-6]) # Remove 'Assign' suffix
1653
1654
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001655def verify_collections_over_arrays(clazz):
1656 """Warn that [] should be Collections."""
1657
Adrian Roosb787c182019-01-03 18:54:33 +01001658 if "@interface" in clazz.split:
1659 return
1660
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001661 safe = ["java.lang.String[]","byte[]","short[]","int[]","long[]","float[]","double[]","boolean[]","char[]"]
1662 for m in clazz.methods:
1663 if m.typ.endswith("[]") and m.typ not in safe:
1664 warn(clazz, m, None, "Method should return Collection<> (or subclass) instead of raw array")
1665 for arg in m.args:
1666 if arg.endswith("[]") and arg not in safe:
1667 warn(clazz, m, None, "Method argument should be Collection<> (or subclass) instead of raw array")
1668
1669
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001670def verify_user_handle(clazz):
1671 """Methods taking UserHandle should be ForUser or AsUser."""
1672 if clazz.name.endswith("Listener") or clazz.name.endswith("Callback") or clazz.name.endswith("Callbacks"): return
1673 if clazz.fullname == "android.app.admin.DeviceAdminReceiver": return
1674 if clazz.fullname == "android.content.pm.LauncherApps": return
1675 if clazz.fullname == "android.os.UserHandle": return
1676 if clazz.fullname == "android.os.UserManager": return
1677
1678 for m in clazz.methods:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001679 if re.match("on[A-Z]+", m.name): continue
Jeff Sharkeya8e5df02018-11-27 17:33:42 -07001680
1681 has_arg = "android.os.UserHandle" in m.args
1682 has_name = m.name.endswith("AsUser") or m.name.endswith("ForUser")
1683
1684 if clazz.fullname.endswith("Manager") and has_arg:
1685 warn(clazz, m, None, "When a method overload is needed to target a specific "
1686 "UserHandle, callers should be directed to use "
1687 "Context.createPackageContextAsUser() and re-obtain the relevant "
1688 "Manager, and no new API should be added")
1689 elif has_arg and not has_name:
1690 warn(clazz, m, None, "Method taking UserHandle should be named 'doFooAsUser' "
1691 "or 'queryFooForUser'")
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001692
1693
1694def verify_params(clazz):
1695 """Parameter classes should be 'Params'."""
1696 if clazz.name.endswith("Params"): return
1697 if clazz.fullname == "android.app.ActivityOptions": return
1698 if clazz.fullname == "android.app.BroadcastOptions": return
1699 if clazz.fullname == "android.os.Bundle": return
1700 if clazz.fullname == "android.os.BaseBundle": return
1701 if clazz.fullname == "android.os.PersistableBundle": return
1702
1703 bad = ["Param","Parameter","Parameters","Args","Arg","Argument","Arguments","Options","Bundle"]
1704 for b in bad:
1705 if clazz.name.endswith(b):
1706 error(clazz, None, None, "Classes holding a set of parameters should be called 'FooParams'")
1707
1708
1709def verify_services(clazz):
1710 """Service name should be FOO_BAR_SERVICE = 'foo_bar'."""
1711 if clazz.fullname != "android.content.Context": return
1712
1713 for f in clazz.fields:
1714 if f.typ != "java.lang.String": continue
1715 found = re.match(r"([A-Z_]+)_SERVICE", f.name)
1716 if found:
1717 expected = found.group(1).lower()
1718 if f.value != expected:
1719 error(clazz, f, "C4", "Inconsistent service value; expected '%s'" % (expected))
1720
1721
1722def verify_tense(clazz):
1723 """Verify tenses of method names."""
1724 if clazz.fullname.startswith("android.opengl"): return
1725
1726 for m in clazz.methods:
1727 if m.name.endswith("Enable"):
1728 warn(clazz, m, None, "Unexpected tense; probably meant 'enabled'")
1729
1730
Jeff Sharkeyee21f692018-02-16 13:29:29 -07001731def verify_icu(clazz):
1732 """Verifies that richer ICU replacements are used."""
1733 better = {
1734 "java.util.TimeZone": "android.icu.util.TimeZone",
1735 "java.util.Calendar": "android.icu.util.Calendar",
1736 "java.util.Locale": "android.icu.util.ULocale",
1737 "java.util.ResourceBundle": "android.icu.util.UResourceBundle",
1738 "java.util.SimpleTimeZone": "android.icu.util.SimpleTimeZone",
1739 "java.util.StringTokenizer": "android.icu.util.StringTokenizer",
1740 "java.util.GregorianCalendar": "android.icu.util.GregorianCalendar",
1741 "java.lang.Character": "android.icu.lang.UCharacter",
1742 "java.text.BreakIterator": "android.icu.text.BreakIterator",
1743 "java.text.Collator": "android.icu.text.Collator",
1744 "java.text.DecimalFormatSymbols": "android.icu.text.DecimalFormatSymbols",
1745 "java.text.NumberFormat": "android.icu.text.NumberFormat",
1746 "java.text.DateFormatSymbols": "android.icu.text.DateFormatSymbols",
1747 "java.text.DateFormat": "android.icu.text.DateFormat",
1748 "java.text.SimpleDateFormat": "android.icu.text.SimpleDateFormat",
1749 "java.text.MessageFormat": "android.icu.text.MessageFormat",
1750 "java.text.DecimalFormat": "android.icu.text.DecimalFormat",
1751 }
1752
1753 for m in clazz.ctors + clazz.methods:
1754 types = []
1755 types.extend(m.typ)
1756 types.extend(m.args)
1757 for arg in types:
1758 if arg in better:
1759 warn(clazz, m, None, "Type %s should be replaced with richer ICU type %s" % (arg, better[arg]))
1760
1761
1762def verify_clone(clazz):
1763 """Verify that clone() isn't implemented; see EJ page 61."""
1764 for m in clazz.methods:
1765 if m.name == "clone":
1766 error(clazz, m, None, "Provide an explicit copy constructor instead of implementing clone()")
1767
1768
Jeff Sharkeyeff9e222018-09-27 16:29:25 -06001769def verify_pfd(clazz):
1770 """Verify that android APIs use PFD over FD."""
1771 examine = clazz.ctors + clazz.methods
1772 for m in examine:
1773 if m.typ == "java.io.FileDescriptor":
1774 error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
1775 if m.typ == "int":
1776 if "Fd" in m.name or "FD" in m.name or "FileDescriptor" in m.name:
1777 error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
1778 for arg in m.args:
1779 if arg == "java.io.FileDescriptor":
1780 error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
1781
1782 for f in clazz.fields:
1783 if f.typ == "java.io.FileDescriptor":
1784 error(clazz, f, "FW11", "Must use ParcelFileDescriptor")
1785
1786
1787def verify_numbers(clazz):
1788 """Discourage small numbers types like short and byte."""
1789
1790 discouraged = ["short","byte"]
1791
1792 for c in clazz.ctors:
1793 for arg in c.args:
1794 if arg in discouraged:
1795 warn(clazz, c, "FW12", "Should avoid odd sized primitives; use int instead")
1796
1797 for f in clazz.fields:
1798 if f.typ in discouraged:
1799 warn(clazz, f, "FW12", "Should avoid odd sized primitives; use int instead")
1800
1801 for m in clazz.methods:
1802 if m.typ in discouraged:
1803 warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead")
1804 for arg in m.args:
1805 if arg in discouraged:
1806 warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead")
1807
1808
1809def verify_singleton(clazz):
1810 """Catch singleton objects with constructors."""
1811
1812 singleton = False
1813 for m in clazz.methods:
1814 if m.name.startswith("get") and m.name.endswith("Instance") and " static " in m.raw:
1815 singleton = True
1816
1817 if singleton:
1818 for c in clazz.ctors:
1819 error(clazz, c, None, "Singleton classes should use getInstance() methods")
1820
1821
1822
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06001823def is_interesting(clazz):
1824 """Test if given class is interesting from an Android PoV."""
1825
1826 if clazz.pkg.name.startswith("java"): return False
1827 if clazz.pkg.name.startswith("junit"): return False
1828 if clazz.pkg.name.startswith("org.apache"): return False
1829 if clazz.pkg.name.startswith("org.xml"): return False
1830 if clazz.pkg.name.startswith("org.json"): return False
1831 if clazz.pkg.name.startswith("org.w3c"): return False
1832 if clazz.pkg.name.startswith("android.icu."): return False
1833 return True
1834
1835
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001836def examine_clazz(clazz):
1837 """Find all style issues in the given class."""
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001838
1839 notice(clazz)
1840
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06001841 if not is_interesting(clazz): return
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001842
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001843 verify_constants(clazz)
1844 verify_enums(clazz)
1845 verify_class_names(clazz)
1846 verify_method_names(clazz)
1847 verify_callbacks(clazz)
1848 verify_listeners(clazz)
1849 verify_actions(clazz)
1850 verify_extras(clazz)
1851 verify_equals(clazz)
1852 verify_parcelable(clazz)
1853 verify_protected(clazz)
1854 verify_fields(clazz)
1855 verify_register(clazz)
1856 verify_sync(clazz)
1857 verify_intent_builder(clazz)
1858 verify_helper_classes(clazz)
1859 verify_builder(clazz)
1860 verify_aidl(clazz)
1861 verify_internal(clazz)
1862 verify_layering(clazz)
1863 verify_boolean(clazz)
1864 verify_collections(clazz)
1865 verify_flags(clazz)
1866 verify_exception(clazz)
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07001867 if not ALLOW_GOOGLE: verify_google(clazz)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001868 verify_bitset(clazz)
1869 verify_manager(clazz)
1870 verify_boxed(clazz)
1871 verify_static_utils(clazz)
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001872 # verify_overload_args(clazz)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001873 verify_callback_handlers(clazz)
1874 verify_context_first(clazz)
1875 verify_listener_last(clazz)
1876 verify_resource_names(clazz)
Jeff Sharkey331279b2016-02-29 16:02:02 -07001877 verify_files(clazz)
Jeff Sharkey70168dd2016-03-30 21:47:16 -06001878 verify_manager_list(clazz)
Jeff Sharkey26c80902016-12-21 13:41:17 -07001879 verify_abstract_inner(clazz)
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001880 verify_runtime_exceptions(clazz)
1881 verify_error(clazz)
1882 verify_units(clazz)
1883 verify_closable(clazz)
Jake Wharton9e6738f2017-08-23 11:59:55 -04001884 verify_member_name_not_kotlin_keyword(clazz)
1885 verify_method_name_not_kotlin_operator(clazz)
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001886 verify_collections_over_arrays(clazz)
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001887 verify_user_handle(clazz)
1888 verify_params(clazz)
1889 verify_services(clazz)
1890 verify_tense(clazz)
Jeff Sharkeyee21f692018-02-16 13:29:29 -07001891 verify_icu(clazz)
1892 verify_clone(clazz)
Jeff Sharkeyeff9e222018-09-27 16:29:25 -06001893 verify_pfd(clazz)
1894 verify_numbers(clazz)
1895 verify_singleton(clazz)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001896
1897
Adrian Roos038a0292018-12-19 17:11:21 +01001898def examine_stream(stream, base_stream=None, in_classes_with_base=[], out_classes_with_base=None):
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001899 """Find all style issues in the given API stream."""
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001900 global failures, noticed
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001901 failures = {}
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001902 noticed = {}
Adrian Roos038a0292018-12-19 17:11:21 +01001903 _parse_stream(stream, examine_clazz, base_f=base_stream,
1904 in_classes_with_base=in_classes_with_base,
1905 out_classes_with_base=out_classes_with_base)
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001906 return (failures, noticed)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001907
1908
1909def examine_api(api):
1910 """Find all style issues in the given parsed API."""
1911 global failures
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001912 failures = {}
1913 for key in sorted(api.keys()):
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001914 examine_clazz(api[key])
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001915 return failures
1916
1917
Jeff Sharkey037458a2014-09-04 15:46:20 -07001918def verify_compat(cur, prev):
1919 """Find any incompatible API changes between two levels."""
1920 global failures
1921
1922 def class_exists(api, test):
1923 return test.fullname in api
1924
1925 def ctor_exists(api, clazz, test):
1926 for m in clazz.ctors:
1927 if m.ident == test.ident: return True
1928 return False
1929
1930 def all_methods(api, clazz):
1931 methods = list(clazz.methods)
1932 if clazz.extends is not None:
1933 methods.extend(all_methods(api, api[clazz.extends]))
1934 return methods
1935
1936 def method_exists(api, clazz, test):
1937 methods = all_methods(api, clazz)
1938 for m in methods:
1939 if m.ident == test.ident: return True
1940 return False
1941
1942 def field_exists(api, clazz, test):
1943 for f in clazz.fields:
1944 if f.ident == test.ident: return True
1945 return False
1946
1947 failures = {}
1948 for key in sorted(prev.keys()):
1949 prev_clazz = prev[key]
1950
1951 if not class_exists(cur, prev_clazz):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001952 error(prev_clazz, None, None, "Class removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07001953 continue
1954
1955 cur_clazz = cur[key]
1956
1957 for test in prev_clazz.ctors:
1958 if not ctor_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001959 error(prev_clazz, prev_ctor, None, "Constructor removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07001960
1961 methods = all_methods(prev, prev_clazz)
1962 for test in methods:
1963 if not method_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001964 error(prev_clazz, test, None, "Method removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07001965
1966 for test in prev_clazz.fields:
1967 if not field_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001968 error(prev_clazz, test, None, "Field removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07001969
1970 return failures
1971
1972
Jeff Sharkey8b141b92018-04-11 10:05:44 -06001973def show_deprecations_at_birth(cur, prev):
1974 """Show API deprecations at birth."""
1975 global failures
1976
1977 # Remove all existing things so we're left with new
1978 for prev_clazz in prev.values():
1979 cur_clazz = cur[prev_clazz.fullname]
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06001980 if not is_interesting(cur_clazz): continue
Jeff Sharkey8b141b92018-04-11 10:05:44 -06001981
1982 sigs = { i.ident: i for i in prev_clazz.ctors }
1983 cur_clazz.ctors = [ i for i in cur_clazz.ctors if i.ident not in sigs ]
1984 sigs = { i.ident: i for i in prev_clazz.methods }
1985 cur_clazz.methods = [ i for i in cur_clazz.methods if i.ident not in sigs ]
1986 sigs = { i.ident: i for i in prev_clazz.fields }
1987 cur_clazz.fields = [ i for i in cur_clazz.fields if i.ident not in sigs ]
1988
1989 # Forget about class entirely when nothing new
1990 if len(cur_clazz.ctors) == 0 and len(cur_clazz.methods) == 0 and len(cur_clazz.fields) == 0:
1991 del cur[prev_clazz.fullname]
1992
1993 for clazz in cur.values():
Adrian Roosb787c182019-01-03 18:54:33 +01001994 if "deprecated" in clazz.split and not clazz.fullname in prev:
Jeff Sharkey8b141b92018-04-11 10:05:44 -06001995 error(clazz, None, None, "Found API deprecation at birth")
1996
1997 for i in clazz.ctors + clazz.methods + clazz.fields:
Adrian Roosb787c182019-01-03 18:54:33 +01001998 if "deprecated" in i.split:
Jeff Sharkey8b141b92018-04-11 10:05:44 -06001999 error(clazz, i, None, "Found API deprecation at birth")
2000
2001 print "%s Deprecated at birth %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True),
2002 format(reset=True)))
2003 for f in sorted(failures):
2004 print failures[f]
2005 print
2006
2007
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06002008def show_stats(cur, prev):
2009 """Show API stats."""
2010
2011 stats = collections.defaultdict(int)
2012 for cur_clazz in cur.values():
2013 if not is_interesting(cur_clazz): continue
2014
2015 if cur_clazz.fullname not in prev:
2016 stats['new_classes'] += 1
2017 stats['new_ctors'] += len(cur_clazz.ctors)
2018 stats['new_methods'] += len(cur_clazz.methods)
2019 stats['new_fields'] += len(cur_clazz.fields)
2020 else:
2021 prev_clazz = prev[cur_clazz.fullname]
2022
2023 sigs = { i.ident: i for i in prev_clazz.ctors }
2024 ctors = len([ i for i in cur_clazz.ctors if i.ident not in sigs ])
2025 sigs = { i.ident: i for i in prev_clazz.methods }
2026 methods = len([ i for i in cur_clazz.methods if i.ident not in sigs ])
2027 sigs = { i.ident: i for i in prev_clazz.fields }
2028 fields = len([ i for i in cur_clazz.fields if i.ident not in sigs ])
2029
2030 if ctors + methods + fields > 0:
2031 stats['extend_classes'] += 1
2032 stats['extend_ctors'] += ctors
2033 stats['extend_methods'] += methods
2034 stats['extend_fields'] += fields
2035
2036 print "#", "".join([ k.ljust(20) for k in sorted(stats.keys()) ])
2037 print " ", "".join([ str(stats[k]).ljust(20) for k in sorted(stats.keys()) ])
2038
2039
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002040if __name__ == "__main__":
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002041 parser = argparse.ArgumentParser(description="Enforces common Android public API design \
2042 patterns. It ignores lint messages from a previous API level, if provided.")
2043 parser.add_argument("current.txt", type=argparse.FileType('r'), help="current.txt")
2044 parser.add_argument("previous.txt", nargs='?', type=argparse.FileType('r'), default=None,
2045 help="previous.txt")
Adrian Roos6eb57b02018-12-13 22:08:29 +01002046 parser.add_argument("--base-current", nargs='?', type=argparse.FileType('r'), default=None,
2047 help="The base current.txt to use when examining system-current.txt or"
2048 " test-current.txt")
2049 parser.add_argument("--base-previous", nargs='?', type=argparse.FileType('r'), default=None,
2050 help="The base previous.txt to use when examining system-previous.txt or"
2051 " test-previous.txt")
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002052 parser.add_argument("--no-color", action='store_const', const=True,
2053 help="Disable terminal colors")
2054 parser.add_argument("--allow-google", action='store_const', const=True,
2055 help="Allow references to Google")
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002056 parser.add_argument("--show-noticed", action='store_const', const=True,
2057 help="Show API changes noticed")
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002058 parser.add_argument("--show-deprecations-at-birth", action='store_const', const=True,
2059 help="Show API deprecations at birth")
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06002060 parser.add_argument("--show-stats", action='store_const', const=True,
2061 help="Show API stats")
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002062 args = vars(parser.parse_args())
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002063
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002064 if args['no_color']:
2065 USE_COLOR = False
2066
2067 if args['allow_google']:
2068 ALLOW_GOOGLE = True
2069
2070 current_file = args['current.txt']
Adrian Roos6eb57b02018-12-13 22:08:29 +01002071 base_current_file = args['base_current']
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002072 previous_file = args['previous.txt']
Adrian Roos6eb57b02018-12-13 22:08:29 +01002073 base_previous_file = args['base_previous']
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002074
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002075 if args['show_deprecations_at_birth']:
2076 with current_file as f:
2077 cur = _parse_stream(f)
2078 with previous_file as f:
2079 prev = _parse_stream(f)
2080 show_deprecations_at_birth(cur, prev)
2081 sys.exit()
2082
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06002083 if args['show_stats']:
2084 with current_file as f:
2085 cur = _parse_stream(f)
2086 with previous_file as f:
2087 prev = _parse_stream(f)
2088 show_stats(cur, prev)
2089 sys.exit()
2090
Adrian Roos038a0292018-12-19 17:11:21 +01002091 classes_with_base = []
2092
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002093 with current_file as f:
Adrian Roos6eb57b02018-12-13 22:08:29 +01002094 if base_current_file:
2095 with base_current_file as base_f:
Adrian Roos038a0292018-12-19 17:11:21 +01002096 cur_fail, cur_noticed = examine_stream(f, base_f,
2097 out_classes_with_base=classes_with_base)
Adrian Roos6eb57b02018-12-13 22:08:29 +01002098 else:
Adrian Roos038a0292018-12-19 17:11:21 +01002099 cur_fail, cur_noticed = examine_stream(f, out_classes_with_base=classes_with_base)
2100
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002101 if not previous_file is None:
2102 with previous_file as f:
Adrian Roos6eb57b02018-12-13 22:08:29 +01002103 if base_previous_file:
2104 with base_previous_file as base_f:
Adrian Roos038a0292018-12-19 17:11:21 +01002105 prev_fail, prev_noticed = examine_stream(f, base_f,
2106 in_classes_with_base=classes_with_base)
Adrian Roos6eb57b02018-12-13 22:08:29 +01002107 else:
Adrian Roos038a0292018-12-19 17:11:21 +01002108 prev_fail, prev_noticed = examine_stream(f, in_classes_with_base=classes_with_base)
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002109
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002110 # ignore errors from previous API level
2111 for p in prev_fail:
2112 if p in cur_fail:
2113 del cur_fail[p]
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002114
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002115 # ignore classes unchanged from previous API level
2116 for k, v in prev_noticed.iteritems():
2117 if k in cur_noticed and v == cur_noticed[k]:
2118 del cur_noticed[k]
2119
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002120 """
2121 # NOTE: disabled because of memory pressure
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002122 # look for compatibility issues
2123 compat_fail = verify_compat(cur, prev)
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002124
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002125 print "%s API compatibility issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
2126 for f in sorted(compat_fail):
2127 print compat_fail[f]
2128 print
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002129 """
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002130
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002131 if args['show_noticed'] and len(cur_noticed) != 0:
2132 print "%s API changes noticed %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
2133 for f in sorted(cur_noticed.keys()):
2134 print f
2135 print
2136
Jason Monk53b2a732017-11-10 15:43:17 -05002137 if len(cur_fail) != 0:
2138 print "%s API style issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
2139 for f in sorted(cur_fail):
2140 print cur_fail[f]
2141 print
2142 sys.exit(77)