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