blob: 75aeb5e2b288633bf7363da0ace582e0788f9090 [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]
Adrian Roos02e18dd2019-02-28 12:41:48 +0100199 self.implements_all = [self.implements]
Adrian Roosb787c182019-01-03 18:54:33 +0100200 else:
201 self.implements = None
Adrian Roos02e18dd2019-02-28 12:41:48 +0100202 self.implements_all = []
Jeff Sharkey037458a2014-09-04 15:46:20 -0700203 else:
Adrian Roosb787c182019-01-03 18:54:33 +0100204 raise ValueError("Unknown signature format: " + sig_format)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700205
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700206 self.fullname = self.pkg.name + "." + self.fullname
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800207 self.fullname_path = self.fullname.split(".")
208
Adrian Roosb787c182019-01-03 18:54:33 +0100209 if self.extends is not None:
210 self.extends_path = self.extends.split(".")
211 else:
212 self.extends_path = []
213
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700214 self.name = self.fullname[self.fullname.rindex(".")+1:]
215
Adrian Roos6eb57b02018-12-13 22:08:29 +0100216 def merge_from(self, other):
217 self.ctors.extend(other.ctors)
218 self.fields.extend(other.fields)
219 self.methods.extend(other.methods)
220
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -0700221 def __hash__(self):
222 return hash((self.raw, tuple(self.ctors), tuple(self.fields), tuple(self.methods)))
223
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700224 def __repr__(self):
225 return self.raw
226
227
228class Package():
Adrian Roosd9871b12019-02-28 12:42:22 +0100229 NAME = re.compile("package(?: .*)? ([A-Za-z0-9.]+)")
Adrian Roosb1faa0b2019-02-26 11:54:40 +0100230
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700231 def __init__(self, line, raw, blame):
232 self.line = line
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700233 self.raw = raw.strip(" {;")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700234 self.blame = blame
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700235
Adrian Roosb1faa0b2019-02-26 11:54:40 +0100236 self.name = Package.NAME.match(raw).group(1)
Jeff Sharkey047d7f02015-02-25 11:27:55 -0800237 self.name_path = self.name.split(".")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700238
239 def __repr__(self):
240 return self.raw
241
Adrian Roose5eeae72019-01-04 20:10:06 +0100242class V2Tokenizer(object):
243 __slots__ = ["raw"]
244
Adrian Rooscf82e042019-01-29 15:01:28 +0100245 SIGNATURE_PREFIX = "// Signature format: "
Adrian Roos5cdfb692019-01-05 22:04:55 +0100246 DELIMITER = re.compile(r'\s+|[()@<>;,={}/"!?]|\[\]|\.\.\.')
Adrian Roosb787c182019-01-03 18:54:33 +0100247 STRING_SPECIAL = re.compile(r'["\\]')
248
249 def __init__(self, raw):
250 self.raw = raw
Adrian Roosb787c182019-01-03 18:54:33 +0100251
Adrian Roose5eeae72019-01-04 20:10:06 +0100252 def tokenize(self):
253 tokens = []
254 current = 0
255 raw = self.raw
256 length = len(raw)
Adrian Roosb787c182019-01-03 18:54:33 +0100257
Adrian Roose5eeae72019-01-04 20:10:06 +0100258 while current < length:
259 while current < length:
260 start = current
261 match = V2Tokenizer.DELIMITER.search(raw, start)
262 if match is not None:
263 match_start = match.start()
264 if match_start == current:
265 end = match.end()
266 else:
267 end = match_start
268 else:
269 end = length
Adrian Roosb787c182019-01-03 18:54:33 +0100270
Adrian Roose5eeae72019-01-04 20:10:06 +0100271 token = raw[start:end]
272 current = end
Adrian Roosb787c182019-01-03 18:54:33 +0100273
Adrian Roose5eeae72019-01-04 20:10:06 +0100274 if token == "" or token[0] == " ":
275 continue
276 else:
277 break
Adrian Roosb787c182019-01-03 18:54:33 +0100278
Adrian Roose5eeae72019-01-04 20:10:06 +0100279 if token == "@":
280 if raw[start:start+11] == "@interface ":
281 current = start + 11
282 tokens.append("@interface")
283 continue
284 elif token == '/':
285 if raw[start:start+2] == "//":
286 current = length
287 continue
288 elif token == '"':
289 current, string_token = self.tokenize_string(raw, length, current)
290 tokens.append(token + string_token)
291 continue
Adrian Roosb787c182019-01-03 18:54:33 +0100292
Adrian Roose5eeae72019-01-04 20:10:06 +0100293 tokens.append(token)
Adrian Roosb787c182019-01-03 18:54:33 +0100294
Adrian Roose5eeae72019-01-04 20:10:06 +0100295 return tokens
Adrian Roosb787c182019-01-03 18:54:33 +0100296
Adrian Roose5eeae72019-01-04 20:10:06 +0100297 def tokenize_string(self, raw, length, current):
298 start = current
299 end = length
Adrian Roosb787c182019-01-03 18:54:33 +0100300 while start < end:
Adrian Roose5eeae72019-01-04 20:10:06 +0100301 match = V2Tokenizer.STRING_SPECIAL.search(raw, start)
Adrian Roosb787c182019-01-03 18:54:33 +0100302 if match:
303 if match.group() == '"':
304 end = match.end()
305 break
306 elif match.group() == '\\':
307 # ignore whatever is after the slash
308 start += 2
309 else:
310 raise ValueError("Unexpected match: `%s`" % (match.group()))
311 else:
Adrian Roose5eeae72019-01-04 20:10:06 +0100312 raise ValueError("Unexpected EOF tokenizing string: `%s`" % (raw[current - 1:],))
Adrian Roosb787c182019-01-03 18:54:33 +0100313
Adrian Roose5eeae72019-01-04 20:10:06 +0100314 token = raw[current:end]
315 return end, token
Adrian Roosb787c182019-01-03 18:54:33 +0100316
Adrian Roose5eeae72019-01-04 20:10:06 +0100317class V2LineParser(object):
318 __slots__ = ["tokenized", "current", "len"]
319
Adrian Roos258c5722019-01-21 15:43:15 +0100320 FIELD_KINDS = ("field", "property", "enum_constant")
Adrian Roosd1e38922019-01-14 15:44:15 +0100321 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 +0100322 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())
323
324 def __init__(self, raw):
Adrian Roose5eeae72019-01-04 20:10:06 +0100325 self.tokenized = V2Tokenizer(raw).tokenize()
Adrian Roosb787c182019-01-03 18:54:33 +0100326 self.current = 0
Adrian Roose5eeae72019-01-04 20:10:06 +0100327 self.len = len(self.tokenized)
Adrian Roosb787c182019-01-03 18:54:33 +0100328
329 def parse_into_method(self, method):
330 method.split = []
331 kind = self.parse_one_of("ctor", "method")
332 method.split.append(kind)
Adrian Roos80545ef2019-02-27 16:45:00 +0100333 method.annotations = self.parse_annotations()
Adrian Roosb787c182019-01-03 18:54:33 +0100334 method.split.extend(self.parse_modifiers())
335 self.parse_matching_paren("<", ">")
Adrian Roos80545ef2019-02-27 16:45:00 +0100336 if "@Deprecated" in method.annotations:
Adrian Roosb787c182019-01-03 18:54:33 +0100337 method.split.append("deprecated")
338 if kind == "ctor":
339 method.typ = "ctor"
340 else:
341 method.typ = self.parse_type()
342 method.split.append(method.typ)
343 method.name = self.parse_name()
344 method.split.append(method.name)
345 self.parse_token("(")
Adrian Roos80545ef2019-02-27 16:45:00 +0100346 method.detailed_args = self.parse_args()
Adrian Roosb787c182019-01-03 18:54:33 +0100347 self.parse_token(")")
348 method.throws = self.parse_throws()
349 if "@interface" in method.clazz.split:
350 self.parse_annotation_default()
351 self.parse_token(";")
352 self.parse_eof()
353
354 def parse_into_class(self, clazz):
355 clazz.split = []
356 annotations = self.parse_annotations()
357 if "@Deprecated" in annotations:
358 clazz.split.append("deprecated")
359 clazz.split.extend(self.parse_modifiers())
360 kind = self.parse_one_of("class", "interface", "@interface", "enum")
361 if kind == "enum":
362 # enums are implicitly final
363 clazz.split.append("final")
364 clazz.split.append(kind)
365 clazz.fullname = self.parse_name()
366 self.parse_matching_paren("<", ">")
367 extends = self.parse_extends()
368 clazz.extends = extends[0] if extends else None
Adrian Roos02e18dd2019-02-28 12:41:48 +0100369 clazz.implements_all = self.parse_implements()
Adrian Roosb787c182019-01-03 18:54:33 +0100370 # The checks assume that interfaces are always found in implements, which isn't true for
371 # subinterfaces.
Adrian Roos02e18dd2019-02-28 12:41:48 +0100372 if not clazz.implements_all and "interface" in clazz.split:
373 clazz.implements_all = [clazz.extends]
374 clazz.implements = clazz.implements_all[0] if clazz.implements_all else None
Adrian Roosb787c182019-01-03 18:54:33 +0100375 self.parse_token("{")
376 self.parse_eof()
377
378 def parse_into_field(self, field):
Adrian Roos258c5722019-01-21 15:43:15 +0100379 kind = self.parse_one_of(*V2LineParser.FIELD_KINDS)
Adrian Roosb787c182019-01-03 18:54:33 +0100380 field.split = [kind]
Adrian Roos80545ef2019-02-27 16:45:00 +0100381 field.annotations = self.parse_annotations()
382 if "@Deprecated" in field.annotations:
Adrian Roosb787c182019-01-03 18:54:33 +0100383 field.split.append("deprecated")
384 field.split.extend(self.parse_modifiers())
385 field.typ = self.parse_type()
386 field.split.append(field.typ)
387 field.name = self.parse_name()
388 field.split.append(field.name)
389 if self.parse_if("="):
390 field.value = self.parse_value_stripped()
391 else:
392 field.value = None
393
394 self.parse_token(";")
395 self.parse_eof()
396
397 def lookahead(self):
398 return self.tokenized[self.current]
399
400 def parse_one_of(self, *options):
401 found = self.lookahead()
402 if found not in options:
403 raise ValueError("Parsing failed, expected one of `%s` but found `%s` in %s" % (options, found, repr(self.tokenized)))
404 return self.parse_token()
405
406 def parse_token(self, tok = None):
407 found = self.lookahead()
408 if tok is not None and found != tok:
409 raise ValueError("Parsing failed, expected `%s` but found `%s` in %s" % (tok, found, repr(self.tokenized)))
410 self.current += 1
411 return found
412
413 def eof(self):
Adrian Roose5eeae72019-01-04 20:10:06 +0100414 return self.current == self.len
Adrian Roosb787c182019-01-03 18:54:33 +0100415
416 def parse_eof(self):
417 if not self.eof():
418 raise ValueError("Parsing failed, expected EOF, but %s has not been parsed in %s" % (self.tokenized[self.current:], self.tokenized))
419
420 def parse_if(self, tok):
421 if not self.eof() and self.lookahead() == tok:
422 self.parse_token()
423 return True
424 return False
425
426 def parse_annotations(self):
427 ret = []
428 while self.lookahead() == "@":
429 ret.append(self.parse_annotation())
430 return ret
431
432 def parse_annotation(self):
433 ret = self.parse_token("@") + self.parse_token()
434 self.parse_matching_paren("(", ")")
435 return ret
436
437 def parse_matching_paren(self, open, close):
438 start = self.current
439 if not self.parse_if(open):
440 return
441 length = len(self.tokenized)
442 count = 1
443 while count > 0:
444 if self.current == length:
445 raise ValueError("Unexpected EOF looking for closing paren: `%s`" % (self.tokenized[start:],))
446 t = self.parse_token()
447 if t == open:
448 count += 1
449 elif t == close:
450 count -= 1
451 return self.tokenized[start:self.current]
452
453 def parse_modifiers(self):
454 ret = []
455 while self.lookahead() in V2LineParser.MODIFIERS:
456 ret.append(self.parse_token())
457 return ret
458
Adrian Roos5cdfb692019-01-05 22:04:55 +0100459 def parse_kotlin_nullability(self):
460 t = self.lookahead()
461 if t == "?" or t == "!":
462 return self.parse_token()
463 return None
464
Adrian Roosb787c182019-01-03 18:54:33 +0100465 def parse_type(self):
Adrian Roosd1e38922019-01-14 15:44:15 +0100466 self.parse_annotations()
Adrian Roosb787c182019-01-03 18:54:33 +0100467 type = self.parse_token()
Adrian Roosd1e38922019-01-14 15:44:15 +0100468 if type[-1] == '.':
469 self.parse_annotations()
470 type += self.parse_token()
Adrian Roosb787c182019-01-03 18:54:33 +0100471 if type in V2LineParser.JAVA_LANG_TYPES:
472 type = "java.lang." + type
473 self.parse_matching_paren("<", ">")
Adrian Roos5cdfb692019-01-05 22:04:55 +0100474 while True:
475 t = self.lookahead()
Adrian Roosd1e38922019-01-14 15:44:15 +0100476 if t == "@":
477 self.parse_annotation()
478 elif t == "[]":
Adrian Roos5cdfb692019-01-05 22:04:55 +0100479 type += self.parse_token()
480 elif self.parse_kotlin_nullability() is not None:
481 pass # discard nullability for now
482 else:
483 break
Adrian Roosb787c182019-01-03 18:54:33 +0100484 return type
485
486 def parse_arg_type(self):
487 type = self.parse_type()
488 if self.parse_if("..."):
489 type += "..."
Adrian Roos5cdfb692019-01-05 22:04:55 +0100490 self.parse_kotlin_nullability() # discard nullability for now
Adrian Roosb787c182019-01-03 18:54:33 +0100491 return type
492
493 def parse_name(self):
494 return self.parse_token()
495
496 def parse_args(self):
497 args = []
498 if self.lookahead() == ")":
499 return args
500
501 while True:
502 args.append(self.parse_arg())
503 if self.lookahead() == ")":
504 return args
505 self.parse_token(",")
506
507 def parse_arg(self):
Adrian Roosd1e38922019-01-14 15:44:15 +0100508 self.parse_if("vararg") # kotlin vararg
Adrian Roos80545ef2019-02-27 16:45:00 +0100509 annotations = self.parse_annotations()
510 arg = Argument(self.parse_arg_type())
511 arg.annotations = annotations
Adrian Roos5cdfb692019-01-05 22:04:55 +0100512 l = self.lookahead()
513 if l != "," and l != ")":
Adrian Roosd1e38922019-01-14 15:44:15 +0100514 if self.lookahead() != '=':
Adrian Roos80545ef2019-02-27 16:45:00 +0100515 arg.name = self.parse_token() # kotlin argument name
Adrian Roos5cdfb692019-01-05 22:04:55 +0100516 if self.parse_if('='): # kotlin default value
Adrian Roos80545ef2019-02-27 16:45:00 +0100517 arg.default = self.parse_expression()
518 return arg
Adrian Roosb787c182019-01-03 18:54:33 +0100519
Adrian Roosd1e38922019-01-14 15:44:15 +0100520 def parse_expression(self):
521 while not self.lookahead() in [')', ',', ';']:
522 (self.parse_matching_paren('(', ')') or
523 self.parse_matching_paren('{', '}') or
524 self.parse_token())
525
Adrian Roosb787c182019-01-03 18:54:33 +0100526 def parse_throws(self):
527 ret = []
528 if self.parse_if("throws"):
529 ret.append(self.parse_type())
530 while self.parse_if(","):
531 ret.append(self.parse_type())
532 return ret
533
534 def parse_extends(self):
535 if self.parse_if("extends"):
536 return self.parse_space_delimited_type_list()
537 return []
538
539 def parse_implements(self):
540 if self.parse_if("implements"):
541 return self.parse_space_delimited_type_list()
542 return []
543
544 def parse_space_delimited_type_list(self, terminals = ["implements", "{"]):
545 types = []
546 while True:
547 types.append(self.parse_type())
548 if self.lookahead() in terminals:
549 return types
550
551 def parse_annotation_default(self):
552 if self.parse_if("default"):
Adrian Roosd1e38922019-01-14 15:44:15 +0100553 self.parse_expression()
Adrian Roosb787c182019-01-03 18:54:33 +0100554
555 def parse_value(self):
556 if self.lookahead() == "{":
557 return " ".join(self.parse_matching_paren("{", "}"))
558 elif self.lookahead() == "(":
559 return " ".join(self.parse_matching_paren("(", ")"))
560 else:
561 return self.parse_token()
562
563 def parse_value_stripped(self):
564 value = self.parse_value()
565 if value[0] == '"':
566 return value[1:-1]
567 return value
568
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700569
Adrian Roos038a0292018-12-19 17:11:21 +0100570def _parse_stream(f, clazz_cb=None, base_f=None, out_classes_with_base=None,
571 in_classes_with_base=[]):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700572 api = {}
Adrian Roos038a0292018-12-19 17:11:21 +0100573 in_classes_with_base = _retry_iterator(in_classes_with_base)
Adrian Roos6eb57b02018-12-13 22:08:29 +0100574
575 if base_f:
Adrian Roos038a0292018-12-19 17:11:21 +0100576 base_classes = _retry_iterator(_parse_stream_to_generator(base_f))
Adrian Roos6eb57b02018-12-13 22:08:29 +0100577 else:
578 base_classes = []
579
Adrian Roos038a0292018-12-19 17:11:21 +0100580 def handle_class(clazz):
Adrian Roos6eb57b02018-12-13 22:08:29 +0100581 if clazz_cb:
582 clazz_cb(clazz)
583 else: # In callback mode, don't keep track of the full API
584 api[clazz.fullname] = clazz
585
Adrian Roos038a0292018-12-19 17:11:21 +0100586 def handle_missed_classes_with_base(clazz):
587 for c in _yield_until_matching_class(in_classes_with_base, clazz):
588 base_class = _skip_to_matching_class(base_classes, c)
589 if base_class:
590 handle_class(base_class)
591
592 for clazz in _parse_stream_to_generator(f):
593 # Before looking at clazz, let's see if there's some classes that were not present, but
594 # may have an entry in the base stream.
595 handle_missed_classes_with_base(clazz)
596
597 base_class = _skip_to_matching_class(base_classes, clazz)
598 if base_class:
599 clazz.merge_from(base_class)
600 if out_classes_with_base is not None:
601 out_classes_with_base.append(clazz)
602 handle_class(clazz)
603
604 handle_missed_classes_with_base(None)
605
Adrian Roos6eb57b02018-12-13 22:08:29 +0100606 return api
607
608def _parse_stream_to_generator(f):
609 line = 0
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700610 pkg = None
611 clazz = None
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700612 blame = None
Adrian Roosb787c182019-01-03 18:54:33 +0100613 sig_format = 1
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700614
Adrian Roos80545ef2019-02-27 16:45:00 +0100615 re_blame = re.compile(r"^(\^?[a-z0-9]{7,}) \(<([^>]+)>.+?\) (.+?)$")
Adrian Roos258c5722019-01-21 15:43:15 +0100616
617 field_prefixes = map(lambda kind: " %s" % (kind,), V2LineParser.FIELD_KINDS)
618 def startsWithFieldPrefix(raw):
619 for prefix in field_prefixes:
620 if raw.startswith(prefix):
621 return True
622 return False
623
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800624 for raw in f:
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700625 line += 1
626 raw = raw.rstrip()
627 match = re_blame.match(raw)
628 if match is not None:
629 blame = match.groups()[0:2]
Adrian Roos80545ef2019-02-27 16:45:00 +0100630 if blame[0].startswith("^"): # Outside of blame range
631 blame = None
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700632 raw = match.groups()[2]
633 else:
634 blame = None
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700635
Adrian Roos80545ef2019-02-27 16:45:00 +0100636 if line == 1 and V2Tokenizer.SIGNATURE_PREFIX in raw:
Adrian Rooscf82e042019-01-29 15:01:28 +0100637 sig_format_string = raw[len(V2Tokenizer.SIGNATURE_PREFIX):]
638 if sig_format_string in ["2.0", "3.0"]:
639 sig_format = 2
640 else:
641 raise ValueError("Unknown format: %s" % (sig_format_string,))
Adrian Roosb787c182019-01-03 18:54:33 +0100642 elif raw.startswith("package"):
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700643 pkg = Package(line, raw, blame)
644 elif raw.startswith(" ") and raw.endswith("{"):
Adrian Roosb787c182019-01-03 18:54:33 +0100645 clazz = Class(pkg, line, raw, blame, sig_format=sig_format)
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700646 elif raw.startswith(" ctor"):
Adrian Roosb787c182019-01-03 18:54:33 +0100647 clazz.ctors.append(Method(clazz, line, raw, blame, sig_format=sig_format))
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700648 elif raw.startswith(" method"):
Adrian Roosb787c182019-01-03 18:54:33 +0100649 clazz.methods.append(Method(clazz, line, raw, blame, sig_format=sig_format))
Adrian Roos258c5722019-01-21 15:43:15 +0100650 elif startsWithFieldPrefix(raw):
Adrian Roosb787c182019-01-03 18:54:33 +0100651 clazz.fields.append(Field(clazz, line, raw, blame, sig_format=sig_format))
Adrian Roos6eb57b02018-12-13 22:08:29 +0100652 elif raw.startswith(" }") and clazz:
Adrian Roos038a0292018-12-19 17:11:21 +0100653 yield clazz
Jeff Sharkeya18a2e32015-02-22 15:54:32 -0800654
Adrian Roos5ed42b62018-12-19 17:10:22 +0100655def _retry_iterator(it):
656 """Wraps an iterator, such that calling send(True) on it will redeliver the same element"""
657 for e in it:
658 while True:
659 retry = yield e
660 if not retry:
661 break
662 # send() was called, asking us to redeliver clazz on next(). Still need to yield
663 # a dummy value to the send() first though.
664 if (yield "Returning clazz on next()"):
665 raise TypeError("send() must be followed by next(), not send()")
666
Adrian Roos038a0292018-12-19 17:11:21 +0100667def _skip_to_matching_class(classes, needle):
668 """Takes a classes iterator and consumes entries until it returns the class we're looking for
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700669
Adrian Roos6eb57b02018-12-13 22:08:29 +0100670 This relies on classes being sorted by package and class name."""
671
672 for clazz in classes:
673 if clazz.pkg.name < needle.pkg.name:
674 # We haven't reached the right package yet
675 continue
Adrian Roos5ed42b62018-12-19 17:10:22 +0100676 if clazz.pkg.name == needle.pkg.name and clazz.fullname < needle.fullname:
677 # We're in the right package, but not the right class yet
Adrian Roos6eb57b02018-12-13 22:08:29 +0100678 continue
679 if clazz.fullname == needle.fullname:
680 return clazz
681 # We ran past the right class. Send it back into the generator, then report failure.
682 classes.send(clazz)
683 return None
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700684
Adrian Roos038a0292018-12-19 17:11:21 +0100685def _yield_until_matching_class(classes, needle):
686 """Takes a class iterator and yields entries it until it reaches the class we're looking for.
687
688 This relies on classes being sorted by package and class name."""
689
690 for clazz in classes:
691 if needle is None:
692 yield clazz
693 elif clazz.pkg.name < needle.pkg.name:
694 # We haven't reached the right package yet
695 yield clazz
696 elif clazz.pkg.name == needle.pkg.name and clazz.fullname < needle.fullname:
697 # We're in the right package, but not the right class yet
698 yield clazz
699 elif clazz.fullname == needle.fullname:
700 # Class found, abort.
701 return
702 else:
703 # We ran past the right class. Send it back into the iterator, then abort.
704 classes.send(clazz)
705 return
706
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700707class Failure():
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800708 def __init__(self, sig, clazz, detail, error, rule, msg):
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700709 self.sig = sig
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700710 self.error = error
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800711 self.rule = rule
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700712 self.msg = msg
713
714 if error:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800715 self.head = "Error %s" % (rule) if rule else "Error"
716 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 -0700717 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800718 self.head = "Warning %s" % (rule) if rule else "Warning"
719 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 -0700720
721 self.line = clazz.line
722 blame = clazz.blame
723 if detail is not None:
724 dump += "\n in " + repr(detail)
725 self.line = detail.line
726 blame = detail.blame
727 dump += "\n in " + repr(clazz)
728 dump += "\n in " + repr(clazz.pkg)
729 dump += "\n at line " + repr(self.line)
730 if blame is not None:
731 dump += "\n last modified by %s in %s" % (blame[1], blame[0])
732
733 self.dump = dump
734
735 def __repr__(self):
736 return self.dump
737
738
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700739failures = {}
Jeff Sharkey294f0de2014-08-29 17:41:43 -0700740
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800741def _fail(clazz, detail, error, rule, msg):
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700742 """Records an API failure to be processed later."""
743 global failures
744
Adrian Roosb787c182019-01-03 18:54:33 +0100745 sig = "%s-%s-%s" % (clazz.fullname, detail.ident if detail else None, msg)
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700746 sig = sig.replace(" deprecated ", " ")
747
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800748 failures[sig] = Failure(sig, clazz, detail, error, rule, msg)
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -0700749
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700750
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800751def warn(clazz, detail, rule, msg):
752 _fail(clazz, detail, False, rule, msg)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700753
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800754def error(clazz, detail, rule, msg):
755 _fail(clazz, detail, True, rule, msg)
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700756
757
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -0700758noticed = {}
759
760def notice(clazz):
761 global noticed
762
763 noticed[clazz.fullname] = hash(clazz)
764
765
Adrian Roos93bafc42019-03-05 18:31:37 +0100766verifiers = {}
767
768def verifier(f):
769 verifiers[f.__name__] = f
770 return f
771
772
773@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700774def verify_constants(clazz):
775 """All static final constants must be FOO_NAME style."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700776 if re.match("android\.R\.[a-z]+", clazz.fullname): return
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600777 if clazz.fullname.startswith("android.os.Build"): return
778 if clazz.fullname == "android.system.OsConstants": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700779
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600780 req = ["java.lang.String","byte","short","int","long","float","double","boolean","char"]
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700781 for f in clazz.fields:
782 if "static" in f.split and "final" in f.split:
783 if re.match("[A-Z0-9_]+", f.name) is None:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800784 error(clazz, f, "C2", "Constant field names must be FOO_NAME")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600785 if f.typ != "java.lang.String":
Jeff Sharkey331279b2016-02-29 16:02:02 -0700786 if f.name.startswith("MIN_") or f.name.startswith("MAX_"):
787 warn(clazz, f, "C8", "If min/max could change in future, make them dynamic methods")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600788 if f.typ in req and f.value is None:
789 error(clazz, f, None, "All constants must be defined at compile time")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700790
Adrian Roos93bafc42019-03-05 18:31:37 +0100791@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700792def verify_enums(clazz):
793 """Enums are bad, mmkay?"""
Adrian Roosb787c182019-01-03 18:54:33 +0100794 if clazz.extends == "java.lang.Enum" or "enum" in clazz.split:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800795 error(clazz, None, "F5", "Enums are not allowed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700796
Adrian Roos93bafc42019-03-05 18:31:37 +0100797@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700798def verify_class_names(clazz):
799 """Try catching malformed class names like myMtp or MTPUser."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700800 if clazz.fullname.startswith("android.opengl"): return
801 if clazz.fullname.startswith("android.renderscript"): return
802 if re.match("android\.R\.[a-z]+", clazz.fullname): return
803
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700804 if re.search("[A-Z]{2,}", clazz.name) is not None:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800805 warn(clazz, None, "S1", "Class names with acronyms should be Mtp not MTP")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700806 if re.match("[^A-Z]", clazz.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800807 error(clazz, None, "S1", "Class must start with uppercase char")
Jeff Sharkeyee21f692018-02-16 13:29:29 -0700808 if clazz.name.endswith("Impl"):
809 error(clazz, None, None, "Don't expose your implementation details")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700810
811
Adrian Roos93bafc42019-03-05 18:31:37 +0100812@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700813def verify_method_names(clazz):
814 """Try catching malformed method names, like Foo() or getMTU()."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700815 if clazz.fullname.startswith("android.opengl"): return
816 if clazz.fullname.startswith("android.renderscript"): return
817 if clazz.fullname == "android.system.OsConstants": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700818
819 for m in clazz.methods:
820 if re.search("[A-Z]{2,}", m.name) is not None:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800821 warn(clazz, m, "S1", "Method names with acronyms should be getMtu() instead of getMTU()")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700822 if re.match("[^a-z]", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800823 error(clazz, m, "S1", "Method name must start with lowercase char")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700824
825
Adrian Roos93bafc42019-03-05 18:31:37 +0100826@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700827def verify_callbacks(clazz):
828 """Verify Callback classes.
829 All callback classes must be abstract.
830 All methods must follow onFoo() naming style."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700831 if clazz.fullname == "android.speech.tts.SynthesisCallback": return
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700832
833 if clazz.name.endswith("Callbacks"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800834 error(clazz, None, "L1", "Callback class names should be singular")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700835 if clazz.name.endswith("Observer"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800836 warn(clazz, None, "L1", "Class should be named FooCallback")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700837
838 if clazz.name.endswith("Callback"):
839 if "interface" in clazz.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800840 error(clazz, None, "CL3", "Callbacks must be abstract class to enable extension in future API levels")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700841
842 for m in clazz.methods:
843 if not re.match("on[A-Z][a-z]*", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800844 error(clazz, m, "L1", "Callback method names must be onFoo() style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700845
846
Adrian Roos93bafc42019-03-05 18:31:37 +0100847@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700848def verify_listeners(clazz):
849 """Verify Listener classes.
850 All Listener classes must be interface.
851 All methods must follow onFoo() naming style.
852 If only a single method, it must match class name:
853 interface OnFooListener { void onFoo() }"""
854
855 if clazz.name.endswith("Listener"):
Adrian Roosb787c182019-01-03 18:54:33 +0100856 if "abstract" in clazz.split and "class" in clazz.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800857 error(clazz, None, "L1", "Listeners should be an interface, or otherwise renamed Callback")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700858
859 for m in clazz.methods:
860 if not re.match("on[A-Z][a-z]*", m.name):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800861 error(clazz, m, "L1", "Listener method names must be onFoo() style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700862
863 if len(clazz.methods) == 1 and clazz.name.startswith("On"):
864 m = clazz.methods[0]
865 if (m.name + "Listener").lower() != clazz.name.lower():
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800866 error(clazz, m, "L1", "Single listener method name must match class name")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700867
868
Adrian Roos93bafc42019-03-05 18:31:37 +0100869@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700870def verify_actions(clazz):
871 """Verify intent actions.
872 All action names must be named ACTION_FOO.
873 All action values must be scoped by package and match name:
874 package android.foo {
875 String ACTION_BAR = "android.foo.action.BAR";
876 }"""
877 for f in clazz.fields:
878 if f.value is None: continue
879 if f.name.startswith("EXTRA_"): continue
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700880 if f.name == "SERVICE_INTERFACE" or f.name == "PROVIDER_INTERFACE": continue
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600881 if "INTERACTION" in f.name: continue
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700882
883 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
884 if "_ACTION" in f.name or "ACTION_" in f.name or ".action." in f.value.lower():
885 if not f.name.startswith("ACTION_"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800886 error(clazz, f, "C3", "Intent action constant name must be ACTION_FOO")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700887 else:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700888 if clazz.fullname == "android.content.Intent":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700889 prefix = "android.intent.action"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700890 elif clazz.fullname == "android.provider.Settings":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700891 prefix = "android.settings"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700892 elif clazz.fullname == "android.app.admin.DevicePolicyManager" or clazz.fullname == "android.app.admin.DeviceAdminReceiver":
893 prefix = "android.app.action"
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700894 else:
895 prefix = clazz.pkg.name + ".action"
896 expected = prefix + "." + f.name[7:]
897 if f.value != expected:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700898 error(clazz, f, "C4", "Inconsistent action value; expected '%s'" % (expected))
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700899
900
Adrian Roos93bafc42019-03-05 18:31:37 +0100901@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700902def verify_extras(clazz):
903 """Verify intent extras.
904 All extra names must be named EXTRA_FOO.
905 All extra values must be scoped by package and match name:
906 package android.foo {
907 String EXTRA_BAR = "android.foo.extra.BAR";
908 }"""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700909 if clazz.fullname == "android.app.Notification": return
910 if clazz.fullname == "android.appwidget.AppWidgetManager": return
911
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700912 for f in clazz.fields:
913 if f.value is None: continue
914 if f.name.startswith("ACTION_"): continue
915
916 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String":
917 if "_EXTRA" in f.name or "EXTRA_" in f.name or ".extra" in f.value.lower():
918 if not f.name.startswith("EXTRA_"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800919 error(clazz, f, "C3", "Intent extra must be EXTRA_FOO")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700920 else:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700921 if clazz.pkg.name == "android.content" and clazz.name == "Intent":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700922 prefix = "android.intent.extra"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700923 elif clazz.pkg.name == "android.app.admin":
924 prefix = "android.app.extra"
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700925 else:
926 prefix = clazz.pkg.name + ".extra"
927 expected = prefix + "." + f.name[6:]
928 if f.value != expected:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700929 error(clazz, f, "C4", "Inconsistent extra value; expected '%s'" % (expected))
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700930
931
Adrian Roos93bafc42019-03-05 18:31:37 +0100932@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700933def verify_equals(clazz):
934 """Verify that equals() and hashCode() must be overridden together."""
Jeff Sharkey40d623e62016-12-21 13:46:33 -0700935 eq = False
936 hc = False
937 for m in clazz.methods:
Adrian Roosb787c182019-01-03 18:54:33 +0100938 if "static" in m.split: continue
939 if m.sig_matches("boolean", "equals", ["java.lang.Object"]): eq = True
940 if m.sig_matches("int", "hashCode", []): hc = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700941 if eq != hc:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800942 error(clazz, None, "M8", "Must override both equals and hashCode; missing one")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700943
944
Adrian Roos93bafc42019-03-05 18:31:37 +0100945@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700946def verify_parcelable(clazz):
947 """Verify that Parcelable objects aren't hiding required bits."""
Adrian Roosb787c182019-01-03 18:54:33 +0100948 if clazz.implements == "android.os.Parcelable":
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700949 creator = [ i for i in clazz.fields if i.name == "CREATOR" ]
950 write = [ i for i in clazz.methods if i.name == "writeToParcel" ]
951 describe = [ i for i in clazz.methods if i.name == "describeContents" ]
952
953 if len(creator) == 0 or len(write) == 0 or len(describe) == 0:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -0800954 error(clazz, None, "FW3", "Parcelable requires CREATOR, writeToParcel, and describeContents; missing one")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700955
Adrian Roosb787c182019-01-03 18:54:33 +0100956 if "final" not in clazz.split:
Jeff Sharkey331279b2016-02-29 16:02:02 -0700957 error(clazz, None, "FW8", "Parcelable classes must be final")
958
Jeff Sharkey336dd0b2018-01-12 12:12:27 -0700959 for c in clazz.ctors:
960 if c.args == ["android.os.Parcel"]:
961 error(clazz, c, "FW3", "Parcelable inflation is exposed through CREATOR, not raw constructors")
962
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700963
Adrian Roos93bafc42019-03-05 18:31:37 +0100964@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700965def verify_protected(clazz):
Jeff Sharkeyb46a9692015-02-17 17:19:41 -0800966 """Verify that no protected methods or fields are allowed."""
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700967 for m in clazz.methods:
Jeff Sharkeyeff9e222018-09-27 16:29:25 -0600968 if m.name == "finalize": continue
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700969 if "protected" in m.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800970 error(clazz, m, "M7", "Protected methods not allowed; must be public")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700971 for f in clazz.fields:
972 if "protected" in f.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -0800973 error(clazz, f, "M7", "Protected fields not allowed; must be public")
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700974
975
Adrian Roos93bafc42019-03-05 18:31:37 +0100976@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700977def verify_fields(clazz):
978 """Verify that all exposed fields are final.
979 Exposed fields must follow myName style.
980 Catch internal mFoo objects being exposed."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700981
982 IGNORE_BARE_FIELDS = [
983 "android.app.ActivityManager.RecentTaskInfo",
984 "android.app.Notification",
985 "android.content.pm.ActivityInfo",
986 "android.content.pm.ApplicationInfo",
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600987 "android.content.pm.ComponentInfo",
988 "android.content.pm.ResolveInfo",
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700989 "android.content.pm.FeatureGroupInfo",
990 "android.content.pm.InstrumentationInfo",
991 "android.content.pm.PackageInfo",
992 "android.content.pm.PackageItemInfo",
Jeff Sharkey24bda1d2017-04-19 16:04:44 -0600993 "android.content.res.Configuration",
994 "android.graphics.BitmapFactory.Options",
Jeff Sharkey1498f9c2014-09-04 12:45:33 -0700995 "android.os.Message",
996 "android.system.StructPollfd",
997 ]
998
Jeff Sharkey8190f4882014-08-28 12:24:07 -0700999 for f in clazz.fields:
1000 if not "final" in f.split:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001001 if clazz.fullname in IGNORE_BARE_FIELDS:
1002 pass
1003 elif clazz.fullname.endswith("LayoutParams"):
1004 pass
1005 elif clazz.fullname.startswith("android.util.Mutable"):
1006 pass
1007 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001008 error(clazz, f, "F2", "Bare fields must be marked final, or add accessors if mutable")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001009
Adrian Roosd1e38922019-01-14 15:44:15 +01001010 if "static" not in f.split and "property" not in f.split:
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001011 if not re.match("[a-z]([a-zA-Z]+)?", f.name):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001012 error(clazz, f, "S1", "Non-static fields must be named using myField style")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001013
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001014 if re.match("[ms][A-Z]", f.name):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001015 error(clazz, f, "F1", "Internal objects must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001016
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001017 if re.match("[A-Z_]+", f.name):
1018 if "static" not in f.split or "final" not in f.split:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001019 error(clazz, f, "C2", "Constants must be marked static final")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001020
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001021
Adrian Roos93bafc42019-03-05 18:31:37 +01001022@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001023def verify_register(clazz):
1024 """Verify parity of registration methods.
1025 Callback objects use register/unregister methods.
1026 Listener objects use add/remove methods."""
1027 methods = [ m.name for m in clazz.methods ]
1028 for m in clazz.methods:
1029 if "Callback" in m.raw:
1030 if m.name.startswith("register"):
1031 other = "unregister" + m.name[8:]
1032 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001033 error(clazz, m, "L2", "Missing unregister method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001034 if m.name.startswith("unregister"):
1035 other = "register" + m.name[10:]
1036 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001037 error(clazz, m, "L2", "Missing register method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001038
1039 if m.name.startswith("add") or m.name.startswith("remove"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001040 error(clazz, m, "L3", "Callback methods should be named register/unregister")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001041
1042 if "Listener" in m.raw:
1043 if m.name.startswith("add"):
1044 other = "remove" + m.name[3:]
1045 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001046 error(clazz, m, "L2", "Missing remove method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001047 if m.name.startswith("remove") and not m.name.startswith("removeAll"):
1048 other = "add" + m.name[6:]
1049 if other not in methods:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001050 error(clazz, m, "L2", "Missing add method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001051
1052 if m.name.startswith("register") or m.name.startswith("unregister"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001053 error(clazz, m, "L3", "Listener methods should be named add/remove")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001054
1055
Adrian Roos93bafc42019-03-05 18:31:37 +01001056@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001057def verify_sync(clazz):
1058 """Verify synchronized methods aren't exposed."""
1059 for m in clazz.methods:
1060 if "synchronized" in m.split:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001061 error(clazz, m, "M5", "Internal locks must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001062
1063
Adrian Roos93bafc42019-03-05 18:31:37 +01001064@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001065def verify_intent_builder(clazz):
1066 """Verify that Intent builders are createFooIntent() style."""
1067 if clazz.name == "Intent": return
1068
1069 for m in clazz.methods:
1070 if m.typ == "android.content.Intent":
1071 if m.name.startswith("create") and m.name.endswith("Intent"):
1072 pass
1073 else:
Adam Powell539ea122015-04-10 13:01:37 -07001074 warn(clazz, m, "FW1", "Methods creating an Intent should be named createFooIntent()")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001075
1076
Adrian Roos93bafc42019-03-05 18:31:37 +01001077@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001078def verify_helper_classes(clazz):
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001079 """Verify that helper classes are named consistently with what they extend.
1080 All developer extendable methods should be named onFoo()."""
1081 test_methods = False
Adrian Roosb787c182019-01-03 18:54:33 +01001082 if clazz.extends == "android.app.Service":
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001083 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001084 if not clazz.name.endswith("Service"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001085 error(clazz, None, "CL4", "Inconsistent class name; should be FooService")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001086
1087 found = False
1088 for f in clazz.fields:
1089 if f.name == "SERVICE_INTERFACE":
1090 found = True
1091 if f.value != clazz.fullname:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001092 error(clazz, f, "C4", "Inconsistent interface constant; expected '%s'" % (clazz.fullname))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001093
Adrian Roosb787c182019-01-03 18:54:33 +01001094 if clazz.extends == "android.content.ContentProvider":
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001095 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001096 if not clazz.name.endswith("Provider"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001097 error(clazz, None, "CL4", "Inconsistent class name; should be FooProvider")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001098
1099 found = False
1100 for f in clazz.fields:
1101 if f.name == "PROVIDER_INTERFACE":
1102 found = True
1103 if f.value != clazz.fullname:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001104 error(clazz, f, "C4", "Inconsistent interface constant; expected '%s'" % (clazz.fullname))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001105
Adrian Roosb787c182019-01-03 18:54:33 +01001106 if clazz.extends == "android.content.BroadcastReceiver":
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001107 test_methods = True
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001108 if not clazz.name.endswith("Receiver"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001109 error(clazz, None, "CL4", "Inconsistent class name; should be FooReceiver")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001110
Adrian Roosb787c182019-01-03 18:54:33 +01001111 if clazz.extends == "android.app.Activity":
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001112 test_methods = True
1113 if not clazz.name.endswith("Activity"):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001114 error(clazz, None, "CL4", "Inconsistent class name; should be FooActivity")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001115
1116 if test_methods:
1117 for m in clazz.methods:
1118 if "final" in m.split: continue
1119 if not re.match("on[A-Z]", m.name):
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001120 if "abstract" in m.split:
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001121 warn(clazz, m, None, "Methods implemented by developers should be named onFoo()")
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001122 else:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001123 warn(clazz, m, None, "If implemented by developer, should be named onFoo(); otherwise consider marking final")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001124
1125
Adrian Roos93bafc42019-03-05 18:31:37 +01001126@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001127def verify_builder(clazz):
1128 """Verify builder classes.
1129 Methods should return the builder to enable chaining."""
Adrian Roosb787c182019-01-03 18:54:33 +01001130 if clazz.extends: return
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001131 if not clazz.name.endswith("Builder"): return
1132
1133 if clazz.name != "Builder":
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001134 warn(clazz, None, None, "Builder should be defined as inner class")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001135
1136 has_build = False
1137 for m in clazz.methods:
1138 if m.name == "build":
1139 has_build = True
1140 continue
1141
1142 if m.name.startswith("get"): continue
1143 if m.name.startswith("clear"): continue
1144
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001145 if m.name.startswith("with"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001146 warn(clazz, m, None, "Builder methods names should use setFoo() style")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001147
1148 if m.name.startswith("set"):
1149 if not m.typ.endswith(clazz.fullname):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001150 warn(clazz, m, "M4", "Methods must return the builder object")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001151
1152 if not has_build:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001153 warn(clazz, None, None, "Missing build() method")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001154
Adrian Roosdeb0ff22019-02-27 23:58:13 +01001155 if "final" not in clazz.split:
1156 error(clazz, None, None, "Builder should be final")
1157
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001158
Adrian Roos93bafc42019-03-05 18:31:37 +01001159@verifier
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001160def verify_aidl(clazz):
1161 """Catch people exposing raw AIDL."""
Adrian Roosb787c182019-01-03 18:54:33 +01001162 if clazz.extends == "android.os.Binder" or clazz.implements == "android.os.IInterface":
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001163 error(clazz, None, None, "Raw AIDL interfaces must not be exposed")
Jeff Sharkey8190f4882014-08-28 12:24:07 -07001164
1165
Adrian Roos93bafc42019-03-05 18:31:37 +01001166@verifier
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001167def verify_internal(clazz):
1168 """Catch people exposing internal classes."""
1169 if clazz.pkg.name.startswith("com.android"):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001170 error(clazz, None, None, "Internal classes must not be exposed")
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001171
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001172def layering_build_ranking(ranking_list):
1173 r = {}
1174 for rank, ps in enumerate(ranking_list):
1175 if not isinstance(ps, list):
1176 ps = [ps]
1177 for p in ps:
1178 rs = r
1179 for n in p.split('.'):
1180 if n not in rs:
1181 rs[n] = {}
1182 rs = rs[n]
1183 rs['-rank'] = rank
1184 return r
1185
1186LAYERING_PACKAGE_RANKING = layering_build_ranking([
1187 ["android.service","android.accessibilityservice","android.inputmethodservice","android.printservice","android.appwidget","android.webkit","android.preference","android.gesture","android.print"],
1188 "android.app",
1189 "android.widget",
1190 "android.view",
1191 "android.animation",
1192 "android.provider",
1193 ["android.content","android.graphics.drawable"],
1194 "android.database",
1195 "android.text",
1196 "android.graphics",
1197 "android.os",
1198 "android.util"
1199])
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001200
Adrian Roos93bafc42019-03-05 18:31:37 +01001201@verifier
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001202def verify_layering(clazz):
1203 """Catch package layering violations.
1204 For example, something in android.os depending on android.app."""
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001205
1206 def rank(p):
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001207 r = None
1208 l = LAYERING_PACKAGE_RANKING
1209 for n in p.split('.'):
1210 if n in l:
1211 l = l[n]
1212 if '-rank' in l:
1213 r = l['-rank']
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001214 else:
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001215 break
1216 return r
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001217
1218 cr = rank(clazz.pkg.name)
1219 if cr is None: return
1220
1221 for f in clazz.fields:
1222 ir = rank(f.typ)
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001223 if ir is not None and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001224 warn(clazz, f, "FW6", "Field type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001225
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001226 for m in itertools.chain(clazz.methods, clazz.ctors):
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001227 ir = rank(m.typ)
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001228 if ir is not None and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001229 warn(clazz, m, "FW6", "Method return type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001230 for arg in m.args:
1231 ir = rank(arg)
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001232 if ir is not None and ir < cr:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001233 warn(clazz, m, "FW6", "Method argument type violates package layering")
Jeff Sharkey932a07c2014-08-28 16:16:02 -07001234
1235
Adrian Roos93bafc42019-03-05 18:31:37 +01001236@verifier
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001237def verify_boolean(clazz):
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001238 """Verifies that boolean accessors are named correctly.
1239 For example, hasFoo() and setHasFoo()."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001240
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001241 def is_get(m): return len(m.args) == 0 and m.typ == "boolean"
1242 def is_set(m): return len(m.args) == 1 and m.args[0] == "boolean"
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001243
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001244 gets = [ m for m in clazz.methods if is_get(m) ]
1245 sets = [ m for m in clazz.methods if is_set(m) ]
1246
1247 def error_if_exists(methods, trigger, expected, actual):
1248 for m in methods:
1249 if m.name == actual:
1250 error(clazz, m, "M6", "Symmetric method for %s must be named %s" % (trigger, expected))
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001251
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001252 for m in clazz.methods:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001253 if is_get(m):
1254 if re.match("is[A-Z]", m.name):
1255 target = m.name[2:]
1256 expected = "setIs" + target
1257 error_if_exists(sets, m.name, expected, "setHas" + target)
1258 elif re.match("has[A-Z]", m.name):
1259 target = m.name[3:]
1260 expected = "setHas" + target
1261 error_if_exists(sets, m.name, expected, "setIs" + target)
1262 error_if_exists(sets, m.name, expected, "set" + target)
1263 elif re.match("get[A-Z]", m.name):
1264 target = m.name[3:]
1265 expected = "set" + target
1266 error_if_exists(sets, m.name, expected, "setIs" + target)
1267 error_if_exists(sets, m.name, expected, "setHas" + target)
1268
1269 if is_set(m):
1270 if re.match("set[A-Z]", m.name):
1271 target = m.name[3:]
1272 expected = "get" + target
1273 error_if_exists(sets, m.name, expected, "is" + target)
1274 error_if_exists(sets, m.name, expected, "has" + target)
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001275
1276
Adrian Roos93bafc42019-03-05 18:31:37 +01001277@verifier
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001278def verify_collections(clazz):
1279 """Verifies that collection types are interfaces."""
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001280 if clazz.fullname == "android.os.Bundle": return
Adrian Roos02e18dd2019-02-28 12:41:48 +01001281 if clazz.fullname == "android.os.Parcel": return
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001282
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001283 bad = ["java.util.Vector", "java.util.LinkedList", "java.util.ArrayList", "java.util.Stack",
1284 "java.util.HashMap", "java.util.HashSet", "android.util.ArraySet", "android.util.ArrayMap"]
1285 for m in clazz.methods:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001286 if m.typ in bad:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001287 error(clazz, m, "CL2", "Return type is concrete collection; must be higher-level interface")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001288 for arg in m.args:
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07001289 if arg in bad:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001290 error(clazz, m, "CL2", "Argument is concrete collection; must be higher-level interface")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001291
Adrian Roos93bafc42019-03-05 18:31:37 +01001292
1293@verifier
Adrian Roos56ff7842019-02-28 12:45:00 +01001294def verify_uris(clazz):
1295 bad = ["java.net.URL", "java.net.URI", "android.net.URL"]
1296
1297 for f in clazz.fields:
1298 if f.typ in bad:
1299 error(clazz, f, None, "Field must be android.net.Uri instead of " + f.typ)
1300
1301 for m in clazz.methods + clazz.ctors:
1302 if m.typ in bad:
1303 error(clazz, m, None, "Must return android.net.Uri instead of " + m.typ)
1304 for arg in m.args:
1305 if arg in bad:
1306 error(clazz, m, None, "Argument must take android.net.Uri instead of " + arg)
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001307
Adrian Roos93bafc42019-03-05 18:31:37 +01001308
1309@verifier
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001310def verify_flags(clazz):
1311 """Verifies that flags are non-overlapping."""
1312 known = collections.defaultdict(int)
1313 for f in clazz.fields:
1314 if "FLAG_" in f.name:
1315 try:
1316 val = int(f.value)
1317 except:
1318 continue
1319
1320 scope = f.name[0:f.name.index("FLAG_")]
1321 if val & known[scope]:
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001322 warn(clazz, f, "C1", "Found overlapping flag constant value")
Jeff Sharkey294f0de2014-08-29 17:41:43 -07001323 known[scope] |= val
1324
1325
Adrian Roos93bafc42019-03-05 18:31:37 +01001326@verifier
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001327def verify_exception(clazz):
1328 """Verifies that methods don't throw generic exceptions."""
1329 for m in clazz.methods:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001330 for t in m.throws:
1331 if t in ["java.lang.Exception", "java.lang.Throwable", "java.lang.Error"]:
1332 error(clazz, m, "S1", "Methods must not throw generic exceptions")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001333
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001334 if t in ["android.os.RemoteException"]:
Adrian Roos02e18dd2019-02-28 12:41:48 +01001335 if clazz.fullname == "android.content.ContentProviderClient": continue
1336 if clazz.fullname == "android.os.Binder": continue
1337 if clazz.fullname == "android.os.IBinder": continue
Jeff Sharkey331279b2016-02-29 16:02:02 -07001338
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001339 error(clazz, m, "FW9", "Methods calling into system server should rethrow RemoteException as RuntimeException")
1340
1341 if len(m.args) == 0 and t in ["java.lang.IllegalArgumentException", "java.lang.NullPointerException"]:
1342 warn(clazz, m, "S1", "Methods taking no arguments should throw IllegalStateException")
Jeff Sharkey331279b2016-02-29 16:02:02 -07001343
Adrian Roos93bafc42019-03-05 18:31:37 +01001344
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001345GOOGLE_IGNORECASE = re.compile("google", re.IGNORECASE)
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001346
Adrian Roos93bafc42019-03-05 18:31:37 +01001347# Not marked as @verifier, because it is only conditionally applied.
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001348def verify_google(clazz):
1349 """Verifies that APIs never reference Google."""
1350
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001351 if GOOGLE_IGNORECASE.search(clazz.raw) is not None:
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001352 error(clazz, None, None, "Must never reference Google")
1353
Adrian Roos1f1b6a82019-01-05 20:09:38 +01001354 for test in clazz.ctors, clazz.fields, clazz.methods:
1355 for t in test:
1356 if GOOGLE_IGNORECASE.search(t.raw) is not None:
1357 error(clazz, t, None, "Must never reference Google")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001358
1359
Adrian Roos93bafc42019-03-05 18:31:37 +01001360@verifier
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001361def verify_bitset(clazz):
1362 """Verifies that we avoid using heavy BitSet."""
1363
1364 for f in clazz.fields:
1365 if f.typ == "java.util.BitSet":
1366 error(clazz, f, None, "Field type must not be heavy BitSet")
1367
1368 for m in clazz.methods:
1369 if m.typ == "java.util.BitSet":
1370 error(clazz, m, None, "Return type must not be heavy BitSet")
1371 for arg in m.args:
1372 if arg == "java.util.BitSet":
1373 error(clazz, m, None, "Argument type must not be heavy BitSet")
1374
1375
Adrian Roos93bafc42019-03-05 18:31:37 +01001376@verifier
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001377def verify_manager(clazz):
1378 """Verifies that FooManager is only obtained from Context."""
1379
1380 if not clazz.name.endswith("Manager"): return
1381
1382 for c in clazz.ctors:
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001383 error(clazz, c, None, "Managers must always be obtained from Context; no direct constructors")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001384
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001385 for m in clazz.methods:
1386 if m.typ == clazz.fullname:
1387 error(clazz, m, None, "Managers must always be obtained from Context")
1388
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001389
Adrian Roos93bafc42019-03-05 18:31:37 +01001390@verifier
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001391def verify_boxed(clazz):
1392 """Verifies that methods avoid boxed primitives."""
1393
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001394 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 -08001395
1396 for c in clazz.ctors:
1397 for arg in c.args:
1398 if arg in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001399 error(clazz, c, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001400
1401 for f in clazz.fields:
1402 if f.typ in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001403 error(clazz, f, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001404
1405 for m in clazz.methods:
1406 if m.typ in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001407 error(clazz, m, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001408 for arg in m.args:
1409 if arg in boxed:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001410 error(clazz, m, "M11", "Must avoid boxed primitives")
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001411
1412
Adrian Roos93bafc42019-03-05 18:31:37 +01001413@verifier
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001414def verify_static_utils(clazz):
1415 """Verifies that helper classes can't be constructed."""
1416 if clazz.fullname.startswith("android.opengl"): return
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001417 if clazz.fullname.startswith("android.R"): return
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001418
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001419 # Only care about classes with default constructors
1420 if len(clazz.ctors) == 1 and len(clazz.ctors[0].args) == 0:
1421 test = []
1422 test.extend(clazz.fields)
1423 test.extend(clazz.methods)
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001424
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001425 if len(test) == 0: return
1426 for t in test:
1427 if "static" not in t.split:
1428 return
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001429
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08001430 error(clazz, None, None, "Fully-static utility classes must not have constructor")
1431
1432
Adrian Roos93bafc42019-03-05 18:31:37 +01001433# @verifier # Disabled for now
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001434def verify_overload_args(clazz):
1435 """Verifies that method overloads add new arguments at the end."""
1436 if clazz.fullname.startswith("android.opengl"): return
1437
1438 overloads = collections.defaultdict(list)
1439 for m in clazz.methods:
1440 if "deprecated" in m.split: continue
1441 overloads[m.name].append(m)
1442
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08001443 for name, methods in overloads.items():
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001444 if len(methods) <= 1: continue
1445
1446 # Look for arguments common across all overloads
1447 def cluster(args):
1448 count = collections.defaultdict(int)
1449 res = set()
1450 for i in range(len(args)):
1451 a = args[i]
1452 res.add("%s#%d" % (a, count[a]))
1453 count[a] += 1
1454 return res
1455
1456 common_args = cluster(methods[0].args)
1457 for m in methods:
1458 common_args = common_args & cluster(m.args)
1459
1460 if len(common_args) == 0: continue
1461
1462 # Require that all common arguments are present at start of signature
1463 locked_sig = None
1464 for m in methods:
1465 sig = m.args[0:len(common_args)]
1466 if not common_args.issubset(cluster(sig)):
1467 warn(clazz, m, "M2", "Expected common arguments [%s] at beginning of overloaded method" % (", ".join(common_args)))
1468 elif not locked_sig:
1469 locked_sig = sig
1470 elif locked_sig != sig:
1471 error(clazz, m, "M2", "Expected consistent argument ordering between overloads: %s..." % (", ".join(locked_sig)))
1472
1473
Adrian Roos93bafc42019-03-05 18:31:37 +01001474@verifier
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001475def verify_callback_handlers(clazz):
1476 """Verifies that methods adding listener/callback have overload
1477 for specifying delivery thread."""
1478
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001479 # Ignore UI packages which assume main thread
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001480 skip = [
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001481 "animation",
1482 "view",
1483 "graphics",
1484 "transition",
1485 "widget",
1486 "webkit",
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001487 ]
1488 for s in skip:
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001489 if s in clazz.pkg.name_path: return
1490 if s in clazz.extends_path: return
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001491
Jeff Sharkey047d7f02015-02-25 11:27:55 -08001492 # Ignore UI classes which assume main thread
1493 if "app" in clazz.pkg.name_path or "app" in clazz.extends_path:
1494 for s in ["ActionBar","Dialog","Application","Activity","Fragment","Loader"]:
1495 if s in clazz.fullname: return
1496 if "content" in clazz.pkg.name_path or "content" in clazz.extends_path:
1497 for s in ["Loader"]:
1498 if s in clazz.fullname: return
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001499
1500 found = {}
1501 by_name = collections.defaultdict(list)
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001502 examine = clazz.ctors + clazz.methods
1503 for m in examine:
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001504 if m.name.startswith("unregister"): continue
1505 if m.name.startswith("remove"): continue
1506 if re.match("on[A-Z]+", m.name): continue
1507
1508 by_name[m.name].append(m)
1509
1510 for a in m.args:
1511 if a.endswith("Listener") or a.endswith("Callback") or a.endswith("Callbacks"):
1512 found[m.name] = m
1513
1514 for f in found.values():
1515 takes_handler = False
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001516 takes_exec = False
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001517 for m in by_name[f.name]:
1518 if "android.os.Handler" in m.args:
1519 takes_handler = True
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001520 if "java.util.concurrent.Executor" in m.args:
1521 takes_exec = True
1522 if not takes_exec:
1523 warn(clazz, f, "L1", "Registration methods should have overload that accepts delivery Executor")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001524
1525
Adrian Roos93bafc42019-03-05 18:31:37 +01001526@verifier
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001527def verify_context_first(clazz):
1528 """Verifies that methods accepting a Context keep it the first argument."""
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001529 examine = clazz.ctors + clazz.methods
1530 for m in examine:
1531 if len(m.args) > 1 and m.args[0] != "android.content.Context":
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001532 if "android.content.Context" in m.args[1:]:
1533 error(clazz, m, "M3", "Context is distinct, so it must be the first argument")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001534 if len(m.args) > 1 and m.args[0] != "android.content.ContentResolver":
1535 if "android.content.ContentResolver" in m.args[1:]:
1536 error(clazz, m, "M3", "ContentResolver is distinct, so it must be the first argument")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001537
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001538
Adrian Roos93bafc42019-03-05 18:31:37 +01001539@verifier
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001540def verify_listener_last(clazz):
1541 """Verifies that methods accepting a Listener or Callback keep them as last arguments."""
1542 examine = clazz.ctors + clazz.methods
1543 for m in examine:
1544 if "Listener" in m.name or "Callback" in m.name: continue
1545 found = False
1546 for a in m.args:
1547 if a.endswith("Callback") or a.endswith("Callbacks") or a.endswith("Listener"):
1548 found = True
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001549 elif found:
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001550 warn(clazz, m, "M3", "Listeners should always be at end of argument list")
1551
1552
Adrian Roos93bafc42019-03-05 18:31:37 +01001553@verifier
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001554def verify_resource_names(clazz):
1555 """Verifies that resource names have consistent case."""
1556 if not re.match("android\.R\.[a-z]+", clazz.fullname): return
1557
1558 # Resources defined by files are foo_bar_baz
1559 if clazz.name in ["anim","animator","color","dimen","drawable","interpolator","layout","transition","menu","mipmap","string","plurals","raw","xml"]:
1560 for f in clazz.fields:
Jeff Sharkeyeff9e222018-09-27 16:29:25 -06001561 if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue
1562 if f.name.startswith("config_"):
1563 error(clazz, f, None, "Expected config name to be config_fooBarBaz style")
1564
Jeff Sharkey90b547b2015-02-18 16:45:54 -08001565 if re.match("[a-z1-9_]+$", f.name): continue
1566 error(clazz, f, None, "Expected resource name in this class to be foo_bar_baz style")
1567
1568 # Resources defined inside files are fooBarBaz
1569 if clazz.name in ["array","attr","id","bool","fraction","integer"]:
1570 for f in clazz.fields:
1571 if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue
1572 if re.match("layout_[a-z][a-zA-Z1-9]*$", f.name): continue
1573 if re.match("state_[a-z_]*$", f.name): continue
1574
1575 if re.match("[a-z][a-zA-Z1-9]*$", f.name): continue
1576 error(clazz, f, "C7", "Expected resource name in this class to be fooBarBaz style")
1577
1578 # Styles are FooBar_Baz
1579 if clazz.name in ["style"]:
1580 for f in clazz.fields:
1581 if re.match("[A-Z][A-Za-z1-9]+(_[A-Z][A-Za-z1-9]+?)*$", f.name): continue
1582 error(clazz, f, "C7", "Expected resource name in this class to be FooBar_Baz style")
Jeff Sharkeyb46a9692015-02-17 17:19:41 -08001583
1584
Adrian Roos93bafc42019-03-05 18:31:37 +01001585@verifier
Jeff Sharkey331279b2016-02-29 16:02:02 -07001586def verify_files(clazz):
1587 """Verifies that methods accepting File also accept streams."""
1588
1589 has_file = set()
1590 has_stream = set()
1591
1592 test = []
1593 test.extend(clazz.ctors)
1594 test.extend(clazz.methods)
1595
1596 for m in test:
1597 if "java.io.File" in m.args:
1598 has_file.add(m)
1599 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:
1600 has_stream.add(m.name)
1601
1602 for m in has_file:
1603 if m.name not in has_stream:
1604 warn(clazz, m, "M10", "Methods accepting File should also accept FileDescriptor or streams")
1605
1606
Adrian Roos93bafc42019-03-05 18:31:37 +01001607@verifier
Jeff Sharkey70168dd2016-03-30 21:47:16 -06001608def verify_manager_list(clazz):
1609 """Verifies that managers return List<? extends Parcelable> instead of arrays."""
1610
1611 if not clazz.name.endswith("Manager"): return
1612
1613 for m in clazz.methods:
1614 if m.typ.startswith("android.") and m.typ.endswith("[]"):
1615 warn(clazz, m, None, "Methods should return List<? extends Parcelable> instead of Parcelable[] to support ParceledListSlice under the hood")
1616
1617
Adrian Roos93bafc42019-03-05 18:31:37 +01001618@verifier
Jeff Sharkey26c80902016-12-21 13:41:17 -07001619def verify_abstract_inner(clazz):
1620 """Verifies that abstract inner classes are static."""
1621
1622 if re.match(".+?\.[A-Z][^\.]+\.[A-Z]", clazz.fullname):
Adrian Roosb787c182019-01-03 18:54:33 +01001623 if "abstract" in clazz.split and "static" not in clazz.split:
Jeff Sharkey26c80902016-12-21 13:41:17 -07001624 warn(clazz, None, None, "Abstract inner classes should be static to improve testability")
1625
1626
Adrian Roos93bafc42019-03-05 18:31:37 +01001627@verifier
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001628def verify_runtime_exceptions(clazz):
1629 """Verifies that runtime exceptions aren't listed in throws."""
1630
1631 banned = [
1632 "java.lang.NullPointerException",
1633 "java.lang.ClassCastException",
1634 "java.lang.IndexOutOfBoundsException",
1635 "java.lang.reflect.UndeclaredThrowableException",
1636 "java.lang.reflect.MalformedParametersException",
1637 "java.lang.reflect.MalformedParameterizedTypeException",
1638 "java.lang.invoke.WrongMethodTypeException",
1639 "java.lang.EnumConstantNotPresentException",
1640 "java.lang.IllegalMonitorStateException",
1641 "java.lang.SecurityException",
1642 "java.lang.UnsupportedOperationException",
1643 "java.lang.annotation.AnnotationTypeMismatchException",
1644 "java.lang.annotation.IncompleteAnnotationException",
1645 "java.lang.TypeNotPresentException",
1646 "java.lang.IllegalStateException",
1647 "java.lang.ArithmeticException",
1648 "java.lang.IllegalArgumentException",
1649 "java.lang.ArrayStoreException",
1650 "java.lang.NegativeArraySizeException",
1651 "java.util.MissingResourceException",
1652 "java.util.EmptyStackException",
1653 "java.util.concurrent.CompletionException",
1654 "java.util.concurrent.RejectedExecutionException",
1655 "java.util.IllformedLocaleException",
1656 "java.util.ConcurrentModificationException",
1657 "java.util.NoSuchElementException",
1658 "java.io.UncheckedIOException",
1659 "java.time.DateTimeException",
1660 "java.security.ProviderException",
1661 "java.nio.BufferUnderflowException",
1662 "java.nio.BufferOverflowException",
1663 ]
1664
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001665 examine = clazz.ctors + clazz.methods
1666 for m in examine:
1667 for t in m.throws:
1668 if t in banned:
1669 error(clazz, m, None, "Methods must not mention RuntimeException subclasses in throws clauses")
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001670
1671
Adrian Roos93bafc42019-03-05 18:31:37 +01001672@verifier
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001673def verify_error(clazz):
1674 """Verifies that we always use Exception instead of Error."""
1675 if not clazz.extends: return
1676 if clazz.extends.endswith("Error"):
1677 error(clazz, None, None, "Trouble must be reported through an Exception, not Error")
1678 if clazz.extends.endswith("Exception") and not clazz.name.endswith("Exception"):
1679 error(clazz, None, None, "Exceptions must be named FooException")
1680
1681
Adrian Roos93bafc42019-03-05 18:31:37 +01001682@verifier
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001683def verify_units(clazz):
1684 """Verifies that we use consistent naming for units."""
1685
1686 # If we find K, recommend replacing with V
1687 bad = {
1688 "Ns": "Nanos",
1689 "Ms": "Millis or Micros",
1690 "Sec": "Seconds", "Secs": "Seconds",
1691 "Hr": "Hours", "Hrs": "Hours",
1692 "Mo": "Months", "Mos": "Months",
1693 "Yr": "Years", "Yrs": "Years",
1694 "Byte": "Bytes", "Space": "Bytes",
1695 }
1696
1697 for m in clazz.methods:
1698 if m.typ not in ["short","int","long"]: continue
1699 for k, v in bad.iteritems():
1700 if m.name.endswith(k):
1701 error(clazz, m, None, "Expected method name units to be " + v)
1702 if m.name.endswith("Nanos") or m.name.endswith("Micros"):
1703 warn(clazz, m, None, "Returned time values are strongly encouraged to be in milliseconds unless you need the extra precision")
1704 if m.name.endswith("Seconds"):
1705 error(clazz, m, None, "Returned time values must be in milliseconds")
1706
1707 for m in clazz.methods:
1708 typ = m.typ
1709 if typ == "void":
1710 if len(m.args) != 1: continue
1711 typ = m.args[0]
1712
1713 if m.name.endswith("Fraction") and typ != "float":
1714 error(clazz, m, None, "Fractions must use floats")
1715 if m.name.endswith("Percentage") and typ != "int":
1716 error(clazz, m, None, "Percentage must use ints")
1717
1718
Adrian Roos93bafc42019-03-05 18:31:37 +01001719@verifier
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001720def verify_closable(clazz):
1721 """Verifies that classes are AutoClosable."""
Adrian Roos02e18dd2019-02-28 12:41:48 +01001722 if "java.lang.AutoCloseable" in clazz.implements_all: return
1723 if "java.io.Closeable" in clazz.implements_all: return
Jeff Sharkey24bda1d2017-04-19 16:04:44 -06001724
1725 for m in clazz.methods:
1726 if len(m.args) > 0: continue
1727 if m.name in ["close","release","destroy","finish","finalize","disconnect","shutdown","stop","free","quit"]:
1728 warn(clazz, m, None, "Classes that release resources should implement AutoClosable and CloseGuard")
1729 return
1730
1731
Adrian Roos93bafc42019-03-05 18:31:37 +01001732@verifier
Jake Wharton9e6738f2017-08-23 11:59:55 -04001733def verify_member_name_not_kotlin_keyword(clazz):
1734 """Prevent method names which are keywords in Kotlin."""
1735
1736 # https://kotlinlang.org/docs/reference/keyword-reference.html#hard-keywords
1737 # This list does not include Java keywords as those are already impossible to use.
1738 keywords = [
1739 'as',
1740 'fun',
1741 'in',
1742 'is',
1743 'object',
1744 'typealias',
1745 'val',
1746 'var',
1747 'when',
1748 ]
1749
1750 for m in clazz.methods:
1751 if m.name in keywords:
1752 error(clazz, m, None, "Method name must not be a Kotlin keyword")
1753 for f in clazz.fields:
1754 if f.name in keywords:
1755 error(clazz, f, None, "Field name must not be a Kotlin keyword")
1756
1757
Adrian Roos93bafc42019-03-05 18:31:37 +01001758@verifier
Jake Wharton9e6738f2017-08-23 11:59:55 -04001759def verify_method_name_not_kotlin_operator(clazz):
1760 """Warn about method names which become operators in Kotlin."""
1761
1762 binary = set()
1763
1764 def unique_binary_op(m, op):
1765 if op in binary:
1766 error(clazz, m, None, "Only one of '{0}' and '{0}Assign' methods should be present for Kotlin".format(op))
1767 binary.add(op)
1768
1769 for m in clazz.methods:
Adrian Roosd1e38922019-01-14 15:44:15 +01001770 if 'static' in m.split or 'operator' in m.split:
Jake Wharton9e6738f2017-08-23 11:59:55 -04001771 continue
1772
1773 # https://kotlinlang.org/docs/reference/operator-overloading.html#unary-prefix-operators
1774 if m.name in ['unaryPlus', 'unaryMinus', 'not'] and len(m.args) == 0:
1775 warn(clazz, m, None, "Method can be invoked as a unary operator from Kotlin")
1776
1777 # https://kotlinlang.org/docs/reference/operator-overloading.html#increments-and-decrements
1778 if m.name in ['inc', 'dec'] and len(m.args) == 0 and m.typ != 'void':
1779 # This only applies if the return type is the same or a subtype of the enclosing class, but we have no
1780 # practical way of checking that relationship here.
1781 warn(clazz, m, None, "Method can be invoked as a pre/postfix inc/decrement operator from Kotlin")
1782
1783 # https://kotlinlang.org/docs/reference/operator-overloading.html#arithmetic
1784 if m.name in ['plus', 'minus', 'times', 'div', 'rem', 'mod', 'rangeTo'] and len(m.args) == 1:
1785 warn(clazz, m, None, "Method can be invoked as a binary operator from Kotlin")
1786 unique_binary_op(m, m.name)
1787
1788 # https://kotlinlang.org/docs/reference/operator-overloading.html#in
1789 if m.name == 'contains' and len(m.args) == 1 and m.typ == 'boolean':
1790 warn(clazz, m, None, "Method can be invoked as a 'in' operator from Kotlin")
1791
1792 # https://kotlinlang.org/docs/reference/operator-overloading.html#indexed
1793 if (m.name == 'get' and len(m.args) > 0) or (m.name == 'set' and len(m.args) > 1):
1794 warn(clazz, m, None, "Method can be invoked with an indexing operator from Kotlin")
1795
1796 # https://kotlinlang.org/docs/reference/operator-overloading.html#invoke
1797 if m.name == 'invoke':
1798 warn(clazz, m, None, "Method can be invoked with function call syntax from Kotlin")
1799
1800 # https://kotlinlang.org/docs/reference/operator-overloading.html#assignments
1801 if m.name in ['plusAssign', 'minusAssign', 'timesAssign', 'divAssign', 'remAssign', 'modAssign'] \
1802 and len(m.args) == 1 \
1803 and m.typ == 'void':
1804 warn(clazz, m, None, "Method can be invoked as a compound assignment operator from Kotlin")
1805 unique_binary_op(m, m.name[:-6]) # Remove 'Assign' suffix
1806
1807
Adrian Roos93bafc42019-03-05 18:31:37 +01001808@verifier
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001809def verify_collections_over_arrays(clazz):
1810 """Warn that [] should be Collections."""
1811
Adrian Roosb787c182019-01-03 18:54:33 +01001812 if "@interface" in clazz.split:
1813 return
1814
Jeff Sharkeybedb3fc2017-12-05 09:42:28 -07001815 safe = ["java.lang.String[]","byte[]","short[]","int[]","long[]","float[]","double[]","boolean[]","char[]"]
1816 for m in clazz.methods:
1817 if m.typ.endswith("[]") and m.typ not in safe:
1818 warn(clazz, m, None, "Method should return Collection<> (or subclass) instead of raw array")
1819 for arg in m.args:
1820 if arg.endswith("[]") and arg not in safe:
1821 warn(clazz, m, None, "Method argument should be Collection<> (or subclass) instead of raw array")
1822
1823
Adrian Roos93bafc42019-03-05 18:31:37 +01001824@verifier
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001825def verify_user_handle(clazz):
1826 """Methods taking UserHandle should be ForUser or AsUser."""
1827 if clazz.name.endswith("Listener") or clazz.name.endswith("Callback") or clazz.name.endswith("Callbacks"): return
1828 if clazz.fullname == "android.app.admin.DeviceAdminReceiver": return
1829 if clazz.fullname == "android.content.pm.LauncherApps": return
1830 if clazz.fullname == "android.os.UserHandle": return
1831 if clazz.fullname == "android.os.UserManager": return
1832
1833 for m in clazz.methods:
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001834 if re.match("on[A-Z]+", m.name): continue
Jeff Sharkeya8e5df02018-11-27 17:33:42 -07001835
1836 has_arg = "android.os.UserHandle" in m.args
1837 has_name = m.name.endswith("AsUser") or m.name.endswith("ForUser")
1838
1839 if clazz.fullname.endswith("Manager") and has_arg:
1840 warn(clazz, m, None, "When a method overload is needed to target a specific "
1841 "UserHandle, callers should be directed to use "
1842 "Context.createPackageContextAsUser() and re-obtain the relevant "
1843 "Manager, and no new API should be added")
1844 elif has_arg and not has_name:
1845 warn(clazz, m, None, "Method taking UserHandle should be named 'doFooAsUser' "
1846 "or 'queryFooForUser'")
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001847
1848
Adrian Roos93bafc42019-03-05 18:31:37 +01001849@verifier
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001850def verify_params(clazz):
1851 """Parameter classes should be 'Params'."""
1852 if clazz.name.endswith("Params"): return
1853 if clazz.fullname == "android.app.ActivityOptions": return
1854 if clazz.fullname == "android.app.BroadcastOptions": return
1855 if clazz.fullname == "android.os.Bundle": return
1856 if clazz.fullname == "android.os.BaseBundle": return
1857 if clazz.fullname == "android.os.PersistableBundle": return
1858
1859 bad = ["Param","Parameter","Parameters","Args","Arg","Argument","Arguments","Options","Bundle"]
1860 for b in bad:
1861 if clazz.name.endswith(b):
1862 error(clazz, None, None, "Classes holding a set of parameters should be called 'FooParams'")
1863
1864
Adrian Roos93bafc42019-03-05 18:31:37 +01001865@verifier
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001866def verify_services(clazz):
1867 """Service name should be FOO_BAR_SERVICE = 'foo_bar'."""
1868 if clazz.fullname != "android.content.Context": return
1869
1870 for f in clazz.fields:
1871 if f.typ != "java.lang.String": continue
1872 found = re.match(r"([A-Z_]+)_SERVICE", f.name)
1873 if found:
1874 expected = found.group(1).lower()
1875 if f.value != expected:
1876 error(clazz, f, "C4", "Inconsistent service value; expected '%s'" % (expected))
1877
1878
Adrian Roos93bafc42019-03-05 18:31:37 +01001879@verifier
Jeff Sharkey336dd0b2018-01-12 12:12:27 -07001880def verify_tense(clazz):
1881 """Verify tenses of method names."""
1882 if clazz.fullname.startswith("android.opengl"): return
1883
1884 for m in clazz.methods:
1885 if m.name.endswith("Enable"):
1886 warn(clazz, m, None, "Unexpected tense; probably meant 'enabled'")
1887
1888
Adrian Roos93bafc42019-03-05 18:31:37 +01001889@verifier
Jeff Sharkeyee21f692018-02-16 13:29:29 -07001890def verify_icu(clazz):
1891 """Verifies that richer ICU replacements are used."""
1892 better = {
1893 "java.util.TimeZone": "android.icu.util.TimeZone",
1894 "java.util.Calendar": "android.icu.util.Calendar",
1895 "java.util.Locale": "android.icu.util.ULocale",
1896 "java.util.ResourceBundle": "android.icu.util.UResourceBundle",
1897 "java.util.SimpleTimeZone": "android.icu.util.SimpleTimeZone",
1898 "java.util.StringTokenizer": "android.icu.util.StringTokenizer",
1899 "java.util.GregorianCalendar": "android.icu.util.GregorianCalendar",
1900 "java.lang.Character": "android.icu.lang.UCharacter",
1901 "java.text.BreakIterator": "android.icu.text.BreakIterator",
1902 "java.text.Collator": "android.icu.text.Collator",
1903 "java.text.DecimalFormatSymbols": "android.icu.text.DecimalFormatSymbols",
1904 "java.text.NumberFormat": "android.icu.text.NumberFormat",
1905 "java.text.DateFormatSymbols": "android.icu.text.DateFormatSymbols",
1906 "java.text.DateFormat": "android.icu.text.DateFormat",
1907 "java.text.SimpleDateFormat": "android.icu.text.SimpleDateFormat",
1908 "java.text.MessageFormat": "android.icu.text.MessageFormat",
1909 "java.text.DecimalFormat": "android.icu.text.DecimalFormat",
1910 }
1911
1912 for m in clazz.ctors + clazz.methods:
1913 types = []
1914 types.extend(m.typ)
1915 types.extend(m.args)
1916 for arg in types:
1917 if arg in better:
1918 warn(clazz, m, None, "Type %s should be replaced with richer ICU type %s" % (arg, better[arg]))
1919
1920
Adrian Roos93bafc42019-03-05 18:31:37 +01001921@verifier
Jeff Sharkeyee21f692018-02-16 13:29:29 -07001922def verify_clone(clazz):
1923 """Verify that clone() isn't implemented; see EJ page 61."""
1924 for m in clazz.methods:
1925 if m.name == "clone":
1926 error(clazz, m, None, "Provide an explicit copy constructor instead of implementing clone()")
1927
1928
Adrian Roos93bafc42019-03-05 18:31:37 +01001929@verifier
Jeff Sharkeyeff9e222018-09-27 16:29:25 -06001930def verify_pfd(clazz):
1931 """Verify that android APIs use PFD over FD."""
Adrian Roos02e18dd2019-02-28 12:41:48 +01001932 if clazz.fullname == "android.os.FileUtils" or clazz.fullname == "android.system.Os":
1933 return
1934
Jeff Sharkeyeff9e222018-09-27 16:29:25 -06001935 examine = clazz.ctors + clazz.methods
1936 for m in examine:
1937 if m.typ == "java.io.FileDescriptor":
1938 error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
1939 if m.typ == "int":
1940 if "Fd" in m.name or "FD" in m.name or "FileDescriptor" in m.name:
1941 error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
1942 for arg in m.args:
1943 if arg == "java.io.FileDescriptor":
1944 error(clazz, m, "FW11", "Must use ParcelFileDescriptor")
1945
1946 for f in clazz.fields:
1947 if f.typ == "java.io.FileDescriptor":
1948 error(clazz, f, "FW11", "Must use ParcelFileDescriptor")
1949
1950
Adrian Roos93bafc42019-03-05 18:31:37 +01001951@verifier
Jeff Sharkeyeff9e222018-09-27 16:29:25 -06001952def verify_numbers(clazz):
1953 """Discourage small numbers types like short and byte."""
1954
1955 discouraged = ["short","byte"]
1956
1957 for c in clazz.ctors:
1958 for arg in c.args:
1959 if arg in discouraged:
1960 warn(clazz, c, "FW12", "Should avoid odd sized primitives; use int instead")
1961
1962 for f in clazz.fields:
1963 if f.typ in discouraged:
1964 warn(clazz, f, "FW12", "Should avoid odd sized primitives; use int instead")
1965
1966 for m in clazz.methods:
1967 if m.typ in discouraged:
1968 warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead")
1969 for arg in m.args:
1970 if arg in discouraged:
1971 warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead")
1972
Adrian Roos93bafc42019-03-05 18:31:37 +01001973
Adrian Roos80545ef2019-02-27 16:45:00 +01001974PRIMITIVES = {"void", "int", "float", "boolean", "short", "char", "byte", "long", "double"}
1975
Adrian Roos93bafc42019-03-05 18:31:37 +01001976@verifier
Adrian Roos80545ef2019-02-27 16:45:00 +01001977def verify_nullability(clazz):
1978 """Catches missing nullability annotations"""
1979
1980 for f in clazz.fields:
1981 if f.value is not None and 'static' in f.split and 'final' in f.split:
1982 continue # Nullability of constants can be inferred.
1983 if f.typ not in PRIMITIVES and not has_nullability(f.annotations):
1984 error(clazz, f, "M12", "Field must be marked either @NonNull or @Nullable")
1985
1986 for c in clazz.ctors:
1987 verify_nullability_args(clazz, c)
1988
1989 for m in clazz.methods:
1990 if m.name == "writeToParcel" or m.name == "onReceive":
1991 continue # Parcelable.writeToParcel() and BroadcastReceiver.onReceive() are not yet annotated
1992
1993 if m.typ not in PRIMITIVES and not has_nullability(m.annotations):
1994 error(clazz, m, "M12", "Return value must be marked either @NonNull or @Nullable")
1995 verify_nullability_args(clazz, m)
1996
1997def verify_nullability_args(clazz, m):
1998 for i, arg in enumerate(m.detailed_args):
1999 if arg.type not in PRIMITIVES and not has_nullability(arg.annotations):
2000 error(clazz, m, "M12", "Argument %d must be marked either @NonNull or @Nullable" % (i+1,))
2001
2002def has_nullability(annotations):
2003 return "@NonNull" in annotations or "@Nullable" in annotations
Jeff Sharkeyeff9e222018-09-27 16:29:25 -06002004
Adrian Roos93bafc42019-03-05 18:31:37 +01002005
2006@verifier
Jeff Sharkeyeff9e222018-09-27 16:29:25 -06002007def verify_singleton(clazz):
2008 """Catch singleton objects with constructors."""
2009
2010 singleton = False
2011 for m in clazz.methods:
2012 if m.name.startswith("get") and m.name.endswith("Instance") and " static " in m.raw:
2013 singleton = True
2014
2015 if singleton:
2016 for c in clazz.ctors:
2017 error(clazz, c, None, "Singleton classes should use getInstance() methods")
2018
2019
2020
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06002021def is_interesting(clazz):
2022 """Test if given class is interesting from an Android PoV."""
2023
2024 if clazz.pkg.name.startswith("java"): return False
2025 if clazz.pkg.name.startswith("junit"): return False
2026 if clazz.pkg.name.startswith("org.apache"): return False
2027 if clazz.pkg.name.startswith("org.xml"): return False
2028 if clazz.pkg.name.startswith("org.json"): return False
2029 if clazz.pkg.name.startswith("org.w3c"): return False
2030 if clazz.pkg.name.startswith("android.icu."): return False
2031 return True
2032
2033
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002034def examine_clazz(clazz):
2035 """Find all style issues in the given class."""
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002036
2037 notice(clazz)
2038
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06002039 if not is_interesting(clazz): return
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002040
Adrian Roos93bafc42019-03-05 18:31:37 +01002041 for v in verifiers.itervalues():
2042 v(clazz)
2043
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002044 if not ALLOW_GOOGLE: verify_google(clazz)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002045
2046
Adrian Roos038a0292018-12-19 17:11:21 +01002047def examine_stream(stream, base_stream=None, in_classes_with_base=[], out_classes_with_base=None):
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002048 """Find all style issues in the given API stream."""
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002049 global failures, noticed
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002050 failures = {}
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002051 noticed = {}
Adrian Roos038a0292018-12-19 17:11:21 +01002052 _parse_stream(stream, examine_clazz, base_f=base_stream,
2053 in_classes_with_base=in_classes_with_base,
2054 out_classes_with_base=out_classes_with_base)
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002055 return (failures, noticed)
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002056
2057
2058def examine_api(api):
2059 """Find all style issues in the given parsed API."""
2060 global failures
Jeff Sharkey1498f9c2014-09-04 12:45:33 -07002061 failures = {}
2062 for key in sorted(api.keys()):
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002063 examine_clazz(api[key])
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002064 return failures
2065
2066
Jeff Sharkey037458a2014-09-04 15:46:20 -07002067def verify_compat(cur, prev):
2068 """Find any incompatible API changes between two levels."""
2069 global failures
2070
2071 def class_exists(api, test):
2072 return test.fullname in api
2073
2074 def ctor_exists(api, clazz, test):
2075 for m in clazz.ctors:
2076 if m.ident == test.ident: return True
2077 return False
2078
2079 def all_methods(api, clazz):
2080 methods = list(clazz.methods)
2081 if clazz.extends is not None:
2082 methods.extend(all_methods(api, api[clazz.extends]))
2083 return methods
2084
2085 def method_exists(api, clazz, test):
2086 methods = all_methods(api, clazz)
2087 for m in methods:
2088 if m.ident == test.ident: return True
2089 return False
2090
2091 def field_exists(api, clazz, test):
2092 for f in clazz.fields:
2093 if f.ident == test.ident: return True
2094 return False
2095
2096 failures = {}
2097 for key in sorted(prev.keys()):
2098 prev_clazz = prev[key]
2099
2100 if not class_exists(cur, prev_clazz):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08002101 error(prev_clazz, None, None, "Class removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07002102 continue
2103
2104 cur_clazz = cur[key]
2105
2106 for test in prev_clazz.ctors:
2107 if not ctor_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08002108 error(prev_clazz, prev_ctor, None, "Constructor removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07002109
2110 methods = all_methods(prev, prev_clazz)
2111 for test in methods:
2112 if not method_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08002113 error(prev_clazz, test, None, "Method removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07002114
2115 for test in prev_clazz.fields:
2116 if not field_exists(cur, cur_clazz, test):
Jeff Sharkey9f64d5c62015-02-14 17:03:47 -08002117 error(prev_clazz, test, None, "Field removed or incompatible change")
Jeff Sharkey037458a2014-09-04 15:46:20 -07002118
2119 return failures
2120
2121
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002122def show_deprecations_at_birth(cur, prev):
2123 """Show API deprecations at birth."""
2124 global failures
2125
2126 # Remove all existing things so we're left with new
2127 for prev_clazz in prev.values():
2128 cur_clazz = cur[prev_clazz.fullname]
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06002129 if not is_interesting(cur_clazz): continue
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002130
2131 sigs = { i.ident: i for i in prev_clazz.ctors }
2132 cur_clazz.ctors = [ i for i in cur_clazz.ctors if i.ident not in sigs ]
2133 sigs = { i.ident: i for i in prev_clazz.methods }
2134 cur_clazz.methods = [ i for i in cur_clazz.methods if i.ident not in sigs ]
2135 sigs = { i.ident: i for i in prev_clazz.fields }
2136 cur_clazz.fields = [ i for i in cur_clazz.fields if i.ident not in sigs ]
2137
2138 # Forget about class entirely when nothing new
2139 if len(cur_clazz.ctors) == 0 and len(cur_clazz.methods) == 0 and len(cur_clazz.fields) == 0:
2140 del cur[prev_clazz.fullname]
2141
2142 for clazz in cur.values():
Adrian Roosb787c182019-01-03 18:54:33 +01002143 if "deprecated" in clazz.split and not clazz.fullname in prev:
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002144 error(clazz, None, None, "Found API deprecation at birth")
2145
2146 for i in clazz.ctors + clazz.methods + clazz.fields:
Adrian Roosb787c182019-01-03 18:54:33 +01002147 if "deprecated" in i.split:
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002148 error(clazz, i, None, "Found API deprecation at birth")
2149
2150 print "%s Deprecated at birth %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True),
2151 format(reset=True)))
2152 for f in sorted(failures):
2153 print failures[f]
2154 print
2155
2156
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06002157def show_stats(cur, prev):
2158 """Show API stats."""
2159
2160 stats = collections.defaultdict(int)
2161 for cur_clazz in cur.values():
2162 if not is_interesting(cur_clazz): continue
2163
2164 if cur_clazz.fullname not in prev:
2165 stats['new_classes'] += 1
2166 stats['new_ctors'] += len(cur_clazz.ctors)
2167 stats['new_methods'] += len(cur_clazz.methods)
2168 stats['new_fields'] += len(cur_clazz.fields)
2169 else:
2170 prev_clazz = prev[cur_clazz.fullname]
2171
2172 sigs = { i.ident: i for i in prev_clazz.ctors }
2173 ctors = len([ i for i in cur_clazz.ctors if i.ident not in sigs ])
2174 sigs = { i.ident: i for i in prev_clazz.methods }
2175 methods = len([ i for i in cur_clazz.methods if i.ident not in sigs ])
2176 sigs = { i.ident: i for i in prev_clazz.fields }
2177 fields = len([ i for i in cur_clazz.fields if i.ident not in sigs ])
2178
2179 if ctors + methods + fields > 0:
2180 stats['extend_classes'] += 1
2181 stats['extend_ctors'] += ctors
2182 stats['extend_methods'] += methods
2183 stats['extend_fields'] += fields
2184
2185 print "#", "".join([ k.ljust(20) for k in sorted(stats.keys()) ])
2186 print " ", "".join([ str(stats[k]).ljust(20) for k in sorted(stats.keys()) ])
2187
2188
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002189if __name__ == "__main__":
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002190 parser = argparse.ArgumentParser(description="Enforces common Android public API design \
2191 patterns. It ignores lint messages from a previous API level, if provided.")
2192 parser.add_argument("current.txt", type=argparse.FileType('r'), help="current.txt")
2193 parser.add_argument("previous.txt", nargs='?', type=argparse.FileType('r'), default=None,
2194 help="previous.txt")
Adrian Roos6eb57b02018-12-13 22:08:29 +01002195 parser.add_argument("--base-current", nargs='?', type=argparse.FileType('r'), default=None,
2196 help="The base current.txt to use when examining system-current.txt or"
2197 " test-current.txt")
2198 parser.add_argument("--base-previous", nargs='?', type=argparse.FileType('r'), default=None,
2199 help="The base previous.txt to use when examining system-previous.txt or"
2200 " test-previous.txt")
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002201 parser.add_argument("--no-color", action='store_const', const=True,
2202 help="Disable terminal colors")
2203 parser.add_argument("--allow-google", action='store_const', const=True,
2204 help="Allow references to Google")
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002205 parser.add_argument("--show-noticed", action='store_const', const=True,
2206 help="Show API changes noticed")
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002207 parser.add_argument("--show-deprecations-at-birth", action='store_const', const=True,
2208 help="Show API deprecations at birth")
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06002209 parser.add_argument("--show-stats", action='store_const', const=True,
2210 help="Show API stats")
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002211 args = vars(parser.parse_args())
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002212
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002213 if args['no_color']:
2214 USE_COLOR = False
2215
2216 if args['allow_google']:
2217 ALLOW_GOOGLE = True
2218
2219 current_file = args['current.txt']
Adrian Roos6eb57b02018-12-13 22:08:29 +01002220 base_current_file = args['base_current']
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002221 previous_file = args['previous.txt']
Adrian Roos6eb57b02018-12-13 22:08:29 +01002222 base_previous_file = args['base_previous']
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002223
Jeff Sharkey8b141b92018-04-11 10:05:44 -06002224 if args['show_deprecations_at_birth']:
2225 with current_file as f:
2226 cur = _parse_stream(f)
2227 with previous_file as f:
2228 prev = _parse_stream(f)
2229 show_deprecations_at_birth(cur, prev)
2230 sys.exit()
2231
Jeff Sharkeyfe5ee6e2018-04-20 11:26:16 -06002232 if args['show_stats']:
2233 with current_file as f:
2234 cur = _parse_stream(f)
2235 with previous_file as f:
2236 prev = _parse_stream(f)
2237 show_stats(cur, prev)
2238 sys.exit()
2239
Adrian Roos038a0292018-12-19 17:11:21 +01002240 classes_with_base = []
2241
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002242 with current_file as f:
Adrian Roos6eb57b02018-12-13 22:08:29 +01002243 if base_current_file:
2244 with base_current_file as base_f:
Adrian Roos038a0292018-12-19 17:11:21 +01002245 cur_fail, cur_noticed = examine_stream(f, base_f,
2246 out_classes_with_base=classes_with_base)
Adrian Roos6eb57b02018-12-13 22:08:29 +01002247 else:
Adrian Roos038a0292018-12-19 17:11:21 +01002248 cur_fail, cur_noticed = examine_stream(f, out_classes_with_base=classes_with_base)
2249
Adam Metcalf1c7e70a2015-03-27 16:11:23 -07002250 if not previous_file is None:
2251 with previous_file as f:
Adrian Roos6eb57b02018-12-13 22:08:29 +01002252 if base_previous_file:
2253 with base_previous_file as base_f:
Adrian Roos038a0292018-12-19 17:11:21 +01002254 prev_fail, prev_noticed = examine_stream(f, base_f,
2255 in_classes_with_base=classes_with_base)
Adrian Roos6eb57b02018-12-13 22:08:29 +01002256 else:
Adrian Roos038a0292018-12-19 17:11:21 +01002257 prev_fail, prev_noticed = examine_stream(f, in_classes_with_base=classes_with_base)
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002258
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002259 # ignore errors from previous API level
2260 for p in prev_fail:
2261 if p in cur_fail:
2262 del cur_fail[p]
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002263
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002264 # ignore classes unchanged from previous API level
2265 for k, v in prev_noticed.iteritems():
2266 if k in cur_noticed and v == cur_noticed[k]:
2267 del cur_noticed[k]
2268
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002269 """
2270 # NOTE: disabled because of memory pressure
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002271 # look for compatibility issues
2272 compat_fail = verify_compat(cur, prev)
Jeff Sharkey8190f4882014-08-28 12:24:07 -07002273
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002274 print "%s API compatibility issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
2275 for f in sorted(compat_fail):
2276 print compat_fail[f]
2277 print
Jeff Sharkeya18a2e32015-02-22 15:54:32 -08002278 """
Jeff Sharkeyed6aaf02015-01-30 13:31:45 -07002279
Jeff Sharkeyc6c6c342017-11-13 09:13:37 -07002280 if args['show_noticed'] and len(cur_noticed) != 0:
2281 print "%s API changes noticed %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
2282 for f in sorted(cur_noticed.keys()):
2283 print f
2284 print
2285
Jason Monk53b2a732017-11-10 15:43:17 -05002286 if len(cur_fail) != 0:
2287 print "%s API style issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True)))
2288 for f in sorted(cur_fail):
2289 print cur_fail[f]
2290 print
2291 sys.exit(77)