diff options
Diffstat (limited to 'tools/apilint/apilint.py')
-rw-r--r-- | tools/apilint/apilint.py | 150 |
1 files changed, 139 insertions, 11 deletions
diff --git a/tools/apilint/apilint.py b/tools/apilint/apilint.py index 77c1c24b17eb..421e54558df4 100644 --- a/tools/apilint/apilint.py +++ b/tools/apilint/apilint.py @@ -72,6 +72,9 @@ class Field(): self.ident = self.raw.replace(" deprecated ", " ") + def __hash__(self): + return hash(self.raw) + def __repr__(self): return self.raw @@ -110,6 +113,9 @@ class Method(): ident = ident[:ident.index(" throws ")] self.ident = ident + def __hash__(self): + return hash(self.raw) + def __repr__(self): return self.raw @@ -145,6 +151,9 @@ class Class(): self.name = self.fullname[self.fullname.rindex(".")+1:] + def __hash__(self): + return hash((self.raw, tuple(self.ctors), tuple(self.fields), tuple(self.methods))) + def __repr__(self): return self.raw @@ -256,6 +265,14 @@ def error(clazz, detail, rule, msg): _fail(clazz, detail, True, rule, msg) +noticed = {} + +def notice(clazz): + global noticed + + noticed[clazz.fullname] = hash(clazz) + + def verify_constants(clazz): """All static final constants must be FOO_NAME style.""" if re.match("android\.R\.[a-z]+", clazz.fullname): return @@ -923,11 +940,14 @@ def verify_callback_handlers(clazz): for f in found.values(): takes_handler = False + takes_exec = False for m in by_name[f.name]: if "android.os.Handler" in m.args: takes_handler = True - if not takes_handler: - warn(clazz, f, "L1", "Registration methods should have overload that accepts delivery Handler") + if "java.util.concurrent.Executor" in m.args: + takes_exec = True + if not takes_exec: + warn(clazz, f, "L1", "Registration methods should have overload that accepts delivery Executor") def verify_context_first(clazz): @@ -951,7 +971,7 @@ def verify_listener_last(clazz): for a in m.args: if a.endswith("Callback") or a.endswith("Callbacks") or a.endswith("Listener"): found = True - elif found and a != "android.os.Handler": + elif found and a != "android.os.Handler" and a != "java.util.concurrent.Executor": warn(clazz, m, "M3", "Listeners should always be at end of argument list") @@ -1127,8 +1147,97 @@ def verify_closable(clazz): return +def verify_member_name_not_kotlin_keyword(clazz): + """Prevent method names which are keywords in Kotlin.""" + + # https://kotlinlang.org/docs/reference/keyword-reference.html#hard-keywords + # This list does not include Java keywords as those are already impossible to use. + keywords = [ + 'as', + 'fun', + 'in', + 'is', + 'object', + 'typealias', + 'val', + 'var', + 'when', + ] + + for m in clazz.methods: + if m.name in keywords: + error(clazz, m, None, "Method name must not be a Kotlin keyword") + for f in clazz.fields: + if f.name in keywords: + error(clazz, f, None, "Field name must not be a Kotlin keyword") + + +def verify_method_name_not_kotlin_operator(clazz): + """Warn about method names which become operators in Kotlin.""" + + binary = set() + + def unique_binary_op(m, op): + if op in binary: + error(clazz, m, None, "Only one of '{0}' and '{0}Assign' methods should be present for Kotlin".format(op)) + binary.add(op) + + for m in clazz.methods: + if 'static' in m.split: + continue + + # https://kotlinlang.org/docs/reference/operator-overloading.html#unary-prefix-operators + if m.name in ['unaryPlus', 'unaryMinus', 'not'] and len(m.args) == 0: + warn(clazz, m, None, "Method can be invoked as a unary operator from Kotlin") + + # https://kotlinlang.org/docs/reference/operator-overloading.html#increments-and-decrements + if m.name in ['inc', 'dec'] and len(m.args) == 0 and m.typ != 'void': + # This only applies if the return type is the same or a subtype of the enclosing class, but we have no + # practical way of checking that relationship here. + warn(clazz, m, None, "Method can be invoked as a pre/postfix inc/decrement operator from Kotlin") + + # https://kotlinlang.org/docs/reference/operator-overloading.html#arithmetic + if m.name in ['plus', 'minus', 'times', 'div', 'rem', 'mod', 'rangeTo'] and len(m.args) == 1: + warn(clazz, m, None, "Method can be invoked as a binary operator from Kotlin") + unique_binary_op(m, m.name) + + # https://kotlinlang.org/docs/reference/operator-overloading.html#in + if m.name == 'contains' and len(m.args) == 1 and m.typ == 'boolean': + warn(clazz, m, None, "Method can be invoked as a 'in' operator from Kotlin") + + # https://kotlinlang.org/docs/reference/operator-overloading.html#indexed + if (m.name == 'get' and len(m.args) > 0) or (m.name == 'set' and len(m.args) > 1): + warn(clazz, m, None, "Method can be invoked with an indexing operator from Kotlin") + + # https://kotlinlang.org/docs/reference/operator-overloading.html#invoke + if m.name == 'invoke': + warn(clazz, m, None, "Method can be invoked with function call syntax from Kotlin") + + # https://kotlinlang.org/docs/reference/operator-overloading.html#assignments + if m.name in ['plusAssign', 'minusAssign', 'timesAssign', 'divAssign', 'remAssign', 'modAssign'] \ + and len(m.args) == 1 \ + and m.typ == 'void': + warn(clazz, m, None, "Method can be invoked as a compound assignment operator from Kotlin") + unique_binary_op(m, m.name[:-6]) # Remove 'Assign' suffix + + +def verify_collections_over_arrays(clazz): + """Warn that [] should be Collections.""" + + safe = ["java.lang.String[]","byte[]","short[]","int[]","long[]","float[]","double[]","boolean[]","char[]"] + for m in clazz.methods: + if m.typ.endswith("[]") and m.typ not in safe: + warn(clazz, m, None, "Method should return Collection<> (or subclass) instead of raw array") + for arg in m.args: + if arg.endswith("[]") and arg not in safe: + warn(clazz, m, None, "Method argument should be Collection<> (or subclass) instead of raw array") + + def examine_clazz(clazz): """Find all style issues in the given class.""" + + notice(clazz) + if clazz.pkg.name.startswith("java"): return if clazz.pkg.name.startswith("junit"): return if clazz.pkg.name.startswith("org.apache"): return @@ -1166,7 +1275,7 @@ def examine_clazz(clazz): verify_manager(clazz) verify_boxed(clazz) verify_static_utils(clazz) - verify_overload_args(clazz) + # verify_overload_args(clazz) verify_callback_handlers(clazz) verify_context_first(clazz) verify_listener_last(clazz) @@ -1178,14 +1287,18 @@ def examine_clazz(clazz): verify_error(clazz) verify_units(clazz) verify_closable(clazz) + verify_member_name_not_kotlin_keyword(clazz) + verify_method_name_not_kotlin_operator(clazz) + verify_collections_over_arrays(clazz) def examine_stream(stream): """Find all style issues in the given API stream.""" - global failures + global failures, noticed failures = {} + noticed = {} _parse_stream(stream, examine_clazz) - return failures + return (failures, noticed) def examine_api(api): @@ -1262,6 +1375,8 @@ if __name__ == "__main__": help="Disable terminal colors") parser.add_argument("--allow-google", action='store_const', const=True, help="Allow references to Google") + parser.add_argument("--show-noticed", action='store_const', const=True, + help="Show API changes noticed") args = vars(parser.parse_args()) if args['no_color']: @@ -1274,16 +1389,21 @@ if __name__ == "__main__": previous_file = args['previous.txt'] with current_file as f: - cur_fail = examine_stream(f) + cur_fail, cur_noticed = examine_stream(f) if not previous_file is None: with previous_file as f: - prev_fail = examine_stream(f) + prev_fail, prev_noticed = examine_stream(f) # ignore errors from previous API level for p in prev_fail: if p in cur_fail: del cur_fail[p] + # ignore classes unchanged from previous API level + for k, v in prev_noticed.iteritems(): + if k in cur_noticed and v == cur_noticed[k]: + del cur_noticed[k] + """ # NOTE: disabled because of memory pressure # look for compatibility issues @@ -1295,7 +1415,15 @@ if __name__ == "__main__": print """ - print "%s API style issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True))) - for f in sorted(cur_fail): - print cur_fail[f] + if args['show_noticed'] and len(cur_noticed) != 0: + print "%s API changes noticed %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True))) + for f in sorted(cur_noticed.keys()): + print f print + + if len(cur_fail) != 0: + print "%s API style issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True))) + for f in sorted(cur_fail): + print cur_fail[f] + print + sys.exit(77) |