13bc791d51
Earlier this year the "apilint.py" script was removed from the build, since it's been replaced by Metalava. However, several features haven't been ported yet, so this change revives them to support the SDK finalization process. This change also updates the script to handle "Signature format: 2.0" files, and reads the API surface from a directory which contains several ".txt" files to support the new Mainline API structuring. Bug: 189224267 Test: manual Change-Id: Ifc4c24a7e159db6725897800d67947d1fd8b9880
314 lines
9.7 KiB
Python
314 lines
9.7 KiB
Python
#!/usr/bin/env python
|
|
|
|
# Copyright (C) 2021 The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the 'License');
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an 'AS IS' BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""
|
|
Usage: deprecated_at_birth.py path/to/next/ path/to/previous/
|
|
Usage: deprecated_at_birth.py prebuilts/sdk/31/public/api/ prebuilts/sdk/30/public/api/
|
|
"""
|
|
|
|
import re, sys, os, collections, traceback, argparse
|
|
|
|
|
|
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
|
|
|
|
def format(fg=None, bg=None, bright=False, bold=False, dim=False, reset=False):
|
|
# manually derived from http://en.wikipedia.org/wiki/ANSI_escape_code#Codes
|
|
codes = []
|
|
if reset: codes.append("0")
|
|
else:
|
|
if not fg is None: codes.append("3%d" % (fg))
|
|
if not bg is None:
|
|
if not bright: codes.append("4%d" % (bg))
|
|
else: codes.append("10%d" % (bg))
|
|
if bold: codes.append("1")
|
|
elif dim: codes.append("2")
|
|
else: codes.append("22")
|
|
return "\033[%sm" % (";".join(codes))
|
|
|
|
|
|
def ident(raw):
|
|
"""Strips superficial signature changes, giving us a strong key that
|
|
can be used to identify members across API levels."""
|
|
raw = raw.replace(" deprecated ", " ")
|
|
raw = raw.replace(" synchronized ", " ")
|
|
raw = raw.replace(" final ", " ")
|
|
raw = re.sub("<.+?>", "", raw)
|
|
raw = re.sub("@[A-Za-z]+ ", "", raw)
|
|
raw = re.sub("@[A-Za-z]+\(.+?\) ", "", raw)
|
|
if " throws " in raw:
|
|
raw = raw[:raw.index(" throws ")]
|
|
return raw
|
|
|
|
|
|
class Field():
|
|
def __init__(self, clazz, line, raw, blame):
|
|
self.clazz = clazz
|
|
self.line = line
|
|
self.raw = raw.strip(" {;")
|
|
self.blame = blame
|
|
|
|
raw = raw.split()
|
|
self.split = list(raw)
|
|
|
|
raw = [ r for r in raw if not r.startswith("@") ]
|
|
for r in ["method", "field", "public", "protected", "static", "final", "abstract", "default", "volatile", "transient"]:
|
|
while r in raw: raw.remove(r)
|
|
|
|
self.typ = raw[0]
|
|
self.name = raw[1].strip(";")
|
|
if len(raw) >= 4 and raw[2] == "=":
|
|
self.value = raw[3].strip(';"')
|
|
else:
|
|
self.value = None
|
|
self.ident = ident(self.raw)
|
|
|
|
def __hash__(self):
|
|
return hash(self.raw)
|
|
|
|
def __repr__(self):
|
|
return self.raw
|
|
|
|
|
|
class Method():
|
|
def __init__(self, clazz, line, raw, blame):
|
|
self.clazz = clazz
|
|
self.line = line
|
|
self.raw = raw.strip(" {;")
|
|
self.blame = blame
|
|
|
|
# drop generics for now
|
|
raw = re.sub("<.+?>", "", raw)
|
|
|
|
raw = re.split("[\s(),;]+", raw)
|
|
for r in ["", ";"]:
|
|
while r in raw: raw.remove(r)
|
|
self.split = list(raw)
|
|
|
|
raw = [ r for r in raw if not r.startswith("@") ]
|
|
for r in ["method", "field", "public", "protected", "static", "final", "abstract", "default", "volatile", "transient"]:
|
|
while r in raw: raw.remove(r)
|
|
|
|
self.typ = raw[0]
|
|
self.name = raw[1]
|
|
self.args = []
|
|
self.throws = []
|
|
target = self.args
|
|
for r in raw[2:]:
|
|
if r == "throws": target = self.throws
|
|
else: target.append(r)
|
|
self.ident = ident(self.raw)
|
|
|
|
def __hash__(self):
|
|
return hash(self.raw)
|
|
|
|
def __repr__(self):
|
|
return self.raw
|
|
|
|
|
|
class Class():
|
|
def __init__(self, pkg, line, raw, blame):
|
|
self.pkg = pkg
|
|
self.line = line
|
|
self.raw = raw.strip(" {;")
|
|
self.blame = blame
|
|
self.ctors = []
|
|
self.fields = []
|
|
self.methods = []
|
|
|
|
raw = raw.split()
|
|
self.split = list(raw)
|
|
if "class" in raw:
|
|
self.fullname = raw[raw.index("class")+1]
|
|
elif "enum" in raw:
|
|
self.fullname = raw[raw.index("enum")+1]
|
|
elif "interface" in raw:
|
|
self.fullname = raw[raw.index("interface")+1]
|
|
elif "@interface" in raw:
|
|
self.fullname = raw[raw.index("@interface")+1]
|
|
else:
|
|
raise ValueError("Funky class type %s" % (self.raw))
|
|
|
|
if "extends" in raw:
|
|
self.extends = raw[raw.index("extends")+1]
|
|
self.extends_path = self.extends.split(".")
|
|
else:
|
|
self.extends = None
|
|
self.extends_path = []
|
|
|
|
self.fullname = self.pkg.name + "." + self.fullname
|
|
self.fullname_path = self.fullname.split(".")
|
|
|
|
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
|
|
|
|
|
|
class Package():
|
|
def __init__(self, line, raw, blame):
|
|
self.line = line
|
|
self.raw = raw.strip(" {;")
|
|
self.blame = blame
|
|
|
|
raw = raw.split()
|
|
self.name = raw[raw.index("package")+1]
|
|
self.name_path = self.name.split(".")
|
|
|
|
def __repr__(self):
|
|
return self.raw
|
|
|
|
|
|
def _parse_stream(f, api={}):
|
|
line = 0
|
|
pkg = None
|
|
clazz = None
|
|
blame = None
|
|
|
|
re_blame = re.compile("^([a-z0-9]{7,}) \(<([^>]+)>.+?\) (.+?)$")
|
|
for raw in f:
|
|
line += 1
|
|
raw = raw.rstrip()
|
|
match = re_blame.match(raw)
|
|
if match is not None:
|
|
blame = match.groups()[0:2]
|
|
raw = match.groups()[2]
|
|
else:
|
|
blame = None
|
|
|
|
if raw.startswith("package"):
|
|
pkg = Package(line, raw, blame)
|
|
elif raw.startswith(" ") and raw.endswith("{"):
|
|
clazz = Class(pkg, line, raw, blame)
|
|
api[clazz.fullname] = clazz
|
|
elif raw.startswith(" ctor"):
|
|
clazz.ctors.append(Method(clazz, line, raw, blame))
|
|
elif raw.startswith(" method"):
|
|
clazz.methods.append(Method(clazz, line, raw, blame))
|
|
elif raw.startswith(" field"):
|
|
clazz.fields.append(Field(clazz, line, raw, blame))
|
|
|
|
return api
|
|
|
|
|
|
def _parse_stream_path(path):
|
|
api = {}
|
|
print "Parsing", path
|
|
for f in os.listdir(path):
|
|
f = os.path.join(path, f)
|
|
if not os.path.isfile(f): continue
|
|
if not f.endswith(".txt"): continue
|
|
if f.endswith("removed.txt"): continue
|
|
print "\t", f
|
|
with open(f) as s:
|
|
api = _parse_stream(s, api)
|
|
print "Parsed", len(api), "APIs"
|
|
print
|
|
return api
|
|
|
|
|
|
class Failure():
|
|
def __init__(self, sig, clazz, detail, error, rule, msg):
|
|
self.sig = sig
|
|
self.error = error
|
|
self.rule = rule
|
|
self.msg = msg
|
|
|
|
if error:
|
|
self.head = "Error %s" % (rule) if rule else "Error"
|
|
dump = "%s%s:%s %s" % (format(fg=RED, bg=BLACK, bold=True), self.head, format(reset=True), msg)
|
|
else:
|
|
self.head = "Warning %s" % (rule) if rule else "Warning"
|
|
dump = "%s%s:%s %s" % (format(fg=YELLOW, bg=BLACK, bold=True), self.head, format(reset=True), msg)
|
|
|
|
self.line = clazz.line
|
|
blame = clazz.blame
|
|
if detail is not None:
|
|
dump += "\n in " + repr(detail)
|
|
self.line = detail.line
|
|
blame = detail.blame
|
|
dump += "\n in " + repr(clazz)
|
|
dump += "\n in " + repr(clazz.pkg)
|
|
dump += "\n at line " + repr(self.line)
|
|
if blame is not None:
|
|
dump += "\n last modified by %s in %s" % (blame[1], blame[0])
|
|
|
|
self.dump = dump
|
|
|
|
def __repr__(self):
|
|
return self.dump
|
|
|
|
|
|
failures = {}
|
|
|
|
def _fail(clazz, detail, error, rule, msg):
|
|
"""Records an API failure to be processed later."""
|
|
global failures
|
|
|
|
sig = "%s-%s-%s" % (clazz.fullname, repr(detail), msg)
|
|
sig = sig.replace(" deprecated ", " ")
|
|
|
|
failures[sig] = Failure(sig, clazz, detail, error, rule, msg)
|
|
|
|
|
|
def warn(clazz, detail, rule, msg):
|
|
_fail(clazz, detail, False, rule, msg)
|
|
|
|
def error(clazz, detail, rule, msg):
|
|
_fail(clazz, detail, True, rule, msg)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
next_path = sys.argv[1]
|
|
prev_path = sys.argv[2]
|
|
|
|
next_api = _parse_stream_path(next_path)
|
|
prev_api = _parse_stream_path(prev_path)
|
|
|
|
# Remove all existing things so we're left with new
|
|
for prev_clazz in prev_api.values():
|
|
if prev_clazz.fullname not in next_api: continue
|
|
cur_clazz = next_api[prev_clazz.fullname]
|
|
|
|
sigs = { i.ident: i for i in prev_clazz.ctors }
|
|
cur_clazz.ctors = [ i for i in cur_clazz.ctors if i.ident not in sigs ]
|
|
sigs = { i.ident: i for i in prev_clazz.methods }
|
|
cur_clazz.methods = [ i for i in cur_clazz.methods if i.ident not in sigs ]
|
|
sigs = { i.ident: i for i in prev_clazz.fields }
|
|
cur_clazz.fields = [ i for i in cur_clazz.fields if i.ident not in sigs ]
|
|
|
|
# Forget about class entirely when nothing new
|
|
if len(cur_clazz.ctors) == 0 and len(cur_clazz.methods) == 0 and len(cur_clazz.fields) == 0:
|
|
del next_api[prev_clazz.fullname]
|
|
|
|
for clazz in next_api.values():
|
|
if "@Deprecated " in clazz.raw and not clazz.fullname in prev_api:
|
|
error(clazz, None, None, "Found API deprecation at birth")
|
|
|
|
if "@Deprecated " in clazz.raw: continue
|
|
|
|
for i in clazz.ctors + clazz.methods + clazz.fields:
|
|
if "@Deprecated " in i.raw:
|
|
error(clazz, i, None, "Found API deprecation at birth " + i.ident)
|
|
|
|
print "%s Deprecated at birth %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True),
|
|
format(reset=True)))
|
|
for f in sorted(failures):
|
|
print failures[f]
|
|
print
|