blob: d53c12734d23c3060556aa2e99d5490ed489fad2 [file] [log] [blame]
Anton Hansson65163dd2022-04-08 11:36:47 +01001#!/usr/bin/env python3
Jeff Sharkey13bc7912021-05-25 19:26:00 -06002
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 ", " ")
Yurii Zubrytskyibb014562022-04-04 15:00:58 -070047 raw = raw.replace(" abstract ", " ")
Jeff Sharkey13bc7912021-05-25 19:26:00 -060048 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 = {}
Anton Hansson65163dd2022-04-08 11:36:47 +0100212 print("Parsing %s" % path)
Jeff Sharkey13bc7912021-05-25 19:26:00 -0600213 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 Hansson65163dd2022-04-08 11:36:47 +0100218 print("\t%s" % f)
Jeff Sharkey13bc7912021-05-25 19:26:00 -0600219 with open(f) as s:
220 api = _parse_stream(s, api)
Anton Hansson65163dd2022-04-08 11:36:47 +0100221 print("Parsed %d APIs" % len(api))
222 print()
Jeff Sharkey13bc7912021-05-25 19:26:00 -0600223 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
Anton Hansson65163dd2022-04-08 11:36:47 +0100310 print("%s Deprecated at birth %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True),
311 format(reset=True))))
Jeff Sharkey13bc7912021-05-25 19:26:00 -0600312 for f in sorted(failures):
Anton Hansson65163dd2022-04-08 11:36:47 +0100313 print(failures[f])
314 print()