1#!/usr/bin/env python3 2 3# Copyright (C) 2021 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the 'License'); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an 'AS IS' BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17""" 18Usage: deprecated_at_birth.py path/to/next/ path/to/previous/ 19Usage: deprecated_at_birth.py prebuilts/sdk/31/public/api/ prebuilts/sdk/30/public/api/ 20""" 21 22import re, sys, os, collections, traceback, argparse 23 24 25BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) 26 27def format(fg=None, bg=None, bright=False, bold=False, dim=False, reset=False): 28 # manually derived from http://en.wikipedia.org/wiki/ANSI_escape_code#Codes 29 codes = [] 30 if reset: codes.append("0") 31 else: 32 if not fg is None: codes.append("3%d" % (fg)) 33 if not bg is None: 34 if not bright: codes.append("4%d" % (bg)) 35 else: codes.append("10%d" % (bg)) 36 if bold: codes.append("1") 37 elif dim: codes.append("2") 38 else: codes.append("22") 39 return "\033[%sm" % (";".join(codes)) 40 41 42def ident(raw): 43 """Strips superficial signature changes, giving us a strong key that 44 can be used to identify members across API levels.""" 45 raw = raw.replace(" deprecated ", " ") 46 raw = raw.replace(" synchronized ", " ") 47 raw = raw.replace(" abstract ", " ") 48 raw = raw.replace(" final ", " ") 49 raw = re.sub("<.+?>", "", raw) 50 raw = re.sub("@[A-Za-z]+ ", "", raw) 51 raw = re.sub("@[A-Za-z]+\(.+?\) ", "", raw) 52 if " throws " in raw: 53 raw = raw[:raw.index(" throws ")] 54 return raw 55 56 57class Field(): 58 def __init__(self, clazz, line, raw, blame): 59 self.clazz = clazz 60 self.line = line 61 self.raw = raw.strip(" {;") 62 self.blame = blame 63 64 raw = raw.split() 65 self.split = list(raw) 66 67 raw = [ r for r in raw if not r.startswith("@") ] 68 for r in ["method", "field", "public", "protected", "static", "final", "abstract", "default", "volatile", "transient"]: 69 while r in raw: raw.remove(r) 70 71 self.typ = raw[0] 72 self.name = raw[1].strip(";") 73 if len(raw) >= 4 and raw[2] == "=": 74 self.value = raw[3].strip(';"') 75 else: 76 self.value = None 77 self.ident = ident(self.raw) 78 79 def __hash__(self): 80 return hash(self.raw) 81 82 def __repr__(self): 83 return self.raw 84 85 86class Method(): 87 def __init__(self, clazz, line, raw, blame): 88 self.clazz = clazz 89 self.line = line 90 self.raw = raw.strip(" {;") 91 self.blame = blame 92 93 # drop generics for now 94 raw = re.sub("<.+?>", "", raw) 95 96 raw = re.split("[\s(),;]+", raw) 97 for r in ["", ";"]: 98 while r in raw: raw.remove(r) 99 self.split = list(raw) 100 101 raw = [ r for r in raw if not r.startswith("@") ] 102 for r in ["method", "field", "public", "protected", "static", "final", "abstract", "default", "volatile", "transient"]: 103 while r in raw: raw.remove(r) 104 105 self.typ = raw[0] 106 self.name = raw[1] 107 self.args = [] 108 self.throws = [] 109 target = self.args 110 for r in raw[2:]: 111 if r == "throws": target = self.throws 112 else: target.append(r) 113 self.ident = ident(self.raw) 114 115 def __hash__(self): 116 return hash(self.raw) 117 118 def __repr__(self): 119 return self.raw 120 121 122class Class(): 123 def __init__(self, pkg, line, raw, blame): 124 self.pkg = pkg 125 self.line = line 126 self.raw = raw.strip(" {;") 127 self.blame = blame 128 self.ctors = [] 129 self.fields = [] 130 self.methods = [] 131 132 raw = raw.split() 133 self.split = list(raw) 134 if "class" in raw: 135 self.fullname = raw[raw.index("class")+1] 136 elif "enum" in raw: 137 self.fullname = raw[raw.index("enum")+1] 138 elif "interface" in raw: 139 self.fullname = raw[raw.index("interface")+1] 140 elif "@interface" in raw: 141 self.fullname = raw[raw.index("@interface")+1] 142 else: 143 raise ValueError("Funky class type %s" % (self.raw)) 144 145 if "extends" in raw: 146 self.extends = raw[raw.index("extends")+1] 147 self.extends_path = self.extends.split(".") 148 else: 149 self.extends = None 150 self.extends_path = [] 151 152 self.fullname = self.pkg.name + "." + self.fullname 153 self.fullname_path = self.fullname.split(".") 154 155 self.name = self.fullname[self.fullname.rindex(".")+1:] 156 157 def __hash__(self): 158 return hash((self.raw, tuple(self.ctors), tuple(self.fields), tuple(self.methods))) 159 160 def __repr__(self): 161 return self.raw 162 163 164class Package(): 165 def __init__(self, line, raw, blame): 166 self.line = line 167 self.raw = raw.strip(" {;") 168 self.blame = blame 169 170 raw = raw.split() 171 self.name = raw[raw.index("package")+1] 172 self.name_path = self.name.split(".") 173 174 def __repr__(self): 175 return self.raw 176 177 178def _parse_stream(f, api={}): 179 line = 0 180 pkg = None 181 clazz = None 182 blame = None 183 184 re_blame = re.compile("^([a-z0-9]{7,}) \(<([^>]+)>.+?\) (.+?)$") 185 for raw in f: 186 line += 1 187 raw = raw.rstrip() 188 match = re_blame.match(raw) 189 if match is not None: 190 blame = match.groups()[0:2] 191 raw = match.groups()[2] 192 else: 193 blame = None 194 195 if raw.startswith("package"): 196 pkg = Package(line, raw, blame) 197 elif raw.startswith(" ") and raw.endswith("{"): 198 clazz = Class(pkg, line, raw, blame) 199 api[clazz.fullname] = clazz 200 elif raw.startswith(" ctor"): 201 clazz.ctors.append(Method(clazz, line, raw, blame)) 202 elif raw.startswith(" method"): 203 clazz.methods.append(Method(clazz, line, raw, blame)) 204 elif raw.startswith(" field"): 205 clazz.fields.append(Field(clazz, line, raw, blame)) 206 207 return api 208 209 210def _parse_stream_path(path): 211 api = {} 212 print("Parsing %s" % path) 213 for f in os.listdir(path): 214 f = os.path.join(path, f) 215 if not os.path.isfile(f): continue 216 if not f.endswith(".txt"): continue 217 if f.endswith("removed.txt"): continue 218 print("\t%s" % f) 219 with open(f) as s: 220 api = _parse_stream(s, api) 221 print("Parsed %d APIs" % len(api)) 222 print() 223 return api 224 225 226class Failure(): 227 def __init__(self, sig, clazz, detail, error, rule, msg): 228 self.sig = sig 229 self.error = error 230 self.rule = rule 231 self.msg = msg 232 233 if error: 234 self.head = "Error %s" % (rule) if rule else "Error" 235 dump = "%s%s:%s %s" % (format(fg=RED, bg=BLACK, bold=True), self.head, format(reset=True), msg) 236 else: 237 self.head = "Warning %s" % (rule) if rule else "Warning" 238 dump = "%s%s:%s %s" % (format(fg=YELLOW, bg=BLACK, bold=True), self.head, format(reset=True), msg) 239 240 self.line = clazz.line 241 blame = clazz.blame 242 if detail is not None: 243 dump += "\n in " + repr(detail) 244 self.line = detail.line 245 blame = detail.blame 246 dump += "\n in " + repr(clazz) 247 dump += "\n in " + repr(clazz.pkg) 248 dump += "\n at line " + repr(self.line) 249 if blame is not None: 250 dump += "\n last modified by %s in %s" % (blame[1], blame[0]) 251 252 self.dump = dump 253 254 def __repr__(self): 255 return self.dump 256 257 258failures = {} 259 260def _fail(clazz, detail, error, rule, msg): 261 """Records an API failure to be processed later.""" 262 global failures 263 264 sig = "%s-%s-%s" % (clazz.fullname, repr(detail), msg) 265 sig = sig.replace(" deprecated ", " ") 266 267 failures[sig] = Failure(sig, clazz, detail, error, rule, msg) 268 269 270def warn(clazz, detail, rule, msg): 271 _fail(clazz, detail, False, rule, msg) 272 273def error(clazz, detail, rule, msg): 274 _fail(clazz, detail, True, rule, msg) 275 276 277if __name__ == "__main__": 278 next_path = sys.argv[1] 279 prev_path = sys.argv[2] 280 281 next_api = _parse_stream_path(next_path) 282 prev_api = _parse_stream_path(prev_path) 283 284 # Remove all existing things so we're left with new 285 for prev_clazz in prev_api.values(): 286 if prev_clazz.fullname not in next_api: continue 287 cur_clazz = next_api[prev_clazz.fullname] 288 289 sigs = { i.ident: i for i in prev_clazz.ctors } 290 cur_clazz.ctors = [ i for i in cur_clazz.ctors if i.ident not in sigs ] 291 sigs = { i.ident: i for i in prev_clazz.methods } 292 cur_clazz.methods = [ i for i in cur_clazz.methods if i.ident not in sigs ] 293 sigs = { i.ident: i for i in prev_clazz.fields } 294 cur_clazz.fields = [ i for i in cur_clazz.fields if i.ident not in sigs ] 295 296 # Forget about class entirely when nothing new 297 if len(cur_clazz.ctors) == 0 and len(cur_clazz.methods) == 0 and len(cur_clazz.fields) == 0: 298 del next_api[prev_clazz.fullname] 299 300 for clazz in next_api.values(): 301 if "@Deprecated " in clazz.raw and not clazz.fullname in prev_api: 302 error(clazz, None, None, "Found API deprecation at birth") 303 304 if "@Deprecated " in clazz.raw: continue 305 306 for i in clazz.ctors + clazz.methods + clazz.fields: 307 if "@Deprecated " in i.raw: 308 error(clazz, i, None, "Found API deprecation at birth " + i.ident) 309 310 print("%s Deprecated at birth %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), 311 format(reset=True)))) 312 for f in sorted(failures): 313 print(failures[f]) 314 print() 315