You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
314 lines
9.7 KiB
314 lines
9.7 KiB
#!/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
|