blob: 295e3de544ee06993a10dd7dbfa559bf31f918c0 [file] [log] [blame]
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001#!/usr/bin/env python
2
3# Copyright (C) 2014 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the 'License');
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an 'AS IS' BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""
18Enforces common Android public API design patterns. It ignores lint messages from
19a previous API level, if provided.
20
21Usage: apilint.py current.txt
22Usage: apilint.py current.txt previous.txt
Jeff Sharkey1498f9c2014-09-04 12:45:33 -070023
24You can also splice in blame details like this:
25$ git blame api/current.txt -t -e > /tmp/currentblame.txt
26$ apilint.py /tmp/currentblame.txt previous.txt --no-color
Jeff Sharkey8190f4882014-08-28 12:24:07 -070027"""
28
Adrian Roos1f1b6a82019-01-05 20:09:38 +010029import re, sys, collections, traceback, argparse, itertools
Jeff Sharkey8190f4882014-08-28 12:24:07 -070030
31
32BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
33
Adam Metcalf1c7e70a2015-03-27 16:11:23 -070034ALLOW_GOOGLE = False
35USE_COLOR = True
36
Jeff Sharkey8190f4882014-08-28 12:24:07 -070037def format(fg=None, bg=None, bright=False, bold=False, dim=False, reset=False):
38 # manually derived from http://en.wikipedia.org/wiki/ANSI_escape_code#Codes
Adam Metcalf1c7e70a2015-03-27 16:11:23 -070039 if not USE_COLOR: return ""
Jeff Sharkey8190f4882014-08-28 12:24:07 -070040 codes = []
41 if reset: codes.append("0")
42 else:
43 if not fg is None: codes.append("3%d" % (fg))
44 if not bg is None:
45 if not bright: codes.append("4%d" % (bg))
46 else: codes.append("10%d" % (bg))
47 if bold: codes.append("1")
48 elif dim: codes.append("2")
49 else: codes.append("22")
50 return "\033[%sm" % (";".join(codes))
51
52
53class Field():
Adrian Roosb787c182019-01-03 18:54:33 +010054 def __init__(self, clazz, line, raw, blame, sig_format = 1):
Jeff Sharkey8190f4882014-08-28 12:24:07 -070055 self.clazz = clazz
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -070056 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -070057 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -070058 self.blame = blame
Jeff Sharkey8190f4882014-08-28 12:24:07 -070059
Adrian Roosb787c182019-01-03 18:54:33 +010060 if sig_format == 2:
61 V2LineParser(raw).parse_into_field(self)
62 elif sig_format == 1:
63 # drop generics for now; may need multiple passes
64 raw = re.sub("<[^<]+?>", "", raw)
65 raw = re.sub("<[^<]+?>", "", raw)
Jeff Sharkey40d67f42018-07-17 13:29:40 -060066
Adrian Roosb787c182019-01-03 18:54:33 +010067 raw = raw.split()
68 self.split = list(raw)
Jeff Sharkey8190f4882014-08-28 12:24:07 -070069
Adrian Roosb787c182019-01-03 18:54:33 +010070 for r in ["field", "volatile", "transient", "public", "protected", "static", "final", "deprecated"]:
71 while r in raw: raw.remove(r)
Jeff Sharkey8190f4882014-08-28 12:24:07 -070072
Adrian Roosb787c182019-01-03 18:54:33 +010073 # ignore annotations for now
74 raw = [ r for r in raw if not r.startswith("@") ]
Jeff Sharkey40d67f42018-07-17 13:29:40 -060075
Adrian Roosb787c182019-01-03 18:54:33 +010076 self.typ = raw[0]
77 self.name = raw[1].strip(";")
78 if len(raw) >= 4 and raw[2] == "=":
79 self.value = raw[3].strip(';"')
80 else:
81 self.value = None
Adrian Roos80545ef2019-02-27 16:45:00 +010082 self.annotations = []
Adrian Roosb787c182019-01-03 18:54:33 +010083
84 self.ident = "-".join((self.typ, self.name, self.value or ""))
Jeff Sharkey037458a2014-09-04 15:46:20 -070085
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -070086 def __hash__(self):
87 return hash(self.raw)
88
Jeff Sharkey8190f4882014-08-28 12:24:07 -070089 def __repr__(self):
90 return self.raw
91
Adrian Roos80545ef2019-02-27 16:45:00 +010092
93class Argument(object):
94
95 __slots__ = ["type", "annotations", "name", "default"]
96
97 def __init__(self, type):
98 self.type = type
99 self.annotations = []
100 self.name = None
101 self.default = None
102
103
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700104class Method():
Adrian Roosb787c182019-01-03 18:54:33 +0100105 def __init__(self, clazz, line, raw, blame, sig_format = 1):
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700106 self.clazz = clazz
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700107 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700108 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700109 self.blame = blame
110
Adrian Roosb787c182019-01-03 18:54:33 +0100111 if sig_format == 2:
112 V2LineParser(raw).parse_into_method(self)
113 elif sig_format == 1:
114 # drop generics for now; may need multiple passes
115 raw = re.sub("<[^<]+?>", "", raw)
116 raw = re.sub("<[^<]+?>", "", raw)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700117
Adrian Roosb787c182019-01-03 18:54:33 +0100118 # handle each clause differently
119 raw_prefix, raw_args, _, raw_throws = re.match(r"(.*?)\((.*?)\)( throws )?(.*?);$", raw).groups()
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600120
Adrian Roosb787c182019-01-03 18:54:33 +0100121 # parse prefixes
122 raw = re.split("[\s]+", raw_prefix)
123 for r in ["", ";"]:
124 while r in raw: raw.remove(r)
125 self.split = list(raw)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700126
Adrian Roosb787c182019-01-03 18:54:33 +0100127 for r in ["method", "public", "protected", "static", "final", "deprecated", "abstract", "default", "operator", "synchronized"]:
128 while r in raw: raw.remove(r)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700129
Adrian Roosb787c182019-01-03 18:54:33 +0100130 self.typ = raw[0]
131 self.name = raw[1]
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600132
Adrian Roosb787c182019-01-03 18:54:33 +0100133 # parse args
Adrian Roos80545ef2019-02-27 16:45:00 +0100134 self.detailed_args = []
Adrian Roosb787c182019-01-03 18:54:33 +0100135 for arg in re.split(",\s*", raw_args):
136 arg = re.split("\s", arg)
137 # ignore annotations for now
138 arg = [ a for a in arg if not a.startswith("@") ]
139 if len(arg[0]) > 0:
Adrian Roos80545ef2019-02-27 16:45:00 +0100140 self.detailed_args.append(Argument(arg[0]))
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600141
Adrian Roosb787c182019-01-03 18:54:33 +0100142 # parse throws
143 self.throws = []
144 for throw in re.split(",\s*", raw_throws):
145 self.throws.append(throw)
Adrian Roos80545ef2019-02-27 16:45:00 +0100146
147 self.annotations = []
Adrian Roosb787c182019-01-03 18:54:33 +0100148 else:
149 raise ValueError("Unknown signature format: " + sig_format)
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600150
Adrian Roos80545ef2019-02-27 16:45:00 +0100151 self.args = map(lambda a: a.type, self.detailed_args)
Adrian Roosb787c182019-01-03 18:54:33 +0100152 self.ident = "-".join((self.typ, self.name, "-".join(self.args)))
153
154 def sig_matches(self, typ, name, args):
155 return typ == self.typ and name == self.name and args == self.args
Jeff Sharkey037458a2014-09-04 15:46:20 -0700156
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -0700157 def __hash__(self):
158 return hash(self.raw)
159
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700160 def __repr__(self):
161 return self.raw
162
163
164class Class():
Adrian Roosb787c182019-01-03 18:54:33 +0100165 def __init__(self, pkg, line, raw, blame, sig_format = 1):
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700166 self.pkg = pkg
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700167 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700168 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700169 self.blame = blame
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700170 self.ctors = []
171 self.fields = []
172 self.methods = []
173
Adrian Roosb787c182019-01-03 18:54:33 +0100174 if sig_format == 2:
175 V2LineParser(raw).parse_into_class(self)
176 elif sig_format == 1:
177 # drop generics for now; may need multiple passes
178 raw = re.sub("<[^<]+?>", "", raw)
179 raw = re.sub("<[^<]+?>", "", raw)
Jeff Sharkey40d67f42018-07-17 13:29:40 -0600180
Adrian Roosb787c182019-01-03 18:54:33 +0100181 raw = raw.split()
182 self.split = list(raw)
183 if "class" in raw:
184 self.fullname = raw[raw.index("class")+1]
185 elif "interface" in raw:
186 self.fullname = raw[raw.index("interface")+1]
187 elif "@interface" in raw:
188 self.fullname = raw[raw.index("@interface")+1]
189 else:
190 raise ValueError("Funky class type %s" % (self.raw))
Jeff Sharkey037458a2014-09-04 15:46:20 -0700191
Adrian Roosb787c182019-01-03 18:54:33 +0100192 if "extends" in raw:
193 self.extends = raw[raw.index("extends")+1]
194 else:
195 self.extends = None
196
197 if "implements" in raw:
198 self.implements = raw[raw.index("implements")+1]
199 else:
200 self.implements = None
Jeff Sharkey037458a2014-09-04 15:46:20 -0700201 else:
Adrian Roosb787c182019-01-03 18:54:33 +0100202 raise ValueError("Unknown signature format: " + sig_format)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700203
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700204 self.fullname = self.pkg.name + "." + self.fullname
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800205 self.fullname_path = self.fullname.split(".")
206
Adrian Roosb787c182019-01-03 18:54:33 +0100207 if self.extends is not None:
208 self.extends_path = self.extends.split(".")
209 else:
210 self.extends_path = []
211
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700212 self.name = self.fullname[self.fullname.rindex(".")+1:]
213
Adrian Roos6eb57b02018-12-13 22:08:29 +0100214 def merge_from(self, other):
215 self.ctors.extend(other.ctors)
216 self.fields.extend(other.fields)
217 self.methods.extend(other.methods)
218
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -0700219 def __hash__(self):
220 return hash((self.raw, tuple(self.ctors), tuple(self.fields), tuple(self.methods)))
221
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700222 def __repr__(self):
223 return self.raw
224
225
226class Package():
Adrian Roosb1faa0b2019-02-26 11:54:40 +0100227 NAME = re.compile("package(?: .*)? ([A-Za-z.]+)")
228
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700229 def __init__(self, line, raw, blame):
230 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700231 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700232 self.blame = blame
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700233
Adrian Roosb1faa0b2019-02-26 11:54:40 +0100234 self.name = Package.NAME.match(raw).group(1)
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800235 self.name_path = self.name.split(".")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700236
237 def __repr__(self):
238 return self.raw
239
Adrian Roose5eeae72019-01-04 20:10:06 +0100240class V2Tokenizer(object):
241 __slots__ = ["raw"]
242
Adrian Rooscf82e042019-01-29 15:01:28 +0100243 SIGNATURE_PREFIX = "// Signature format: "
Adrian Roos5cdfb692019-01-05 22:04:55 +0100244 DELIMITER = re.compile(r'\s+|[()@<>;,={}/"!?]|\[\]|\.\.\.')
Adrian Roosb787c182019-01-03 18:54:33 +0100245 STRING_SPECIAL = re.compile(r'["\\]')
246
247 def __init__(self, raw):
248 self.raw = raw
Adrian Roosb787c182019-01-03 18:54:33 +0100249
Adrian Roose5eeae72019-01-04 20:10:06 +0100250 def tokenize(self):
251 tokens = []
252 current = 0
253 raw = self.raw
254 length = len(raw)
Adrian Roosb787c182019-01-03 18:54:33 +0100255
Adrian Roose5eeae72019-01-04 20:10:06 +0100256 while current < length:
257 while current < length:
258 start = current
259 match = V2Tokenizer.DELIMITER.search(raw, start)
260 if match is not None:
261 match_start = match.start()
262 if match_start == current:
263 end = match.end()
264 else:
265 end = match_start
266 else:
267 end = length
Adrian Roosb787c182019-01-03 18:54:33 +0100268
Adrian Roose5eeae72019-01-04 20:10:06 +0100269 token = raw[start:end]
270 current = end
Adrian Roosb787c182019-01-03 18:54:33 +0100271
Adrian Roose5eeae72019-01-04 20:10:06 +0100272 if token == "" or token[0] == " ":
273 continue
274 else:
275 break
Adrian Roosb787c182019-01-03 18:54:33 +0100276
Adrian Roose5eeae72019-01-04 20:10:06 +0100277 if token == "@":
278 if raw[start:start+11] == "@interface ":
279 current = start + 11
280 tokens.append("@interface")
281 continue
282 elif token == '/':
283 if raw[start:start+2] == "//":
284 current = length
285 continue
286 elif token == '"':
287 current, string_token = self.tokenize_string(raw, length, current)
288 tokens.append(token + string_token)
289 continue
Adrian Roosb787c182019-01-03 18:54:33 +0100290
Adrian Roose5eeae72019-01-04 20:10:06 +0100291 tokens.append(token)
Adrian Roosb787c182019-01-03 18:54:33 +0100292
Adrian Roose5eeae72019-01-04 20:10:06 +0100293 return tokens
Adrian Roosb787c182019-01-03 18:54:33 +0100294
Adrian Roose5eeae72019-01-04 20:10:06 +0100295 def tokenize_string(self, raw, length, current):
296 start = current
297 end = length
Adrian Roosb787c182019-01-03 18:54:33 +0100298 while start < end:
Adrian Roose5eeae72019-01-04 20:10:06 +0100299 match = V2Tokenizer.STRING_SPECIAL.search(raw, start)
Adrian Roosb787c182019-01-03 18:54:33 +0100300 if match:
301 if match.group() == '"':
302 end = match.end()
303 break
304 elif match.group() == '\\':
305 # ignore whatever is after the slash
306 start += 2
307 else:
308 raise ValueError("Unexpected match: `%s`" % (match.group()))
309 else:
Adrian Roose5eeae72019-01-04 20:10:06 +0100310 raise ValueError("Unexpected EOF tokenizing string: `%s`" % (raw[current - 1:],))
Adrian Roosb787c182019-01-03 18:54:33 +0100311
Adrian Roose5eeae72019-01-04 20:10:06 +0100312 token = raw[current:end]
313 return end, token
Adrian Roosb787c182019-01-03 18:54:33 +0100314
Adrian Roose5eeae72019-01-04 20:10:06 +0100315class V2LineParser(object):
316 __slots__ = ["tokenized", "current", "len"]
317
Adrian Roos258c5722019-01-21 15:43:15 +0100318 FIELD_KINDS = ("field", "property", "enum_constant")
Adrian Roosd1e38922019-01-14 15:44:15 +0100319 MODIFIERS = set("public protected internal private abstract default static final transient volatile synchronized native operator sealed strictfp infix inline suspend vararg".split())
Adrian Roosb787c182019-01-03 18:54:33 +0100320 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())
321
322 def __init__(self, raw):
Adrian Roose5eeae72019-01-04 20:10:06 +0100323 self.tokenized = V2Tokenizer(raw).tokenize()
Adrian Roosb787c182019-01-03 18:54:33 +0100324 self.current = 0
Adrian Roose5eeae72019-01-04 20:10:06 +0100325 self.len = len(self.tokenized)
Adrian Roosb787c182019-01-03 18:54:33 +0100326
327 def parse_into_method(self, method):
328 method.split = []
329 kind = self.parse_one_of("ctor", "method")
330 method.split.append(kind)
Adrian Roos80545ef2019-02-27 16:45:00 +0100331 method.annotations = self.parse_annotations()
Adrian Roosb787c182019-01-03 18:54:33 +0100332 method.split.extend(self.parse_modifiers())
333 self.parse_matching_paren("<", ">")
Adrian Roos80545ef2019-02-27 16:45:00 +0100334 if "@Deprecated" in method.annotations:
Adrian Roosb787c182019-01-03 18:54:33 +0100335 method.split.append("deprecated")
336 if kind == "ctor":
337 method.typ = "ctor"
338 else:
339 method.typ = self.parse_type()
340 method.split.append(method.typ)
341 method.name = self.parse_name()
342 method.split.append(method.name)
343 self.parse_token("(")
Adrian Roos80545ef2019-02-27 16:45:00 +0100344 method.detailed_args = self.parse_args()
Adrian Roosb787c182019-01-03 18:54:33 +0100345 self.parse_token(")")
346 method.throws = self.parse_throws()
347 if "@interface" in method.clazz.split:
348 self.parse_annotation_default()
349 self.parse_token(";")
350 self.parse_eof()
351
352 def parse_into_class(self, clazz):
353 clazz.split = []
354 annotations = self.parse_annotations()
355 if "@Deprecated" in annotations:
356 clazz.split.append("deprecated")
357 clazz.split.extend(self.parse_modifiers())
358 kind = self.parse_one_of("class", "interface", "@interface", "enum")
359 if kind == "enum":
360 # enums are implicitly final
361 clazz.split.append("final")
362 clazz.split.append(kind)
363 clazz.fullname = self.parse_name()
364 self.parse_matching_paren("<", ">")
365 extends = self.parse_extends()
366 clazz.extends = extends[0] if extends else None
367 implements = self.parse_implements()
368 clazz.implements = implements[0] if implements else None
369 # The checks assume that interfaces are always found in implements, which isn't true for
370 # subinterfaces.
371 if not implements and "interface" in clazz.split:
372 clazz.implements = clazz.extends
373 self.parse_token("{")
374 self.parse_eof()
375
376 def parse_into_field(self, field):
Adrian Roos258c5722019-01-21 15:43:15 +0100377 kind = self.parse_one_of(*V2LineParser.FIELD_KINDS)
Adrian Roosb787c182019-01-03 18:54:33 +0100378 field.split = [kind]
Adrian Roos80545ef2019-02-27 16:45:00 +0100379 field.annotations = self.parse_annotations()
380 if "@Deprecated" in field.annotations:
Adrian Roosb787c182019-01-03 18:54:33 +0100381 field.split.append("deprecated")
382 field.split.extend(self.parse_modifiers())
383 field.typ = self.parse_type()
384 field.split.append(field.typ)
385 field.name = self.parse_name()
386 field.split.append(field.name)
387 if self.parse_if("="):
388 field.value = self.parse_value_stripped()
389 else:
390 field.value = None
391
392 self.parse_token(";")
393 self.parse_eof()
394
395 def lookahead(self):
396 return self.tokenized[self.current]
397
398 def parse_one_of(self, *options):
399 found = self.lookahead()
400 if found not in options:
401 raise ValueError("Parsing failed, expected one of `%s` but found `%s` in %s" % (options, found, repr(self.tokenized)))
402 return self.parse_token()
403
404 def parse_token(self, tok = None):
405 found = self.lookahead()
406 if tok is not None and found != tok:
407 raise ValueError("Parsing failed, expected `%s` but found `%s` in %s" % (tok, found, repr(self.tokenized)))
408 self.current += 1
409 return found
410
411 def eof(self):
Adrian Roose5eeae72019-01-04 20:10:06 +0100412 return self.current == self.len
Adrian Roosb787c182019-01-03 18:54:33 +0100413
414 def parse_eof(self):
415 if not self.eof():
416 raise ValueError("Parsing failed, expected EOF, but %s has not been parsed in %s" % (self.tokenized[self.current:], self.tokenized))
417
418 def parse_if(self, tok):
419 if not self.eof() and self.lookahead() == tok:
420 self.parse_token()
421 return True
422 return False
423
424 def parse_annotations(self):
425 ret = []
426 while self.lookahead() == "@":
427 ret.append(self.parse_annotation())
428 return ret
429
430 def parse_annotation(self):
431 ret = self.parse_token("@") + self.parse_token()
432 self.parse_matching_paren("(", ")")
433 return ret
434
435 def parse_matching_paren(self, open, close):
436 start = self.current
437 if not self.parse_if(open):
438 return
439 length = len(self.tokenized)
440 count = 1
441 while count > 0:
442 if self.current == length:
443 raise ValueError("Unexpected EOF looking for closing paren: `%s`" % (self.tokenized[start:],))
444 t = self.parse_token()
445 if t == open:
446 count += 1
447 elif t == close:
448 count -= 1
449 return self.tokenized[start:self.current]
450
451 def parse_modifiers(self):
452 ret = []
453 while self.lookahead() in V2LineParser.MODIFIERS:
454 ret.append(self.parse_token())
455 return ret
456
Adrian Roos5cdfb692019-01-05 22:04:55 +0100457 def parse_kotlin_nullability(self):
458 t = self.lookahead()
459 if t == "?" or t == "!":
460 return self.parse_token()
461 return None
462
Adrian Roosb787c182019-01-03 18:54:33 +0100463 def parse_type(self):
Adrian Roosd1e38922019-01-14 15:44:15 +0100464 self.parse_annotations()
Adrian Roosb787c182019-01-03 18:54:33 +0100465 type = self.parse_token()
Adrian Roosd1e38922019-01-14 15:44:15 +0100466 if type[-1] == '.':
467 self.parse_annotations()
468 type += self.parse_token()
Adrian Roosb787c182019-01-03 18:54:33 +0100469 if type in V2LineParser.JAVA_LANG_TYPES:
470 type = "java.lang." + type
471 self.parse_matching_paren("<", ">")
Adrian Roos5cdfb692019-01-05 22:04:55 +0100472 while True:
473 t = self.lookahead()
Adrian Roosd1e38922019-01-14 15:44:15 +0100474 if t == "@":
475 self.parse_annotation()
476 elif t == "[]":
Adrian Roos5cdfb692019-01-05 22:04:55 +0100477 type += self.parse_token()
478 elif self.parse_kotlin_nullability() is not None:
479 pass # discard nullability for now
480 else:
481 break
Adrian Roosb787c182019-01-03 18:54:33 +0100482 return type
483
484 def parse_arg_type(self):
485 type = self.parse_type()
486 if self.parse_if("..."):
487 type += "..."
Adrian Roos5cdfb692019-01-05 22:04:55 +0100488 self.parse_kotlin_nullability() # discard nullability for now
Adrian Roosb787c182019-01-03 18:54:33 +0100489 return type
490
491 def parse_name(self):
492 return self.parse_token()
493
494 def parse_args(self):
495 args = []
496 if self.lookahead() == ")":
497 return args
498
499 while True:
500 args.append(self.parse_arg())
501 if self.lookahead() == ")":
502 return args
503 self.parse_token(",")
504
505 def parse_arg(self):
Adrian Roosd1e38922019-01-14 15:44:15 +0100506 self.parse_if("vararg") # kotlin vararg
Adrian Roos80545ef2019-02-27 16:45:00 +0100507 annotations = self.parse_annotations()
508 arg = Argument(self.parse_arg_type())
509 arg.annotations = annotations
Adrian Roos5cdfb692019-01-05 22:04:55 +0100510 l = self.lookahead()
511 if l != "," and l != ")":
Adrian Roosd1e38922019-01-14 15:44:15 +0100512 if self.lookahead() != '=':
Adrian Roos80545ef2019-02-27 16:45:00 +0100513 arg.name = self.parse_token() # kotlin argument name
Adrian Roos5cdfb692019-01-05 22:04:55 +0100514 if self.parse_if('='): # kotlin default value
Adrian Roos80545ef2019-02-27 16:45:00 +0100515 arg.default = self.parse_expression()
516 return arg
Adrian Roosb787c182019-01-03 18:54:33 +0100517
Adrian Roosd1e38922019-01-14 15:44:15 +0100518 def parse_expression(self):
519 while not self.lookahead() in [')', ',', ';']:
520 (self.parse_matching_paren('(', ')') or
521 self.parse_matching_paren('{', '}') or
522 self.parse_token())
523
Adrian Roosb787c182019-01-03 18:54:33 +0100524 def parse_throws(self):
525 ret = []
526 if self.parse_if("throws"):
527 ret.append(self.parse_type())
528 while self.parse_if(","):
529 ret.append(self.parse_type())
530 return ret
531
532 def parse_extends(self):
533 if self.parse_if("extends"):
534 return self.parse_space_delimited_type_list()
535 return []
536
537 def parse_implements(self):
538 if self.parse_if("implements"):
539 return self.parse_space_delimited_type_list()
540 return []
541
542 def parse_space_delimited_type_list(self, terminals = ["implements", "{"]):
543 types = []
544 while True:
545 types.append(self.parse_type())
546 if self.lookahead() in terminals:
547 return types
548
549 def parse_annotation_default(self):
550 if self.parse_if("default"):
Adrian Roosd1e38922019-01-14 15:44:15 +0100551 self.parse_expression()
Adrian Roosb787c182019-01-03 18:54:33 +0100552
553 def parse_value(self):
554 if self.lookahead() == "{":
555 return " ".join(self.parse_matching_paren("{", "}"))
556 elif self.lookahead() == "(":
557 return " ".join(self.parse_matching_paren("(", ")"))
558 else:
559 return self.parse_token()
560
561 def parse_value_stripped(self):
562 value = self.parse_value()
563 if value[0] == '"':
564 return value[1:-1]
565 return value
566
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700567
Adrian Roos038a0292018-12-19 17:11:21 +0100568def _parse_stream(f, clazz_cb=None, base_f=None, out_classes_with_base=None,
569 in_classes_with_base=[]):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700570 api = {}
Adrian Roos038a0292018-12-19 17:11:21 +0100571 in_classes_with_base = _retry_iterator(in_classes_with_base)
Adrian Roos6eb57b02018-12-13 22:08:29 +0100572
573 if base_f:
Adrian Roos038a0292018-12-19 17:11:21 +0100574 base_classes = _retry_iterator(_parse_stream_to_generator(base_f))
Adrian Roos6eb57b02018-12-13 22:08:29 +0100575 else:
576 base_classes = []
577
Adrian Roos038a0292018-12-19 17:11:21 +0100578 def handle_class(clazz):
Adrian Roos6eb57b02018-12-13 22:08:29 +0100579 if clazz_cb:
580 clazz_cb(clazz)
581 else: # In callback mode, don't keep track of the full API
582 api[clazz.fullname] = clazz
583
Adrian Roos038a0292018-12-19 17:11:21 +0100584 def handle_missed_classes_with_base(clazz):
585 for c in _yield_until_matching_class(in_classes_with_base, clazz):
586 base_class = _skip_to_matching_class(base_classes, c)
587 if base_class:
588 handle_class(base_class)
589
590 for clazz in _parse_stream_to_generator(f):
591 # Before looking at clazz, let's see if there's some classes that were not present, but
592 # may have an entry in the base stream.
593 handle_missed_classes_with_base(clazz)
594
595 base_class = _skip_to_matching_class(base_classes, clazz)
596 if base_class:
597 clazz.merge_from(base_class)
598 if out_classes_with_base is not None:
599 out_classes_with_base.append(clazz)
600 handle_class(clazz)
601
602 handle_missed_classes_with_base(None)
603
Adrian Roos6eb57b02018-12-13 22:08:29 +0100604 return api
605
606def _parse_stream_to_generator(f):
607 line = 0
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700608 pkg = None
609 clazz = None
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700610 blame = None
Adrian Roosb787c182019-01-03 18:54:33 +0100611 sig_format = 1
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700612
Adrian Roos80545ef2019-02-27 16:45:00 +0100613 re_blame = re.compile(r"^(\^?[a-z0-9]{7,}) \(<([^>]+)>.+?\) (.+?)$")
Adrian Roos258c5722019-01-21 15:43:15 +0100614
615 field_prefixes = map(lambda kind: " %s" % (kind,), V2LineParser.FIELD_KINDS)
616 def startsWithFieldPrefix(raw):
617 for prefix in field_prefixes:
618 if raw.startswith(prefix):
619 return True
620 return False
621
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800622 for raw in f:
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700623 line += 1
624 raw = raw.rstrip()
625 match = re_blame.match(raw)
626 if match is not None:
627 blame = match.groups()[0:2]
Adrian Roos80545ef2019-02-27 16:45:00 +0100628 if blame[0].startswith("^"): # Outside of blame range
629 blame = None
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700630 raw = match.groups()[2]
631 else:
632 blame = None
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700633
Adrian Roos80545ef2019-02-27 16:45:00 +0100634 if line == 1 and V2Tokenizer.SIGNATURE_PREFIX in raw:
Adrian Rooscf82e042019-01-29 15:01:28 +0100635 sig_format_string = raw[len(V2Tokenizer.SIGNATURE_PREFIX):]
636 if sig_format_string in ["2.0", "3.0"]:
637 sig_format = 2
638 else:
639 raise ValueError("Unknown format: %s" % (sig_format_string,))
Adrian Roosb787c182019-01-03 18:54:33 +0100640 elif raw.startswith("package"):
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700641 pkg = Package(line, raw, blame)
642 elif raw.startswith(" ") and raw.endswith("{"):
Adrian Roosb787c182019-01-03 18:54:33 +0100643 clazz = Class(pkg, line, raw, blame, sig_format=sig_format)
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700644 elif raw.startswith(" ctor"):
Adrian Roosb787c182019-01-03 18:54:33 +0100645 clazz.ctors.append(Method(clazz, line, raw, blame, sig_format=sig_format))
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700646 elif raw.startswith(" method"):
Adrian Roosb787c182019-01-03 18:54:33 +0100647 clazz.methods.append(Method(clazz, line, raw, blame, sig_format=sig_format))
Adrian Roos258c5722019-01-21 15:43:15 +0100648 elif startsWithFieldPrefix(raw):
Adrian Roosb787c182019-01-03 18:54:33 +0100649 clazz.fields.append(Field(clazz, line, raw, blame, sig_format=sig_format))
Adrian Roos6eb57b02018-12-13 22:08:29 +0100650 elif raw.startswith(" }") and clazz:
Adrian Roos038a0292018-12-19 17:11:21 +0100651 yield clazz
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800652
Adrian Roos5ed42b62018-12-19 17:10:22 +0100653def _retry_iterator(it):
654 """Wraps an iterator, such that calling send(True) on it will redeliver the same element"""
655 for e in it:
656 while True:
657 retry = yield e
658 if not retry:
659 break
660 # send() was called, asking us to redeliver clazz on next(). Still need to yield
661 # a dummy value to the send() first though.
662 if (yield "Returning clazz on next()"):
663 raise TypeError("send() must be followed by next(), not send()")
664
Adrian Roos038a0292018-12-19 17:11:21 +0100665def _skip_to_matching_class(classes, needle):
666 """Takes a classes iterator and consumes entries until it returns the class we're looking for
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700667
Adrian Roos6eb57b02018-12-13 22:08:29 +0100668 This relies on classes being sorted by package and class name."""
669
670 for clazz in classes:
671 if clazz.pkg.name < needle.pkg.name:
672 # We haven't reached the right package yet
673 continue
Adrian Roos5ed42b62018-12-19 17:10:22 +0100674 if clazz.pkg.name == needle.pkg.name and clazz.fullname < needle.fullname:
675 # We're in the right package, but not the right class yet
Adrian Roos6eb57b02018-12-13 22:08:29 +0100676 continue
677 if clazz.fullname == needle.fullname:
678 return clazz
679 # We ran past the right class. Send it back into the generator, then report failure.
680 classes.send(clazz)
681 return None
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700682
Adrian Roos038a0292018-12-19 17:11:21 +0100683def _yield_until_matching_class(classes, needle):
684 """Takes a class iterator and yields entries it until it reaches the class we're looking for.
685
686 This relies on classes being sorted by package and class name."""
687
688 for clazz in classes:
689 if needle is None:
690 yield clazz
691 elif clazz.pkg.name < needle.pkg.name:
692 # We haven't reached the right package yet
693 yield clazz
694 elif clazz.pkg.name == needle.pkg.name and clazz.fullname < needle.fullname:
695 # We're in the right package, but not the right class yet
696 yield clazz
697 elif clazz.fullname == needle.fullname:
698 # Class found, abort.
699 return
700 else:
701 # We ran past the right class. Send it back into the iterator, then abort.
702 classes.send(clazz)
703 return
704
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700705class Failure():
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800706 def __init__(self, sig, clazz, detail, error, rule, msg):
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700707 self.sig = sig
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700708 self.error = error
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800709 self.rule = rule
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700710 self.msg = msg
711
712 if error:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800713 self.head = "Error %s" % (rule) if rule else "Error"
714 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 -0700715 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800716 self.head = "Warning %s" % (rule) if rule else "Warning"
717 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 -0700718
719 self.line = clazz.line
720 blame = clazz.blame
721 if detail is not None:
722 dump += "\n in " + repr(detail)
723 self.line = detail.line
724 blame = detail.blame
725 dump += "\n in " + repr(clazz)
726 dump += "\n in " + repr(clazz.pkg)
727 dump += "\n at line " + repr(self.line)
728 if blame is not None:
729 dump += "\n last modified by %s in %s" % (blame[1], blame[0])
730
731 self.dump = dump
732
733 def __repr__(self):
734 return self.dump
735
736
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700737failures = {}
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700738
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800739def _fail(clazz, detail, error, rule, msg):
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700740 """Records an API failure to be processed later."""
741 global failures
742
Adrian Roosb787c182019-01-03 18:54:33 +0100743 sig = "%s-%s-%s" % (clazz.fullname, detail.ident if detail else None, msg)
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700744 sig = sig.replace(" deprecated ", " ")
745
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800746 failures[sig] = Failure(sig, clazz, detail, error, rule, msg)
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700747
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700748
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800749def warn(clazz, detail, rule, msg):
750 _fail(clazz, detail, False, rule, msg)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700751
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800752def error(clazz, detail, rule, msg):
753 _fail(clazz, detail, True, rule, msg)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700754
755
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -0700756noticed = {}
757
758def notice(clazz):
759 global noticed
760
761 noticed[clazz.fullname] = hash(clazz)
762
763
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700764def verify_constants(clazz):
765 """All static final constants must be FOO_NAME style."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700766 if re.match("android\.R\.[a-z]+", clazz.fullname): return
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600767 if clazz.fullname.startswith("android.os.Build"): return
768 if clazz.fullname == "android.system.OsConstants": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700769
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600770 req = ["java.lang.String","byte","short","int","long","float","double","boolean","char"]
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700771 for f in clazz.fields:
772 if "static" in f.split and "final" in f.split:
773 if re.match("[A-Z0-9_]+", f.name) is None:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800774 error(clazz, f, "C2", "Constant field names must be FOO_NAME")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600775 if f.typ != "java.lang.String":
Jeff Sharkey331279b2016-02-29 16:02:02 -0700776 if f.name.startswith("MIN_") or f.name.startswith("MAX_"):
777 warn(clazz, f, "C8", "If min/max could change in future, make them dynamic methods")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600778 if f.typ in req and f.value is None:
779 error(clazz, f, None, "All constants must be defined at compile time")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700780
781
782def verify_enums(clazz):
783 """Enums are bad, mmkay?"""
Adrian Roosb787c182019-01-03 18:54:33 +0100784 if clazz.extends == "java.lang.Enum" or "enum" in clazz.split:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800785 error(clazz, None, "F5", "Enums are not allowed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700786
787
788def verify_class_names(clazz):
789 """Try catching malformed class names like myMtp or MTPUser."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700790 if clazz.fullname.startswith("android.opengl"): return
791 if clazz.fullname.startswith("android.renderscript"): return
792 if re.match("android\.R\.[a-z]+", clazz.fullname): return
793
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700794 if re.search("[A-Z]{2,}", clazz.name) is not None:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800795 warn(clazz, None, "S1", "Class names with acronyms should be Mtp not MTP")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700796 if re.match("[^A-Z]", clazz.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800797 error(clazz, None, "S1", "Class must start with uppercase char")
Jeff Sharkeyee21f692018-02-16 13:29:29 -0700798 if clazz.name.endswith("Impl"):
799 error(clazz, None, None, "Don't expose your implementation details")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700800
801
802def verify_method_names(clazz):
803 """Try catching malformed method names, like Foo() or getMTU()."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700804 if clazz.fullname.startswith("android.opengl"): return
805 if clazz.fullname.startswith("android.renderscript"): return
806 if clazz.fullname == "android.system.OsConstants": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700807
808 for m in clazz.methods:
809 if re.search("[A-Z]{2,}", m.name) is not None:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800810 warn(clazz, m, "S1", "Method names with acronyms should be getMtu() instead of getMTU()")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700811 if re.match("[^a-z]", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800812 error(clazz, m, "S1", "Method name must start with lowercase char")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700813
814
815def verify_callbacks(clazz):
816 """Verify Callback classes.
817 All callback classes must be abstract.
818 All methods must follow onFoo() naming style."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700819 if clazz.fullname == "android.speech.tts.SynthesisCallback": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700820
821 if clazz.name.endswith("Callbacks"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800822 error(clazz, None, "L1", "Callback class names should be singular")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700823 if clazz.name.endswith("Observer"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800824 warn(clazz, None, "L1", "Class should be named FooCallback")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700825
826 if clazz.name.endswith("Callback"):
827 if "interface" in clazz.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800828 error(clazz, None, "CL3", "Callbacks must be abstract class to enable extension in future API levels")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700829
830 for m in clazz.methods:
831 if not re.match("on[A-Z][a-z]*", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800832 error(clazz, m, "L1", "Callback method names must be onFoo() style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700833
834
835def verify_listeners(clazz):
836 """Verify Listener classes.
837 All Listener classes must be interface.
838 All methods must follow onFoo() naming style.
839 If only a single method, it must match class name:
840 interface OnFooListener { void onFoo() }"""
841
842 if clazz.name.endswith("Listener"):
Adrian Roosb787c182019-01-03 18:54:33 +0100843 if "abstract" in clazz.split and "class" in clazz.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800844 error(clazz, None, "L1", "Listeners should be an interface, or otherwise renamed Callback")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700845
846 for m in clazz.methods:
847 if not re.match("on[A-Z][a-z]*", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800848 error(clazz, m, "L1", "Listener method names must be onFoo() style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700849
850 if len(clazz.methods) == 1 and clazz.name.startswith("On"):
851 m = clazz.methods[0]
852 if (m.name + "Listener").lower() != clazz.name.lower():
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800853 error(clazz, m, "L1", "Single listener method name must match class name")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700854
855
856def verify_actions(clazz):
857 """Verify intent actions.
858 All action names must be named ACTION_FOO.
859 All action values must be scoped by package and match name:
860 package android.foo {
861 String ACTION_BAR = "android.foo.action.BAR";
862 }"""
863 for f in clazz.fields:
864 if f.value is None: continue
865 if f.name.startswith("EXTRA_"): continue
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700866 if f.name == "SERVICE_INTERFACE" or f.name == "PROVIDER_INTERFACE": continue
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600867 if "INTERACTION" in f.name: continue
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700868
869 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
870 if "_ACTION" in f.name or "ACTION_" in f.name or ".action." in f.value.lower():
871 if not f.name.startswith("ACTION_"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800872 error(clazz, f, "C3", "Intent action constant name must be ACTION_FOO")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700873 else:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700874 if clazz.fullname == "android.content.Intent":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700875 prefix = "android.intent.action"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700876 elif clazz.fullname == "android.provider.Settings":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700877 prefix = "android.settings"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700878 elif clazz.fullname == "android.app.admin.DevicePolicyManager" or clazz.fullname == "android.app.admin.DeviceAdminReceiver":
879 prefix = "android.app.action"
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700880 else:
881 prefix = clazz.pkg.name + ".action"
882 expected = prefix + "." + f.name[7:]
883 if f.value != expected:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700884 error(clazz, f, "C4", "Inconsistent action value; expected '%s'" % (expected))
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700885
886
887def verify_extras(clazz):
888 """Verify intent extras.
889 All extra names must be named EXTRA_FOO.
890 All extra values must be scoped by package and match name:
891 package android.foo {
892 String EXTRA_BAR = "android.foo.extra.BAR";
893 }"""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700894 if clazz.fullname == "android.app.Notification": return
895 if clazz.fullname == "android.appwidget.AppWidgetManager": return
896
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700897 for f in clazz.fields:
898 if f.value is None: continue
899 if f.name.startswith("ACTION_"): continue
900
901 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
902 if "_EXTRA" in f.name or "EXTRA_" in f.name or ".extra" in f.value.lower():
903 if not f.name.startswith("EXTRA_"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800904 error(clazz, f, "C3", "Intent extra must be EXTRA_FOO")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700905 else:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700906 if clazz.pkg.name == "android.content" and clazz.name == "Intent":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700907 prefix = "android.intent.extra"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700908 elif clazz.pkg.name == "android.app.admin":
909 prefix = "android.app.extra"
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700910 else:
911 prefix = clazz.pkg.name + ".extra"
912 expected = prefix + "." + f.name[6:]
913 if f.value != expected:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700914 error(clazz, f, "C4", "Inconsistent extra value; expected '%s'" % (expected))
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700915
916
917def verify_equals(clazz):
918 """Verify that equals() and hashCode() must be overridden together."""
Jeff Sharkey40d623e2016-12-21 13:46:33 -0700919 eq = False
920 hc = False
921 for m in clazz.methods:
Adrian Roosb787c182019-01-03 18:54:33 +0100922 if "static" in m.split: continue
923 if m.sig_matches("boolean", "equals", ["java.lang.Object"]): eq = True
924 if m.sig_matches("int", "hashCode", []): hc = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700925 if eq != hc:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800926 error(clazz, None, "M8", "Must override both equals and hashCode; missing one")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700927
928
929def verify_parcelable(clazz):
930 """Verify that Parcelable objects aren't hiding required bits."""
Adrian Roosb787c182019-01-03 18:54:33 +0100931 if clazz.implements == "android.os.Parcelable":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700932 creator = [ i for i in clazz.fields if i.name == "CREATOR" ]
933 write = [ i for i in clazz.methods if i.name == "writeToParcel" ]
934 describe = [ i for i in clazz.methods if i.name == "describeContents" ]
935
936 if len(creator) == 0 or len(write) == 0 or len(describe) == 0:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800937 error(clazz, None, "FW3", "Parcelable requires CREATOR, writeToParcel, and describeContents; missing one")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700938
Adrian Roosb787c182019-01-03 18:54:33 +0100939 if "final" not in clazz.split:
Jeff Sharkey331279b2016-02-29 16:02:02 -0700940 error(clazz, None, "FW8", "Parcelable classes must be final")
941
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700942 for c in clazz.ctors:
943 if c.args == ["android.os.Parcel"]:
944 error(clazz, c, "FW3", "Parcelable inflation is exposed through CREATOR, not raw constructors")
945
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700946
947def verify_protected(clazz):
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800948 """Verify that no protected methods or fields are allowed."""
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700949 for m in clazz.methods:
Jeff Sharkeyeff9e222018-09-27 16:29:25 -0600950 if m.name == "finalize": continue
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700951 if "protected" in m.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800952 error(clazz, m, "M7", "Protected methods not allowed; must be public")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700953 for f in clazz.fields:
954 if "protected" in f.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800955 error(clazz, f, "M7", "Protected fields not allowed; must be public")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700956
957
958def verify_fields(clazz):
959 """Verify that all exposed fields are final.
960 Exposed fields must follow myName style.
961 Catch internal mFoo objects being exposed."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700962
963 IGNORE_BARE_FIELDS = [
964 "android.app.ActivityManager.RecentTaskInfo",
965 "android.app.Notification",
966 "android.content.pm.ActivityInfo",
967 "android.content.pm.ApplicationInfo",
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600968 "android.content.pm.ComponentInfo",
969 "android.content.pm.ResolveInfo",
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700970 "android.content.pm.FeatureGroupInfo",
971 "android.content.pm.InstrumentationInfo",
972 "android.content.pm.PackageInfo",
973 "android.content.pm.PackageItemInfo",
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600974 "android.content.res.Configuration",
975 "android.graphics.BitmapFactory.Options",
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700976 "android.os.Message",
977 "android.system.StructPollfd",
978 ]
979
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700980 for f in clazz.fields:
981 if not "final" in f.split:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700982 if clazz.fullname in IGNORE_BARE_FIELDS:
983 pass
984 elif clazz.fullname.endswith("LayoutParams"):
985 pass
986 elif clazz.fullname.startswith("android.util.Mutable"):
987 pass
988 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800989 error(clazz, f, "F2", "Bare fields must be marked final, or add accessors if mutable")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700990
Adrian Roosd1e38922019-01-14 15:44:15 +0100991 if "static" not in f.split and "property" not in f.split:
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700992 if not re.match("[a-z]([a-zA-Z]+)?", f.name):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800993 error(clazz, f, "S1", "Non-static fields must be named using myField style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700994
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700995 if re.match("[ms][A-Z]", f.name):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800996 error(clazz, f, "F1", "Internal objects must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700997
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700998 if re.match("[A-Z_]+", f.name):
999 if "static" not in f.split or "final" not in f.split:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001000 error(clazz, f, "C2", "Constants must be marked static final")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001001
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001002
1003def verify_register(clazz):
1004 """Verify parity of registration methods.
1005 Callback objects use register/unregister methods.
1006 Listener objects use add/remove methods."""
1007 methods = [ m.name for m in clazz.methods ]
1008 for m in clazz.methods:
1009 if "Callback" in m.raw:
1010 if m.name.startswith("register"):
1011 other = "unregister" + m.name[8:]
1012 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001013 error(clazz, m, "L2", "Missing unregister method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001014 if m.name.startswith("unregister"):
1015 other = "register" + m.name[10:]
1016 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001017 error(clazz, m, "L2", "Missing register method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001018
1019 if m.name.startswith("add") or m.name.startswith("remove"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001020 error(clazz, m, "L3", "Callback methods should be named register/unregister")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001021
1022 if "Listener" in m.raw:
1023 if m.name.startswith("add"):
1024 other = "remove" + m.name[3:]
1025 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001026 error(clazz, m, "L2", "Missing remove method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001027 if m.name.startswith("remove") and not m.name.startswith("removeAll"):
1028 other = "add" + m.name[6:]
1029 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001030 error(clazz, m, "L2", "Missing add method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001031
1032 if m.name.startswith("register") or m.name.startswith("unregister"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001033 error(clazz, m, "L3", "Listener methods should be named add/remove")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001034
1035
1036def verify_sync(clazz):
1037 """Verify synchronized methods aren't exposed."""
1038 for m in clazz.methods:
1039 if "synchronized" in m.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001040 error(clazz, m, "M5", "Internal locks must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001041
1042
1043def verify_intent_builder(clazz):
1044 """Verify that Intent builders are createFooIntent() style."""
1045 if clazz.name == "Intent": return
1046
1047 for m in clazz.methods:
1048 if m.typ == "android.content.Intent":
1049 if m.name.startswith("create") and m.name.endswith("Intent"):
1050 pass
1051 else:
Adam Powell539ea122015-04-10 13:01:37 -07001052 warn(clazz, m, "FW1", "Methods creating an Intent should be named createFooIntent()")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001053
1054
1055def verify_helper_classes(clazz):
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001056 """Verify that helper classes are named consistently with what they extend.
1057 All developer extendable methods should be named onFoo()."""
1058 test_methods = False
Adrian Roosb787c182019-01-03 18:54:33 +01001059 if clazz.extends == "android.app.Service":
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001060 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001061 if not clazz.name.endswith("Service"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001062 error(clazz, None, "CL4", "Inconsistent class name; should be FooService")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001063
1064 found = False
1065 for f in clazz.fields:
1066 if f.name == "SERVICE_INTERFACE":
1067 found = True
1068 if f.value != clazz.fullname:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001069 error(clazz, f, "C4", "Inconsistent interface constant; expected '%s'" % (clazz.fullname))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001070
Adrian Roosb787c182019-01-03 18:54:33 +01001071 if clazz.extends == "android.content.ContentProvider":
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001072 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001073 if not clazz.name.endswith("Provider"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001074 error(clazz, None, "CL4", "Inconsistent class name; should be FooProvider")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001075
1076 found = False
1077 for f in clazz.fields:
1078 if f.name == "PROVIDER_INTERFACE":
1079 found = True
1080 if f.value != clazz.fullname:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001081 error(clazz, f, "C4", "Inconsistent interface constant; expected '%s'" % (clazz.fullname))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001082
Adrian Roosb787c182019-01-03 18:54:33 +01001083 if clazz.extends == "android.content.BroadcastReceiver":
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001084 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001085 if not clazz.name.endswith("Receiver"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001086 error(clazz, None, "CL4", "Inconsistent class name; should be FooReceiver")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001087
Adrian Roosb787c182019-01-03 18:54:33 +01001088 if clazz.extends == "android.app.Activity":
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001089 test_methods = True
1090 if not clazz.name.endswith("Activity"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001091 error(clazz, None, "CL4", "Inconsistent class name; should be FooActivity")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001092
1093 if test_methods:
1094 for m in clazz.methods:
1095 if "final" in m.split: continue
1096 if not re.match("on[A-Z]", m.name):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001097 if "abstract" in m.split:
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001098 warn(clazz, m, None, "Methods implemented by developers should be named onFoo()")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001099 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001100 warn(clazz, m, None, "If implemented by developer, should be named onFoo(); otherwise consider marking final")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001101
1102
1103def verify_builder(clazz):
1104 """Verify builder classes.
1105 Methods should return the builder to enable chaining."""
Adrian Roosb787c182019-01-03 18:54:33 +01001106 if clazz.extends: return
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001107 if not clazz.name.endswith("Builder"): return
1108
1109 if clazz.name != "Builder":
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001110 warn(clazz, None, None, "Builder should be defined as inner class")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001111
1112 has_build = False
1113 for m in clazz.methods:
1114 if m.name == "build":
1115 has_build = True
1116 continue
1117
1118 if m.name.startswith("get"): continue
1119 if m.name.startswith("clear"): continue
1120
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001121 if m.name.startswith("with"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001122 warn(clazz, m, None, "Builder methods names should use setFoo() style")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001123
1124 if m.name.startswith("set"):
1125 if not m.typ.endswith(clazz.fullname):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001126 warn(clazz, m, "M4", "Methods must return the builder object")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001127
1128 if not has_build:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001129 warn(clazz, None, None, "Missing build() method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001130
Adrian Roosdeb0ff22019-02-27 23:58:13 +01001131 if "final" not in clazz.split:
1132 error(clazz, None, None, "Builder should be final")
1133
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001134
1135def verify_aidl(clazz):
1136 """Catch people exposing raw AIDL."""
Adrian Roosb787c182019-01-03 18:54:33 +01001137 if clazz.extends == "android.os.Binder" or clazz.implements == "android.os.IInterface":
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001138 error(clazz, None, None, "Raw AIDL interfaces must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001139
1140
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001141def verify_internal(clazz):
1142 """Catch people exposing internal classes."""
1143 if clazz.pkg.name.startswith("com.android"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001144 error(clazz, None, None, "Internal classes must not be exposed")
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001145
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001146def layering_build_ranking(ranking_list):
1147 r = {}
1148 for rank, ps in enumerate(ranking_list):
1149 if not isinstance(ps, list):
1150 ps = [ps]
1151 for p in ps:
1152 rs = r
1153 for n in p.split('.'):
1154 if n not in rs:
1155 rs[n] = {}
1156 rs = rs[n]
1157 rs['-rank'] = rank
1158 return r
1159
1160LAYERING_PACKAGE_RANKING = layering_build_ranking([
1161 ["android.service","android.accessibilityservice","android.inputmethodservice","android.printservice","android.appwidget","android.webkit","android.preference","android.gesture","android.print"],
1162 "android.app",
1163 "android.widget",
1164 "android.view",
1165 "android.animation",
1166 "android.provider",
1167 ["android.content","android.graphics.drawable"],
1168 "android.database",
1169 "android.text",
1170 "android.graphics",
1171 "android.os",
1172 "android.util"
1173])
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001174
1175def verify_layering(clazz):
1176 """Catch package layering violations.
1177 For example, something in android.os depending on android.app."""
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001178
1179 def rank(p):
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001180 r = None
1181 l = LAYERING_PACKAGE_RANKING
1182 for n in p.split('.'):
1183 if n in l:
1184 l = l[n]
1185 if '-rank' in l:
1186 r = l['-rank']
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001187 else:
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001188 break
1189 return r
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001190
1191 cr = rank(clazz.pkg.name)
1192 if cr is None: return
1193
1194 for f in clazz.fields:
1195 ir = rank(f.typ)
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001196 if ir is not None and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001197 warn(clazz, f, "FW6", "Field type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001198
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001199 for m in itertools.chain(clazz.methods, clazz.ctors):
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001200 ir = rank(m.typ)
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001201 if ir is not None and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001202 warn(clazz, m, "FW6", "Method return type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001203 for arg in m.args:
1204 ir = rank(arg)
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001205 if ir is not None and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001206 warn(clazz, m, "FW6", "Method argument type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001207
1208
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001209def verify_boolean(clazz):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001210 """Verifies that boolean accessors are named correctly.
1211 For example, hasFoo() and setHasFoo()."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001212
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001213 def is_get(m): return len(m.args) == 0 and m.typ == "boolean"
1214 def is_set(m): return len(m.args) == 1 and m.args[0] == "boolean"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001215
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001216 gets = [ m for m in clazz.methods if is_get(m) ]
1217 sets = [ m for m in clazz.methods if is_set(m) ]
1218
1219 def error_if_exists(methods, trigger, expected, actual):
1220 for m in methods:
1221 if m.name == actual:
1222 error(clazz, m, "M6", "Symmetric method for %s must be named %s" % (trigger, expected))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001223
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001224 for m in clazz.methods:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001225 if is_get(m):
1226 if re.match("is[A-Z]", m.name):
1227 target = m.name[2:]
1228 expected = "setIs" + target
1229 error_if_exists(sets, m.name, expected, "setHas" + target)
1230 elif re.match("has[A-Z]", m.name):
1231 target = m.name[3:]
1232 expected = "setHas" + target
1233 error_if_exists(sets, m.name, expected, "setIs" + target)
1234 error_if_exists(sets, m.name, expected, "set" + target)
1235 elif re.match("get[A-Z]", m.name):
1236 target = m.name[3:]
1237 expected = "set" + target
1238 error_if_exists(sets, m.name, expected, "setIs" + target)
1239 error_if_exists(sets, m.name, expected, "setHas" + target)
1240
1241 if is_set(m):
1242 if re.match("set[A-Z]", m.name):
1243 target = m.name[3:]
1244 expected = "get" + target
1245 error_if_exists(sets, m.name, expected, "is" + target)
1246 error_if_exists(sets, m.name, expected, "has" + target)
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001247
1248
1249def verify_collections(clazz):
1250 """Verifies that collection types are interfaces."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001251 if clazz.fullname == "android.os.Bundle": return
1252
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001253 bad = ["java.util.Vector", "java.util.LinkedList", "java.util.ArrayList", "java.util.Stack",
1254 "java.util.HashMap", "java.util.HashSet", "android.util.ArraySet", "android.util.ArrayMap"]
1255 for m in clazz.methods:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001256 if m.typ in bad:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001257 error(clazz, m, "CL2", "Return type is concrete collection; must be higher-level interface")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001258 for arg in m.args:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001259 if arg in bad:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001260 error(clazz, m, "CL2", "Argument is concrete collection; must be higher-level interface")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001261
1262
1263def verify_flags(clazz):
1264 """Verifies that flags are non-overlapping."""
1265 known = collections.defaultdict(int)
1266 for f in clazz.fields:
1267 if "FLAG_" in f.name:
1268 try:
1269 val = int(f.value)
1270 except:
1271 continue
1272
1273 scope = f.name[0:f.name.index("FLAG_")]
1274 if val & known[scope]:
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001275 warn(clazz, f, "C1", "Found overlapping flag constant value")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001276 known[scope] |= val
1277
1278
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001279def verify_exception(clazz):
1280 """Verifies that methods don't throw generic exceptions."""
1281 for m in clazz.methods:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001282 for t in m.throws:
1283 if t in ["java.lang.Exception", "java.lang.Throwable", "java.lang.Error"]:
1284 error(clazz, m, "S1", "Methods must not throw generic exceptions")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001285
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001286 if t in ["android.os.RemoteException"]:
1287 if clazz.name == "android.content.ContentProviderClient": continue
1288 if clazz.name == "android.os.Binder": continue
1289 if clazz.name == "android.os.IBinder": continue
Jeff Sharkey331279b2016-02-29 16:02:02 -07001290
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001291 error(clazz, m, "FW9", "Methods calling into system server should rethrow RemoteException as RuntimeException")
1292
1293 if len(m.args) == 0 and t in ["java.lang.IllegalArgumentException", "java.lang.NullPointerException"]:
1294 warn(clazz, m, "S1", "Methods taking no arguments should throw IllegalStateException")
Jeff Sharkey331279b2016-02-29 16:02:02 -07001295
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001296GOOGLE_IGNORECASE = re.compile("google", re.IGNORECASE)
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001297
1298def verify_google(clazz):
1299 """Verifies that APIs never reference Google."""
1300
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001301 if GOOGLE_IGNORECASE.search(clazz.raw) is not None:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001302 error(clazz, None, None, "Must never reference Google")
1303
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001304 for test in clazz.ctors, clazz.fields, clazz.methods:
1305 for t in test:
1306 if GOOGLE_IGNORECASE.search(t.raw) is not None:
1307 error(clazz, t, None, "Must never reference Google")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001308
1309
1310def verify_bitset(clazz):
1311 """Verifies that we avoid using heavy BitSet."""
1312
1313 for f in clazz.fields:
1314 if f.typ == "java.util.BitSet":
1315 error(clazz, f, None, "Field type must not be heavy BitSet")
1316
1317 for m in clazz.methods:
1318 if m.typ == "java.util.BitSet":
1319 error(clazz, m, None, "Return type must not be heavy BitSet")
1320 for arg in m.args:
1321 if arg == "java.util.BitSet":
1322 error(clazz, m, None, "Argument type must not be heavy BitSet")
1323
1324
1325def verify_manager(clazz):
1326 """Verifies that FooManager is only obtained from Context."""
1327
1328 if not clazz.name.endswith("Manager"): return
1329
1330 for c in clazz.ctors:
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001331 error(clazz, c, None, "Managers must always be obtained from Context; no direct constructors")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001332
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001333 for m in clazz.methods:
1334 if m.typ == clazz.fullname:
1335 error(clazz, m, None, "Managers must always be obtained from Context")
1336
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001337
1338def verify_boxed(clazz):
1339 """Verifies that methods avoid boxed primitives."""
1340
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001341 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 -08001342
1343 for c in clazz.ctors:
1344 for arg in c.args:
1345 if arg in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001346 error(clazz, c, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001347
1348 for f in clazz.fields:
1349 if f.typ in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001350 error(clazz, f, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001351
1352 for m in clazz.methods:
1353 if m.typ in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001354 error(clazz, m, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001355 for arg in m.args:
1356 if arg in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001357 error(clazz, m, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001358
1359
1360def verify_static_utils(clazz):
1361 """Verifies that helper classes can't be constructed."""
1362 if clazz.fullname.startswith("android.opengl"): return
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001363 if clazz.fullname.startswith("android.R"): return
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001364
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001365 # Only care about classes with default constructors
1366 if len(clazz.ctors) == 1 and len(clazz.ctors[0].args) == 0:
1367 test = []
1368 test.extend(clazz.fields)
1369 test.extend(clazz.methods)
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001370
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001371 if len(test) == 0: return
1372 for t in test:
1373 if "static" not in t.split:
1374 return
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001375
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001376 error(clazz, None, None, "Fully-static utility classes must not have constructor")
1377
1378
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001379def verify_overload_args(clazz):
1380 """Verifies that method overloads add new arguments at the end."""
1381 if clazz.fullname.startswith("android.opengl"): return
1382
1383 overloads = collections.defaultdict(list)
1384 for m in clazz.methods:
1385 if "deprecated" in m.split: continue
1386 overloads[m.name].append(m)
1387
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001388 for name, methods in overloads.items():
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001389 if len(methods) <= 1: continue
1390
1391 # Look for arguments common across all overloads
1392 def cluster(args):
1393 count = collections.defaultdict(int)
1394 res = set()
1395 for i in range(len(args)):
1396 a = args[i]
1397 res.add("%s#%d" % (a, count[a]))
1398 count[a] += 1
1399 return res
1400
1401 common_args = cluster(methods[0].args)
1402 for m in methods:
1403 common_args = common_args & cluster(m.args)
1404
1405 if len(common_args) == 0: continue
1406
1407 # Require that all common arguments are present at start of signature
1408 locked_sig = None
1409 for m in methods:
1410 sig = m.args[0:len(common_args)]
1411 if not common_args.issubset(cluster(sig)):
1412 warn(clazz, m, "M2", "Expected common arguments [%s] at beginning of overloaded method" % (", ".join(common_args)))
1413 elif not locked_sig:
1414 locked_sig = sig
1415 elif locked_sig != sig:
1416 error(clazz, m, "M2", "Expected consistent argument ordering between overloads: %s..." % (", ".join(locked_sig)))
1417
1418
1419def verify_callback_handlers(clazz):
1420 """Verifies that methods adding listener/callback have overload
1421 for specifying delivery thread."""
1422
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001423 # Ignore UI packages which assume main thread
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001424 skip = [
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001425 "animation",
1426 "view",
1427 "graphics",
1428 "transition",
1429 "widget",
1430 "webkit",
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001431 ]
1432 for s in skip:
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001433 if s in clazz.pkg.name_path: return
1434 if s in clazz.extends_path: return
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001435
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001436 # Ignore UI classes which assume main thread
1437 if "app" in clazz.pkg.name_path or "app" in clazz.extends_path:
1438 for s in ["ActionBar","Dialog","Application","Activity","Fragment","Loader"]:
1439 if s in clazz.fullname: return
1440 if "content" in clazz.pkg.name_path or "content" in clazz.extends_path:
1441 for s in ["Loader"]:
1442 if s in clazz.fullname: return
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001443
1444 found = {}
1445 by_name = collections.defaultdict(list)
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001446 examine = clazz.ctors + clazz.methods
1447 for m in examine:
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001448 if m.name.startswith("unregister"): continue
1449 if m.name.startswith("remove"): continue
1450 if re.match("on[A-Z]+", m.name): continue
1451
1452 by_name[m.name].append(m)
1453
1454 for a in m.args:
1455 if a.endswith("Listener") or a.endswith("Callback") or a.endswith("Callbacks"):
1456 found[m.name] = m
1457
1458 for f in found.values():
1459 takes_handler = False
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001460 takes_exec = False
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001461 for m in by_name[f.name]:
1462 if "android.os.Handler" in m.args:
1463 takes_handler = True
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001464 if "java.util.concurrent.Executor" in m.args:
1465 takes_exec = True
1466 if not takes_exec:
1467 warn(clazz, f, "L1", "Registration methods should have overload that accepts delivery Executor")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001468
1469
1470def verify_context_first(clazz):
1471 """Verifies that methods accepting a Context keep it the first argument."""
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001472 examine = clazz.ctors + clazz.methods
1473 for m in examine:
1474 if len(m.args) > 1 and m.args[0] != "android.content.Context":
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001475 if "android.content.Context" in m.args[1:]:
1476 error(clazz, m, "M3", "Context is distinct, so it must be the first argument")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001477 if len(m.args) > 1 and m.args[0] != "android.content.ContentResolver":
1478 if "android.content.ContentResolver" in m.args[1:]:
1479 error(clazz, m, "M3", "ContentResolver is distinct, so it must be the first argument")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001480
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001481
1482def verify_listener_last(clazz):
1483 """Verifies that methods accepting a Listener or Callback keep them as last arguments."""
1484 examine = clazz.ctors + clazz.methods
1485 for m in examine:
1486 if "Listener" in m.name or "Callback" in m.name: continue
1487 found = False
1488 for a in m.args:
1489 if a.endswith("Callback") or a.endswith("Callbacks") or a.endswith("Listener"):
1490 found = True
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001491 elif found:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001492 warn(clazz, m, "M3", "Listeners should always be at end of argument list")
1493
1494
1495def verify_resource_names(clazz):
1496 """Verifies that resource names have consistent case."""
1497 if not re.match("android\.R\.[a-z]+", clazz.fullname): return
1498
1499 # Resources defined by files are foo_bar_baz
1500 if clazz.name in ["anim","animator","color","dimen","drawable","interpolator","layout","transition","menu","mipmap","string","plurals","raw","xml"]:
1501 for f in clazz.fields:
Jeff Sharkeyeff9e222018-09-27 16:29:25 -06001502 if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue
1503 if f.name.startswith("config_"):
1504 error(clazz, f, None, "Expected config name to be config_fooBarBaz style")
1505
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001506 if re.match("[a-z1-9_]+$", f.name): continue
1507 error(clazz, f, None, "Expected resource name in this class to be foo_bar_baz style")
1508
1509 # Resources defined inside files are fooBarBaz
1510 if clazz.name in ["array","attr","id","bool","fraction","integer"]:
1511 for f in clazz.fields:
1512 if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue
1513 if re.match("layout_[a-z][a-zA-Z1-9]*$", f.name): continue
1514 if re.match("state_[a-z_]*$", f.name): continue
1515
1516 if re.match("[a-z][a-zA-Z1-9]*$", f.name): continue
1517 error(clazz, f, "C7", "Expected resource name in this class to be fooBarBaz style")
1518
1519 # Styles are FooBar_Baz
1520 if clazz.name in ["style"]:
1521 for f in clazz.fields:
1522 if re.match("[A-Z][A-Za-z1-9]+(_[A-Z][A-Za-z1-9]+?)*$", f.name): continue
1523 error(clazz, f, "C7", "Expected resource name in this class to be FooBar_Baz style")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001524
1525
Jeff Sharkey331279b2016-02-29 16:02:02 -07001526def verify_files(clazz):
1527 """Verifies that methods accepting File also accept streams."""
1528
1529 has_file = set()
1530 has_stream = set()
1531
1532 test = []
1533 test.extend(clazz.ctors)
1534 test.extend(clazz.methods)
1535
1536 for m in test:
1537 if "java.io.File" in m.args:
1538 has_file.add(m)
1539 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:
1540 has_stream.add(m.name)
1541
1542 for m in has_file:
1543 if m.name not in has_stream:
1544 warn(clazz, m, "M10", "Methods accepting File should also accept FileDescriptor or streams")
1545
1546
Jeff Sharkey70168dd2016-03-30 21:47:16 -06001547def verify_manager_list(clazz):
1548 """Verifies that managers return List<? extends Parcelable> instead of arrays."""
1549
1550 if not clazz.name.endswith("Manager"): return
1551
1552 for m in clazz.methods:
1553 if m.typ.startswith("android.") and m.typ.endswith("[]"):
1554 warn(clazz, m, None, "Methods should return List<? extends Parcelable> instead of Parcelable[] to support ParceledListSlice under the hood")
1555
1556
Jeff Sharkey26c80902016-12-21 13:41:17 -07001557def verify_abstract_inner(clazz):
1558 """Verifies that abstract inner classes are static."""
1559
1560 if re.match(".+?\.[A-Z][^\.]+\.[A-Z]", clazz.fullname):
Adrian Roosb787c182019-01-03 18:54:33 +01001561 if "abstract" in clazz.split and "static" not in clazz.split:
Jeff Sharkey26c80902016-12-21 13:41:17 -07001562 warn(clazz, None, None, "Abstract inner classes should be static to improve testability")
1563
1564
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001565def verify_runtime_exceptions(clazz):
1566 """Verifies that runtime exceptions aren't listed in throws."""
1567
1568 banned = [
1569 "java.lang.NullPointerException",
1570 "java.lang.ClassCastException",
1571 "java.lang.IndexOutOfBoundsException",
1572 "java.lang.reflect.UndeclaredThrowableException",
1573 "java.lang.reflect.MalformedParametersException",
1574 "java.lang.reflect.MalformedParameterizedTypeException",
1575 "java.lang.invoke.WrongMethodTypeException",
1576 "java.lang.EnumConstantNotPresentException",
1577 "java.lang.IllegalMonitorStateException",
1578 "java.lang.SecurityException",
1579 "java.lang.UnsupportedOperationException",
1580 "java.lang.annotation.AnnotationTypeMismatchException",
1581 "java.lang.annotation.IncompleteAnnotationException",
1582 "java.lang.TypeNotPresentException",
1583 "java.lang.IllegalStateException",
1584 "java.lang.ArithmeticException",
1585 "java.lang.IllegalArgumentException",
1586 "java.lang.ArrayStoreException",
1587 "java.lang.NegativeArraySizeException",
1588 "java.util.MissingResourceException",
1589 "java.util.EmptyStackException",
1590 "java.util.concurrent.CompletionException",
1591 "java.util.concurrent.RejectedExecutionException",
1592 "java.util.IllformedLocaleException",
1593 "java.util.ConcurrentModificationException",
1594 "java.util.NoSuchElementException",
1595 "java.io.UncheckedIOException",
1596 "java.time.DateTimeException",
1597 "java.security.ProviderException",
1598 "java.nio.BufferUnderflowException",
1599 "java.nio.BufferOverflowException",
1600 ]
1601
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001602 examine = clazz.ctors + clazz.methods
1603 for m in examine:
1604 for t in m.throws:
1605 if t in banned:
1606 error(clazz, m, None, "Methods must not mention RuntimeException subclasses in throws clauses")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001607
1608
1609def verify_error(clazz):
1610 """Verifies that we always use Exception instead of Error."""
1611 if not clazz.extends: return
1612 if clazz.extends.endswith("Error"):
1613 error(clazz, None, None, "Trouble must be reported through an Exception, not Error")
1614 if clazz.extends.endswith("Exception") and not clazz.name.endswith("Exception"):
1615 error(clazz, None, None, "Exceptions must be named FooException")
1616
1617
1618def verify_units(clazz):
1619 """Verifies that we use consistent naming for units."""
1620
1621 # If we find K, recommend replacing with V
1622 bad = {
1623 "Ns": "Nanos",
1624 "Ms": "Millis or Micros",
1625 "Sec": "Seconds", "Secs": "Seconds",
1626 "Hr": "Hours", "Hrs": "Hours",
1627 "Mo": "Months", "Mos": "Months",
1628 "Yr": "Years", "Yrs": "Years",
1629 "Byte": "Bytes", "Space": "Bytes",
1630 }
1631
1632 for m in clazz.methods:
1633 if m.typ not in ["short","int","long"]: continue
1634 for k, v in bad.iteritems():
1635 if m.name.endswith(k):
1636 error(clazz, m, None, "Expected method name units to be " + v)
1637 if m.name.endswith("Nanos") or m.name.endswith("Micros"):
1638 warn(clazz, m, None, "Returned time values are strongly encouraged to be in milliseconds unless you need the extra precision")
1639 if m.name.endswith("Seconds"):
1640 error(clazz, m, None, "Returned time values must be in milliseconds")
1641
1642 for m in clazz.methods:
1643 typ = m.typ
1644 if typ == "void":
1645 if len(m.args) != 1: continue
1646 typ = m.args[0]
1647
1648 if m.name.endswith("Fraction") and typ != "float":
1649 error(clazz, m, None, "Fractions must use floats")
1650 if m.name.endswith("Percentage") and typ != "int":
1651 error(clazz, m, None, "Percentage must use ints")
1652
1653
1654def verify_closable(clazz):
1655 """Verifies that classes are AutoClosable."""
Adrian Roosb787c182019-01-03 18:54:33 +01001656 if clazz.implements == "java.lang.AutoCloseable": return
1657 if clazz.implements == "java.io.Closeable": return
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001658
1659 for m in clazz.methods:
1660 if len(m.args) > 0: continue
1661 if m.name in ["close","release","destroy","finish","finalize","disconnect","shutdown","stop","free","quit"]:
1662 warn(clazz, m, None, "Classes that release resources should implement AutoClosable and CloseGuard")
1663 return
1664
1665
Jake Wharton9e6738f2017-08-23 11:59:55 -04001666def verify_member_name_not_kotlin_keyword(clazz):
1667 """Prevent method names which are keywords in Kotlin."""
1668
1669 # https://kotlinlang.org/docs/reference/keyword-reference.html#hard-keywords
1670 # This list does not include Java keywords as those are already impossible to use.
1671 keywords = [
1672 'as',
1673 'fun',
1674 'in',
1675 'is',
1676 'object',
1677 'typealias',
1678 'val',
1679 'var',
1680 'when',
1681 ]
1682
1683 for m in clazz.methods:
1684 if m.name in keywords:
1685 error(clazz, m, None, "Method name must not be a Kotlin keyword")
1686 for f in clazz.fields:
1687 if f.name in keywords:
1688 error(clazz, f, None, "Field name must not be a Kotlin keyword")
1689
1690
1691def verify_method_name_not_kotlin_operator(clazz):
1692 """Warn about method names which become operators in Kotlin."""
1693
1694 binary = set()
1695
1696 def unique_binary_op(m, op):
1697 if op in binary:
1698 error(clazz, m, None, "Only one of '{0}' and '{0}Assign' methods should be present for Kotlin".format(op))
1699 binary.add(op)
1700
1701 for m in clazz.methods:
Adrian Roosd1e38922019-01-14 15:44:15 +01001702 if 'static' in m.split or 'operator' in m.split:
Jake Wharton9e6738f2017-08-23 11:59:55 -04001703 continue
1704
1705 # https://kotlinlang.org/docs/reference/operator-overloading.html#unary-prefix-operators
1706 if m.name in ['unaryPlus', 'unaryMinus', 'not'] and len(m.args) == 0:
1707 warn(clazz, m, None, "Method can be invoked as a unary operator from Kotlin")
1708
1709 # https://kotlinlang.org/docs/reference/operator-overloading.html#increments-and-decrements
1710 if m.name in ['inc', 'dec'] and len(m.args) == 0 and m.typ != 'void':
1711 # This only applies if the return type is the same or a subtype of the enclosing class, but we have no
1712 # practical way of checking that relationship here.
1713 warn(clazz, m, None, "Method can be invoked as a pre/postfix inc/decrement operator from Kotlin")
1714
1715 # https://kotlinlang.org/docs/reference/operator-overloading.html#arithmetic
1716 if m.name in ['plus', 'minus', 'times', 'div', 'rem', 'mod', 'rangeTo'] and len(m.args) == 1:
1717 warn(clazz, m, None, "Method can be invoked as a binary operator from Kotlin")
1718 unique_binary_op(m, m.name)
1719
1720 # https://kotlinlang.org/docs/reference/operator-overloading.html#in
1721 if m.name == 'contains' and len(m.args) == 1 and m.typ == 'boolean':
1722 warn(clazz, m, None, "Method can be invoked as a 'in' operator from Kotlin")
1723
1724 # https://kotlinlang.org/docs/reference/operator-overloading.html#indexed
1725 if (m.name == 'get' and len(m.args) > 0) or (m.name == 'set' and len(m.args) > 1):
1726 warn(clazz, m, None, "Method can be invoked with an indexing operator from Kotlin")
1727
1728 # https://kotlinlang.org/docs/reference/operator-overloading.html#invoke
1729 if m.name == 'invoke':
1730 warn(clazz, m, None, "Method can be invoked with function call syntax from Kotlin")
1731
1732 # https://kotlinlang.org/docs/reference/operator-overloading.html#assignments
1733 if m.name in ['plusAssign', 'minusAssign', 'timesAssign', 'divAssign', 'remAssign', 'modAssign'] \
1734 and len(m.args) == 1 \
1735 and m.typ == 'void':
1736 warn(clazz, m, None, "Method can be invoked as a compound assignment operator from Kotlin")
1737 unique_binary_op(m, m.name[:-6]) # Remove 'Assign' suffix
1738
1739
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001740def verify_collections_over_arrays(clazz):
1741 """Warn that [] should be Collections."""
1742
Adrian Roosb787c182019-01-03 18:54:33 +01001743 if "@interface" in clazz.split:
1744 return
1745
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001746 safe = ["java.lang.String[]","byte[]","short[]","int[]","long[]","float[]","double[]","boolean[]","char[]"]
1747 for m in clazz.methods:
1748 if m.typ.endswith("[]") and m.typ not in safe:
1749 warn(clazz, m, None, "Method should return Collection<> (or subclass) instead of raw array")
1750 for arg in m.args:
1751 if arg.endswith("[]") and arg not in safe:
1752 warn(clazz, m, None, "Method argument should be Collection<> (or subclass) instead of raw array")
1753
1754
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001755def verify_user_handle(clazz):
1756 """Methods taking UserHandle should be ForUser or AsUser."""
1757 if clazz.name.endswith("Listener") or clazz.name.endswith("Callback") or clazz.name.endswith("Callbacks"): return
1758 if clazz.fullname == "android.app.admin.DeviceAdminReceiver": return
1759 if clazz.fullname == "android.content.pm.LauncherApps": return
1760 if clazz.fullname == "android.os.UserHandle": return
1761 if clazz.fullname == "android.os.UserManager": return
1762
1763 for m in clazz.methods:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001764 if re.match("on[A-Z]+", m.name): continue
Jeff Sharkeya8e5df02018-11-27 17:33:42 -07001765
1766 has_arg = "android.os.UserHandle" in m.args
1767 has_name = m.name.endswith("AsUser") or m.name.endswith("ForUser")
1768
1769 if clazz.fullname.endswith("Manager") and has_arg:
1770 warn(clazz, m, None, "When a method overload is needed to target a specific "
1771 "UserHandle, callers should be directed to use "
1772 "Context.createPackageContextAsUser() and re-obtain the relevant "
1773 "Manager, and no new API should be added")
1774 elif has_arg and not has_name:
1775 warn(clazz, m, None, "Method taking UserHandle should be named 'doFooAsUser' "
1776 "or 'queryFooForUser'")
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001777
1778
1779def verify_params(clazz):
1780 """Parameter classes should be 'Params'."""
1781 if clazz.name.endswith("Params"): return
1782 if clazz.fullname == "android.app.ActivityOptions": return
1783 if clazz.fullname == "android.app.BroadcastOptions": return
1784 if clazz.fullname == "android.os.Bundle": return
1785 if clazz.fullname == "android.os.BaseBundle": return
1786 if clazz.fullname == "android.os.PersistableBundle": return
1787
1788 bad = ["Param","Parameter","Parameters","Args","Arg","Argument","Arguments","Options","Bundle"]
1789 for b in bad:
1790 if clazz.name.endswith(b):
1791 error(clazz, None, None, "Classes holding a set of parameters should be called 'FooParams'")
1792
1793
1794def verify_services(clazz):
1795 """Service name should be FOO_BAR_SERVICE = 'foo_bar'."""
1796 if clazz.fullname != "android.content.Context": return
1797
1798 for f in clazz.fields:
1799 if f.typ != "java.lang.String": continue
1800 found = re.match(r"([A-Z_]+)_SERVICE", f.name)
1801 if found:
1802 expected = found.group(1).lower()
1803 if f.value != expected:
1804 error(clazz, f, "C4", "Inconsistent service value; expected '%s'" % (expected))
1805
1806
1807def verify_tense(clazz):
1808 """Verify tenses of method names."""
1809 if clazz.fullname.startswith("android.opengl"): return
1810
1811 for m in clazz.methods:
1812 if m.name.endswith("Enable"):
1813 warn(clazz, m, None, "Unexpected tense; probably meant 'enabled'")
1814
1815
Jeff Sharkeyee21f692018-02-16 13:29:29 -07001816def verify_icu(clazz):
1817 """Verifies that richer ICU replacements are used."""
1818 better = {
1819 "java.util.TimeZone": "android.icu.util.TimeZone",
1820 "java.util.Calendar": "android.icu.util.Calendar",
1821 "java.util.Locale": "android.icu.util.ULocale",
1822 "java.util.ResourceBundle": "android.icu.util.UResourceBundle",
1823 "java.util.SimpleTimeZone": "android.icu.util.SimpleTimeZone",
1824 "java.util.StringTokenizer": "android.icu.util.StringTokenizer",
1825 "java.util.GregorianCalendar": "android.icu.util.GregorianCalendar",
1826 "java.lang.Character": "android.icu.lang.UCharacter",
1827 "java.text.BreakIterator": "android.icu.text.BreakIterator",
1828 "java.text.Collator": "android.icu.text.Collator",
1829 "java.text.DecimalFormatSymbols": "android.icu.text.DecimalFormatSymbols",
1830 "java.text.NumberFormat": "android.icu.text.NumberFormat",
1831 "java.text.DateFormatSymbols": "android.icu.text.DateFormatSymbols",
1832 "java.text.DateFormat": "android.icu.text.DateFormat",
1833 "java.text.SimpleDateFormat": "android.icu.text.SimpleDateFormat",
1834 "java.text.MessageFormat": "android.icu.text.MessageFormat",
1835 "java.text.DecimalFormat": "android.icu.text.DecimalFormat",
1836 }
1837
1838 for m in clazz.ctors + clazz.methods:
1839 types = []
1840 types.extend(m.typ)
1841 types.extend(m.args)
1842 for arg in types:
1843 if arg in better:
1844 warn(clazz, m, None, "Type %s should be replaced with richer ICU type %s" % (arg, better[arg]))
1845
1846
1847def verify_clone(clazz):
1848 """Verify that clone() isn't implemented; see EJ page 61."""
1849 for m in clazz.methods:
1850 if m.name == "clone":
1851 error(clazz, m, None, "Provide an explicit copy constructor instead of implementing clone()")
1852
1853
Jeff Sharkeyeff9e222018-09-27 16:29:25 -06001854def verify_pfd(clazz):
1855 """Verify that android APIs use PFD over FD."""
1856 examine = clazz.ctors + clazz.methods
1857 for m in examine:
1858 if m.typ == "java.io.FileDescriptor":
1859 error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
1860 if m.typ == "int":
1861 if "Fd" in m.name or "FD" in m.name or "FileDescriptor" in m.name:
1862 error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
1863 for arg in m.args:
1864 if arg == "java.io.FileDescriptor":
1865 error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
1866
1867 for f in clazz.fields:
1868 if f.typ == "java.io.FileDescriptor":
1869 error(clazz, f, "FW11", "Must use ParcelFileDescriptor")
1870
1871
1872def verify_numbers(clazz):
1873 """Discourage small numbers types like short and byte."""
1874
1875 discouraged = ["short","byte"]
1876
1877 for c in clazz.ctors:
1878 for arg in c.args:
1879 if arg in discouraged:
1880 warn(clazz, c, "FW12", "Should avoid odd sized primitives; use int instead")
1881
1882 for f in clazz.fields:
1883 if f.typ in discouraged:
1884 warn(clazz, f, "FW12", "Should avoid odd sized primitives; use int instead")
1885
1886 for m in clazz.methods:
1887 if m.typ in discouraged:
1888 warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead")
1889 for arg in m.args:
1890 if arg in discouraged:
1891 warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead")
1892
Adrian Roos80545ef2019-02-27 16:45:00 +01001893PRIMITIVES = {"void", "int", "float", "boolean", "short", "char", "byte", "long", "double"}
1894
1895def verify_nullability(clazz):
1896 """Catches missing nullability annotations"""
1897
1898 for f in clazz.fields:
1899 if f.value is not None and 'static' in f.split and 'final' in f.split:
1900 continue # Nullability of constants can be inferred.
1901 if f.typ not in PRIMITIVES and not has_nullability(f.annotations):
1902 error(clazz, f, "M12", "Field must be marked either @NonNull or @Nullable")
1903
1904 for c in clazz.ctors:
1905 verify_nullability_args(clazz, c)
1906
1907 for m in clazz.methods:
1908 if m.name == "writeToParcel" or m.name == "onReceive":
1909 continue # Parcelable.writeToParcel() and BroadcastReceiver.onReceive() are not yet annotated
1910
1911 if m.typ not in PRIMITIVES and not has_nullability(m.annotations):
1912 error(clazz, m, "M12", "Return value must be marked either @NonNull or @Nullable")
1913 verify_nullability_args(clazz, m)
1914
1915def verify_nullability_args(clazz, m):
1916 for i, arg in enumerate(m.detailed_args):
1917 if arg.type not in PRIMITIVES and not has_nullability(arg.annotations):
1918 error(clazz, m, "M12", "Argument %d must be marked either @NonNull or @Nullable" % (i+1,))
1919
1920def has_nullability(annotations):
1921 return "@NonNull" in annotations or "@Nullable" in annotations
Jeff Sharkeyeff9e222018-09-27 16:29:25 -06001922
1923def verify_singleton(clazz):
1924 """Catch singleton objects with constructors."""
1925
1926 singleton = False
1927 for m in clazz.methods:
1928 if m.name.startswith("get") and m.name.endswith("Instance") and " static " in m.raw:
1929 singleton = True
1930
1931 if singleton:
1932 for c in clazz.ctors:
1933 error(clazz, c, None, "Singleton classes should use getInstance() methods")
1934
1935
1936
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06001937def is_interesting(clazz):
1938 """Test if given class is interesting from an Android PoV."""
1939
1940 if clazz.pkg.name.startswith("java"): return False
1941 if clazz.pkg.name.startswith("junit"): return False
1942 if clazz.pkg.name.startswith("org.apache"): return False
1943 if clazz.pkg.name.startswith("org.xml"): return False
1944 if clazz.pkg.name.startswith("org.json"): return False
1945 if clazz.pkg.name.startswith("org.w3c"): return False
1946 if clazz.pkg.name.startswith("android.icu."): return False
1947 return True
1948
1949
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001950def examine_clazz(clazz):
1951 """Find all style issues in the given class."""
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07001952
1953 notice(clazz)
1954
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06001955 if not is_interesting(clazz): return
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001956
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001957 verify_constants(clazz)
1958 verify_enums(clazz)
1959 verify_class_names(clazz)
1960 verify_method_names(clazz)
1961 verify_callbacks(clazz)
1962 verify_listeners(clazz)
1963 verify_actions(clazz)
1964 verify_extras(clazz)
1965 verify_equals(clazz)
1966 verify_parcelable(clazz)
1967 verify_protected(clazz)
1968 verify_fields(clazz)
1969 verify_register(clazz)
1970 verify_sync(clazz)
1971 verify_intent_builder(clazz)
1972 verify_helper_classes(clazz)
1973 verify_builder(clazz)
1974 verify_aidl(clazz)
1975 verify_internal(clazz)
1976 verify_layering(clazz)
1977 verify_boolean(clazz)
1978 verify_collections(clazz)
1979 verify_flags(clazz)
1980 verify_exception(clazz)
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07001981 if not ALLOW_GOOGLE: verify_google(clazz)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001982 verify_bitset(clazz)
1983 verify_manager(clazz)
1984 verify_boxed(clazz)
1985 verify_static_utils(clazz)
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001986 # verify_overload_args(clazz)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001987 verify_callback_handlers(clazz)
1988 verify_context_first(clazz)
1989 verify_listener_last(clazz)
1990 verify_resource_names(clazz)
Jeff Sharkey331279b2016-02-29 16:02:02 -07001991 verify_files(clazz)
Jeff Sharkey70168dd2016-03-30 21:47:16 -06001992 verify_manager_list(clazz)
Jeff Sharkey26c80902016-12-21 13:41:17 -07001993 verify_abstract_inner(clazz)
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001994 verify_runtime_exceptions(clazz)
1995 verify_error(clazz)
1996 verify_units(clazz)
1997 verify_closable(clazz)
Jake Wharton9e6738f2017-08-23 11:59:55 -04001998 verify_member_name_not_kotlin_keyword(clazz)
1999 verify_method_name_not_kotlin_operator(clazz)
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07002000 verify_collections_over_arrays(clazz)
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07002001 verify_user_handle(clazz)
2002 verify_params(clazz)
2003 verify_services(clazz)
2004 verify_tense(clazz)
Jeff Sharkeyee21f692018-02-16 13:29:29 -07002005 verify_icu(clazz)
2006 verify_clone(clazz)
Jeff Sharkeyeff9e222018-09-27 16:29:25 -06002007 verify_pfd(clazz)
2008 verify_numbers(clazz)
2009 verify_singleton(clazz)
Adrian Roos80545ef2019-02-27 16:45:00 +01002010 verify_nullability(clazz)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002011
2012
Adrian Roos038a0292018-12-19 17:11:21 +01002013def examine_stream(stream, base_stream=None, in_classes_with_base=[], out_classes_with_base=None):
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002014 """Find all style issues in the given API stream."""
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002015 global failures, noticed
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002016 failures = {}
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002017 noticed = {}
Adrian Roos038a0292018-12-19 17:11:21 +01002018 _parse_stream(stream, examine_clazz, base_f=base_stream,
2019 in_classes_with_base=in_classes_with_base,
2020 out_classes_with_base=out_classes_with_base)
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002021 return (failures, noticed)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002022
2023
2024def examine_api(api):
2025 """Find all style issues in the given parsed API."""
2026 global failures
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07002027 failures = {}
2028 for key in sorted(api.keys()):
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002029 examine_clazz(api[key])
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002030 return failures
2031
2032
Jeff Sharkey037458a2014-09-04 15:46:20 -07002033def verify_compat(cur, prev):
2034 """Find any incompatible API changes between two levels."""
2035 global failures
2036
2037 def class_exists(api, test):
2038 return test.fullname in api
2039
2040 def ctor_exists(api, clazz, test):
2041 for m in clazz.ctors:
2042 if m.ident == test.ident: return True
2043 return False
2044
2045 def all_methods(api, clazz):
2046 methods = list(clazz.methods)
2047 if clazz.extends is not None:
2048 methods.extend(all_methods(api, api[clazz.extends]))
2049 return methods
2050
2051 def method_exists(api, clazz, test):
2052 methods = all_methods(api, clazz)
2053 for m in methods:
2054 if m.ident == test.ident: return True
2055 return False
2056
2057 def field_exists(api, clazz, test):
2058 for f in clazz.fields:
2059 if f.ident == test.ident: return True
2060 return False
2061
2062 failures = {}
2063 for key in sorted(prev.keys()):
2064 prev_clazz = prev[key]
2065
2066 if not class_exists(cur, prev_clazz):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08002067 error(prev_clazz, None, None, "Class removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07002068 continue
2069
2070 cur_clazz = cur[key]
2071
2072 for test in prev_clazz.ctors:
2073 if not ctor_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08002074 error(prev_clazz, prev_ctor, None, "Constructor removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07002075
2076 methods = all_methods(prev, prev_clazz)
2077 for test in methods:
2078 if not method_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08002079 error(prev_clazz, test, None, "Method removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07002080
2081 for test in prev_clazz.fields:
2082 if not field_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08002083 error(prev_clazz, test, None, "Field removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07002084
2085 return failures
2086
2087
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002088def show_deprecations_at_birth(cur, prev):
2089 """Show API deprecations at birth."""
2090 global failures
2091
2092 # Remove all existing things so we're left with new
2093 for prev_clazz in prev.values():
2094 cur_clazz = cur[prev_clazz.fullname]
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06002095 if not is_interesting(cur_clazz): continue
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002096
2097 sigs = { i.ident: i for i in prev_clazz.ctors }
2098 cur_clazz.ctors = [ i for i in cur_clazz.ctors if i.ident not in sigs ]
2099 sigs = { i.ident: i for i in prev_clazz.methods }
2100 cur_clazz.methods = [ i for i in cur_clazz.methods if i.ident not in sigs ]
2101 sigs = { i.ident: i for i in prev_clazz.fields }
2102 cur_clazz.fields = [ i for i in cur_clazz.fields if i.ident not in sigs ]
2103
2104 # Forget about class entirely when nothing new
2105 if len(cur_clazz.ctors) == 0 and len(cur_clazz.methods) == 0 and len(cur_clazz.fields) == 0:
2106 del cur[prev_clazz.fullname]
2107
2108 for clazz in cur.values():
Adrian Roosb787c182019-01-03 18:54:33 +01002109 if "deprecated" in clazz.split and not clazz.fullname in prev:
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002110 error(clazz, None, None, "Found API deprecation at birth")
2111
2112 for i in clazz.ctors + clazz.methods + clazz.fields:
Adrian Roosb787c182019-01-03 18:54:33 +01002113 if "deprecated" in i.split:
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002114 error(clazz, i, None, "Found API deprecation at birth")
2115
2116 print "%s Deprecated at birth %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True),
2117 format(reset=True)))
2118 for f in sorted(failures):
2119 print failures[f]
2120 print
2121
2122
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06002123def show_stats(cur, prev):
2124 """Show API stats."""
2125
2126 stats = collections.defaultdict(int)
2127 for cur_clazz in cur.values():
2128 if not is_interesting(cur_clazz): continue
2129
2130 if cur_clazz.fullname not in prev:
2131 stats['new_classes'] += 1
2132 stats['new_ctors'] += len(cur_clazz.ctors)
2133 stats['new_methods'] += len(cur_clazz.methods)
2134 stats['new_fields'] += len(cur_clazz.fields)
2135 else:
2136 prev_clazz = prev[cur_clazz.fullname]
2137
2138 sigs = { i.ident: i for i in prev_clazz.ctors }
2139 ctors = len([ i for i in cur_clazz.ctors if i.ident not in sigs ])
2140 sigs = { i.ident: i for i in prev_clazz.methods }
2141 methods = len([ i for i in cur_clazz.methods if i.ident not in sigs ])
2142 sigs = { i.ident: i for i in prev_clazz.fields }
2143 fields = len([ i for i in cur_clazz.fields if i.ident not in sigs ])
2144
2145 if ctors + methods + fields > 0:
2146 stats['extend_classes'] += 1
2147 stats['extend_ctors'] += ctors
2148 stats['extend_methods'] += methods
2149 stats['extend_fields'] += fields
2150
2151 print "#", "".join([ k.ljust(20) for k in sorted(stats.keys()) ])
2152 print " ", "".join([ str(stats[k]).ljust(20) for k in sorted(stats.keys()) ])
2153
2154
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002155if __name__ == "__main__":
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002156 parser = argparse.ArgumentParser(description="Enforces common Android public API design \
2157 patterns. It ignores lint messages from a previous API level, if provided.")
2158 parser.add_argument("current.txt", type=argparse.FileType('r'), help="current.txt")
2159 parser.add_argument("previous.txt", nargs='?', type=argparse.FileType('r'), default=None,
2160 help="previous.txt")
Adrian Roos6eb57b02018-12-13 22:08:29 +01002161 parser.add_argument("--base-current", nargs='?', type=argparse.FileType('r'), default=None,
2162 help="The base current.txt to use when examining system-current.txt or"
2163 " test-current.txt")
2164 parser.add_argument("--base-previous", nargs='?', type=argparse.FileType('r'), default=None,
2165 help="The base previous.txt to use when examining system-previous.txt or"
2166 " test-previous.txt")
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002167 parser.add_argument("--no-color", action='store_const', const=True,
2168 help="Disable terminal colors")
2169 parser.add_argument("--allow-google", action='store_const', const=True,
2170 help="Allow references to Google")
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002171 parser.add_argument("--show-noticed", action='store_const', const=True,
2172 help="Show API changes noticed")
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002173 parser.add_argument("--show-deprecations-at-birth", action='store_const', const=True,
2174 help="Show API deprecations at birth")
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06002175 parser.add_argument("--show-stats", action='store_const', const=True,
2176 help="Show API stats")
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002177 args = vars(parser.parse_args())
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002178
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002179 if args['no_color']:
2180 USE_COLOR = False
2181
2182 if args['allow_google']:
2183 ALLOW_GOOGLE = True
2184
2185 current_file = args['current.txt']
Adrian Roos6eb57b02018-12-13 22:08:29 +01002186 base_current_file = args['base_current']
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002187 previous_file = args['previous.txt']
Adrian Roos6eb57b02018-12-13 22:08:29 +01002188 base_previous_file = args['base_previous']
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002189
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002190 if args['show_deprecations_at_birth']:
2191 with current_file as f:
2192 cur = _parse_stream(f)
2193 with previous_file as f:
2194 prev = _parse_stream(f)
2195 show_deprecations_at_birth(cur, prev)
2196 sys.exit()
2197
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06002198 if args['show_stats']:
2199 with current_file as f:
2200 cur = _parse_stream(f)
2201 with previous_file as f:
2202 prev = _parse_stream(f)
2203 show_stats(cur, prev)
2204 sys.exit()
2205
Adrian Roos038a0292018-12-19 17:11:21 +01002206 classes_with_base = []
2207
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002208 with current_file as f:
Adrian Roos6eb57b02018-12-13 22:08:29 +01002209 if base_current_file:
2210 with base_current_file as base_f:
Adrian Roos038a0292018-12-19 17:11:21 +01002211 cur_fail, cur_noticed = examine_stream(f, base_f,
2212 out_classes_with_base=classes_with_base)
Adrian Roos6eb57b02018-12-13 22:08:29 +01002213 else:
Adrian Roos038a0292018-12-19 17:11:21 +01002214 cur_fail, cur_noticed = examine_stream(f, out_classes_with_base=classes_with_base)
2215
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002216 if not previous_file is None:
2217 with previous_file as f:
Adrian Roos6eb57b02018-12-13 22:08:29 +01002218 if base_previous_file:
2219 with base_previous_file as base_f:
Adrian Roos038a0292018-12-19 17:11:21 +01002220 prev_fail, prev_noticed = examine_stream(f, base_f,
2221 in_classes_with_base=classes_with_base)
Adrian Roos6eb57b02018-12-13 22:08:29 +01002222 else:
Adrian Roos038a0292018-12-19 17:11:21 +01002223 prev_fail, prev_noticed = examine_stream(f, in_classes_with_base=classes_with_base)
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002224
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002225 # ignore errors from previous API level
2226 for p in prev_fail:
2227 if p in cur_fail:
2228 del cur_fail[p]
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002229
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002230 # ignore classes unchanged from previous API level
2231 for k, v in prev_noticed.iteritems():
2232 if k in cur_noticed and v == cur_noticed[k]:
2233 del cur_noticed[k]
2234
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002235 """
2236 # NOTE: disabled because of memory pressure
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002237 # look for compatibility issues
2238 compat_fail = verify_compat(cur, prev)
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002239
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002240 print "%s API compatibility issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
2241 for f in sorted(compat_fail):
2242 print compat_fail[f]
2243 print
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002244 """
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002245
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002246 if args['show_noticed'] and len(cur_noticed) != 0:
2247 print "%s API changes noticed %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
2248 for f in sorted(cur_noticed.keys()):
2249 print f
2250 print
2251
Jason Monk53b2a732017-11-10 15:43:17 -05002252 if len(cur_fail) != 0:
2253 print "%s API style issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
2254 for f in sorted(cur_fail):
2255 print cur_fail[f]
2256 print
2257 sys.exit(77)