1#!/usr/bin/env python 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(" final ", " ") 48 raw = re.sub("<.+?>", "", raw) 49 raw = re.sub("@[A-Za-z]+ ", "", raw) 50 raw = re.sub("@[A-Za-z]+\(.+?\) ", "", raw) 51 if " throws " in raw: 52 raw = raw[:raw.index(" throws ")] 53 return raw 54 55 56class Field(): 57 def __init__(self, clazz, line, raw, blame): 58 self.clazz = clazz 59 self.line = line 60 self.raw = raw.strip(" {;") 61 self.blame = blame 62 63 raw = raw.split() 64 self.split = list(raw) 65 66 raw = [ r for r in raw if not r.startswith("@") ] 67 for r in ["method", "field", "public", "protected", "static", "final", "abstract", "default", "volatile", "transient"]: 68 while r in raw: raw.remove(r) 69 70 self.typ = raw[0] 71 self.name = raw[1].strip(";") 72 if len(raw) >= 4 and raw[2] == "=": 73 self.value = raw[3].strip(';"') 74 else: 75 self.value = None 76 self.ident = ident(self.raw) 77 78 def __hash__(self): 79 return hash(self.raw) 80 81 def __repr__(self): 82 return self.raw 83 84 85class Method(): 86 def __init__(self, clazz, line, raw, blame): 87 self.clazz = clazz 88 self.line = line 89 self.raw = raw.strip(" {;") 90 self.blame = blame 91 92 # drop generics for now 93 raw = re.sub("<.+?>", "", raw) 94 95 raw = re.split("[\s(),;]+", raw) 96 for r in ["", ";"]: 97 while r in raw: raw.remove(r) 98 self.split = list(raw) 99 100 raw = [ r for r in raw if not r.startswith("@") ] 101 for r in ["method", "field", "public", "protected", "static", "final", "abstract", "default", "volatile", "transient"]: 102 while r in raw: raw.remove(r) 103 104 self.typ = raw[0] 105 self.name = raw[1] 106 self.args = [] 107 self.throws = [] 108 target = self.args 109 for r in raw[2:]: 110 if r == "throws": target = self.throws 111 else: target.append(r) 112 self.ident = ident(self.raw) 113 114 def __hash__(self): 115 return hash(self.raw) 116 117 def __repr__(self): 118 return self.raw 119 120 121class Class(): 122 def __init__(self, pkg, line, raw, blame): 123 self.pkg = pkg 124 self.line = line 125 self.raw = raw.strip(" {;") 126 self.blame = blame 127 self.ctors = [] 128 self.fields = [] 129 self.methods = [] 130 131 raw = raw.split() 132 self.split = list(raw) 133 if "class" in raw: 134 self.fullname = raw[raw.index("class")+1] 135 elif "enum" in raw: 136 self.fullname = raw[raw.index("enum")+1] 137 elif "interface" in raw: 138 self.fullname = raw[raw.index("interface")+1] 139 elif "@interface" in raw: 140 self.fullname = raw[raw.index("@interface")+1] 141 else: 142 raise ValueError("Funky class type %s" % (self.raw)) 143 144 if "extends" in raw: 145 self.extends = raw[raw.index("extends")+1] 146 self.extends_path = self.extends.split(".") 147 else: 148 self.extends = None 149 self.extends_path = [] 150 151 self.fullname = self.pkg.name + "." + self.fullname 152 self.fullname_path = self.fullname.split(".") 153 154 self.name = self.fullname[self.fullname.rindex(".")+1:] 155 156 def __hash__(self): 157 return hash((self.raw, tuple(self.ctors), tuple(self.fields), tuple(self.methods))) 158 159 def __repr__(self): 160 return self.raw 161 162 163class Package(): 164 def __init__(self, line, raw, blame): 165 self.line = line 166 self.raw = raw.strip(" {;") 167 self.blame = blame 168 169 raw = raw.split() 170 self.name = raw[raw.index("package")+1] 171 self.name_path = self.name.split(".") 172 173 def __repr__(self): 174 return self.raw 175 176 177def _parse_stream(f, api={}): 178 line = 0 179 pkg = None 180 clazz = None 181 blame = None 182 183 re_blame = re.compile("^([a-z0-9]{7,}) \(<([^>]+)>.+?\) (.+?)$") 184 for raw in f: 185 line += 1 186 raw = raw.rstrip() 187 match = re_blame.match(raw) 188 if match is not None: 189 blame = match.groups()[0:2] 190 raw = match.groups()[2] 191 else: 192 blame = None 193 194 if raw.startswith("package"): 195 pkg = Package(line, raw, blame) 196 elif raw.startswith(" ") and raw.endswith("{"): 197 clazz = Class(pkg, line, raw, blame) 198 api[clazz.fullname] = clazz 199 elif raw.startswith(" ctor"): 200 clazz.ctors.append(Method(clazz, line, raw, blame)) 201 elif raw.startswith(" method"): 202 clazz.methods.append(Method(clazz, line, raw, blame)) 203 elif raw.startswith(" field"): 204 clazz.fields.append(Field(clazz, line, raw, blame)) 205 206 return api 207 208 209def _parse_stream_path(path): 210 api = {} 211 print "Parsing", path 212 for f in os.listdir(path): 213 f = os.path.join(path, f) 214 if not os.path.isfile(f): continue 215 if not f.endswith(".txt"): continue 216 if f.endswith("removed.txt"): continue 217 print "\t", f 218 with open(f) as s: 219 api = _parse_stream(s, api) 220 print "Parsed", len(api), "APIs" 221 print 222 return api 223 224 225class Failure(): 226 def __init__(self, sig, clazz, detail, error, rule, msg): 227 self.sig = sig 228 self.error = error 229 self.rule = rule 230 self.msg = msg 231 232 if error: 233 self.head = "Error %s" % (rule) if rule else "Error" 234 dump = "%s%s:%s %s" % (format(fg=RED, bg=BLACK, bold=True), self.head, format(reset=True), msg) 235 else: 236 self.head = "Warning %s" % (rule) if rule else "Warning" 237 dump = "%s%s:%s %s" % (format(fg=YELLOW, bg=BLACK, bold=True), self.head, format(reset=True), msg) 238 239 self.line = clazz.line 240 blame = clazz.blame 241 if detail is not None: 242 dump += "\n in " + repr(detail) 243 self.line = detail.line 244 blame = detail.blame 245 dump += "\n in " + repr(clazz) 246 dump += "\n in " + repr(clazz.pkg) 247 dump += "\n at line " + repr(self.line) 248 if blame is not None: 249 dump += "\n last modified by %s in %s" % (blame[1], blame[0]) 250 251 self.dump = dump 252 253 def __repr__(self): 254 return self.dump 255 256 257failures = {} 258 259def _fail(clazz, detail, error, rule, msg): 260 """Records an API failure to be processed later.""" 261 global failures 262 263 sig = "%s-%s-%s" % (clazz.fullname, repr(detail), msg) 264 sig = sig.replace(" deprecated ", " ") 265 266 failures[sig] = Failure(sig, clazz, detail, error, rule, msg) 267 268 269def warn(clazz, detail, rule, msg): 270 _fail(clazz, detail, False, rule, msg) 271 272def error(clazz, detail, rule, msg): 273 _fail(clazz, detail, True, rule, msg) 274 275 276if __name__ == "__main__": 277 next_path = sys.argv[1] 278 prev_path = sys.argv[2] 279 280 next_api = _parse_stream_path(next_path) 281 prev_api = _parse_stream_path(prev_path) 282 283 # Remove all existing things so we're left with new 284 for prev_clazz in prev_api.values(): 285 if prev_clazz.fullname not in next_api: continue 286 cur_clazz = next_api[prev_clazz.fullname] 287 288 sigs = { i.ident: i for i in prev_clazz.ctors } 289 cur_clazz.ctors = [ i for i in cur_clazz.ctors if i.ident not in sigs ] 290 sigs = { i.ident: i for i in prev_clazz.methods } 291 cur_clazz.methods = [ i for i in cur_clazz.methods if i.ident not in sigs ] 292 sigs = { i.ident: i for i in prev_clazz.fields } 293 cur_clazz.fields = [ i for i in cur_clazz.fields if i.ident not in sigs ] 294 295 # Forget about class entirely when nothing new 296 if len(cur_clazz.ctors) == 0 and len(cur_clazz.methods) == 0 and len(cur_clazz.fields) == 0: 297 del next_api[prev_clazz.fullname] 298 299 for clazz in next_api.values(): 300 if "@Deprecated " in clazz.raw and not clazz.fullname in prev_api: 301 error(clazz, None, None, "Found API deprecation at birth") 302 303 if "@Deprecated " in clazz.raw: continue 304 305 for i in clazz.ctors + clazz.methods + clazz.fields: 306 if "@Deprecated " in i.raw: 307 error(clazz, i, None, "Found API deprecation at birth " + i.ident) 308 309 print "%s Deprecated at birth %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), 310 format(reset=True))) 311 for f in sorted(failures): 312 print failures[f] 313 print 314