315 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			315 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
| #!/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()
 |