Anton Hansson | 65163dd | 2022-04-08 11:36:47 +0100 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
Jeff Sharkey | 13bc791 | 2021-05-25 19:26:00 -0600 | [diff] [blame] | 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 | """ |
| 18 | Usage: deprecated_at_birth.py path/to/next/ path/to/previous/ |
| 19 | Usage: deprecated_at_birth.py prebuilts/sdk/31/public/api/ prebuilts/sdk/30/public/api/ |
| 20 | """ |
| 21 | |
| 22 | import re, sys, os, collections, traceback, argparse |
| 23 | |
| 24 | |
| 25 | BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) |
| 26 | |
| 27 | def 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 | |
| 42 | def 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 ", " ") |
Yurii Zubrytskyi | bb01456 | 2022-04-04 15:00:58 -0700 | [diff] [blame] | 47 | raw = raw.replace(" abstract ", " ") |
Jeff Sharkey | 13bc791 | 2021-05-25 19:26:00 -0600 | [diff] [blame] | 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 | |
| 57 | class 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 | |
| 86 | class 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 | |
| 122 | class 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 | |
| 164 | class 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 | |
| 178 | def _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 | |
| 210 | def _parse_stream_path(path): |
| 211 | api = {} |
Anton Hansson | 65163dd | 2022-04-08 11:36:47 +0100 | [diff] [blame] | 212 | print("Parsing %s" % path) |
Jeff Sharkey | 13bc791 | 2021-05-25 19:26:00 -0600 | [diff] [blame] | 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 |
Anton Hansson | 65163dd | 2022-04-08 11:36:47 +0100 | [diff] [blame] | 218 | print("\t%s" % f) |
Jeff Sharkey | 13bc791 | 2021-05-25 19:26:00 -0600 | [diff] [blame] | 219 | with open(f) as s: |
| 220 | api = _parse_stream(s, api) |
Anton Hansson | 65163dd | 2022-04-08 11:36:47 +0100 | [diff] [blame] | 221 | print("Parsed %d APIs" % len(api)) |
| 222 | print() |
Jeff Sharkey | 13bc791 | 2021-05-25 19:26:00 -0600 | [diff] [blame] | 223 | return api |
| 224 | |
| 225 | |
| 226 | class 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 | |
| 258 | failures = {} |
| 259 | |
| 260 | def _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 | |
| 270 | def warn(clazz, detail, rule, msg): |
| 271 | _fail(clazz, detail, False, rule, msg) |
| 272 | |
| 273 | def error(clazz, detail, rule, msg): |
| 274 | _fail(clazz, detail, True, rule, msg) |
| 275 | |
| 276 | |
| 277 | if __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 | |
Anton Hansson | 65163dd | 2022-04-08 11:36:47 +0100 | [diff] [blame] | 310 | print("%s Deprecated at birth %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), |
| 311 | format(reset=True)))) |
Jeff Sharkey | 13bc791 | 2021-05-25 19:26:00 -0600 | [diff] [blame] | 312 | for f in sorted(failures): |
Anton Hansson | 65163dd | 2022-04-08 11:36:47 +0100 | [diff] [blame] | 313 | print(failures[f]) |
| 314 | print() |