| #!/usr/bin/env python3 |
| |
| # 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(" abstract ", " ") |
| 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 %s" % 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%s" % f) |
| with open(f) as s: |
| api = _parse_stream(s, api) |
| print("Parsed %d APIs" % len(api)) |
| 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() |