API Lint: Add support for base current.txt

Allows specifying a base current.txt and previous.txt file when linting
system-current.txt and test-current.txt to avoid false positive error
messages due to public API members not being duplicated in the respective
non-public APIs

Test: python tools/apilint/apilint.py --base-current=api/current.txt api/system-current.txt
Change-Id: I306a99b1423584ef3fcdc9272a83cb5eacc37227
(cherry picked from commit 7690d0d4eea0ffa429351b0b1caa34cdb3e0d37f)
diff --git a/tools/apilint/apilint.py b/tools/apilint/apilint.py
index cb8fef9..b5a990e 100644
--- a/tools/apilint/apilint.py
+++ b/tools/apilint/apilint.py
@@ -183,6 +183,11 @@
 
         self.name = self.fullname[self.fullname.rindex(".")+1:]
 
+    def merge_from(self, other):
+        self.ctors.extend(other.ctors)
+        self.fields.extend(other.fields)
+        self.methods.extend(other.methods)
+
     def __hash__(self):
         return hash((self.raw, tuple(self.ctors), tuple(self.fields), tuple(self.methods)))
 
@@ -204,9 +209,28 @@
         return self.raw
 
 
-def _parse_stream(f, clazz_cb=None):
-    line = 0
+def _parse_stream(f, clazz_cb=None, base_f=None):
     api = {}
+
+    if base_f:
+        base_classes = _parse_stream_to_generator(base_f)
+    else:
+        base_classes = []
+
+    for clazz in _parse_stream_to_generator(f):
+        base_class = _parse_to_matching_class(base_classes, clazz)
+        if base_class:
+            clazz.merge_from(base_class)
+
+        if clazz_cb:
+            clazz_cb(clazz)
+        else: # In callback mode, don't keep track of the full API
+            api[clazz.fullname] = clazz
+
+    return api
+
+def _parse_stream_to_generator(f):
+    line = 0
     pkg = None
     clazz = None
     blame = None
@@ -225,26 +249,41 @@
         if raw.startswith("package"):
             pkg = Package(line, raw, blame)
         elif raw.startswith("  ") and raw.endswith("{"):
-            # When provided with class callback, we treat as incremental
-            # parse and don't build up entire API
-            if clazz and clazz_cb:
-                clazz_cb(clazz)
             clazz = Class(pkg, line, raw, blame)
-            if not clazz_cb:
-                api[clazz.fullname] = clazz
         elif raw.startswith("    ctor"):
             clazz.ctors.append(Method(clazz, line, raw, blame))
         elif raw.startswith("    method"):
             clazz.methods.append(Method(clazz, line, raw, blame))
         elif raw.startswith("    field"):
             clazz.fields.append(Field(clazz, line, raw, blame))
+        elif raw.startswith("  }") and clazz:
+            while True:
+                retry = yield clazz
+                if not retry:
+                    break
+                # send() was called, asking us to redeliver clazz on next(). Still need to yield
+                # a dummy value to the send() first though.
+                if (yield "Returning clazz on next()"):
+                        raise TypeError("send() must be followed by next(), not send()")
 
-    # Handle last trailing class
-    if clazz and clazz_cb:
-        clazz_cb(clazz)
 
-    return api
+def _parse_to_matching_class(classes, needle):
+    """Takes a classes generator and parses it until it returns the class we're looking for
 
+    This relies on classes being sorted by package and class name."""
+
+    for clazz in classes:
+        if clazz.pkg.name < needle.pkg.name:
+            # We haven't reached the right package yet
+            continue
+        if clazz.name < needle.name:
+            # We haven't reached the right class yet
+            continue
+        if clazz.fullname == needle.fullname:
+            return clazz
+        # We ran past the right class. Send it back into the generator, then report failure.
+        classes.send(clazz)
+        return None
 
 class Failure():
     def __init__(self, sig, clazz, detail, error, rule, msg):
@@ -1504,12 +1543,12 @@
     verify_singleton(clazz)
 
 
-def examine_stream(stream):
+def examine_stream(stream, base_stream=None):
     """Find all style issues in the given API stream."""
     global failures, noticed
     failures = {}
     noticed = {}
-    _parse_stream(stream, examine_clazz)
+    _parse_stream(stream, examine_clazz, base_f=base_stream)
     return (failures, noticed)
 
 
@@ -1650,6 +1689,12 @@
     parser.add_argument("current.txt", type=argparse.FileType('r'), help="current.txt")
     parser.add_argument("previous.txt", nargs='?', type=argparse.FileType('r'), default=None,
             help="previous.txt")
+    parser.add_argument("--base-current", nargs='?', type=argparse.FileType('r'), default=None,
+            help="The base current.txt to use when examining system-current.txt or"
+                 " test-current.txt")
+    parser.add_argument("--base-previous", nargs='?', type=argparse.FileType('r'), default=None,
+            help="The base previous.txt to use when examining system-previous.txt or"
+                 " test-previous.txt")
     parser.add_argument("--no-color", action='store_const', const=True,
             help="Disable terminal colors")
     parser.add_argument("--allow-google", action='store_const', const=True,
@@ -1669,7 +1714,9 @@
         ALLOW_GOOGLE = True
 
     current_file = args['current.txt']
+    base_current_file = args['base_current']
     previous_file = args['previous.txt']
+    base_previous_file = args['base_previous']
 
     if args['show_deprecations_at_birth']:
         with current_file as f:
@@ -1688,10 +1735,18 @@
         sys.exit()
 
     with current_file as f:
-        cur_fail, cur_noticed = examine_stream(f)
+        if base_current_file:
+            with base_current_file as base_f:
+                cur_fail, cur_noticed = examine_stream(f, base_f)
+        else:
+            cur_fail, cur_noticed = examine_stream(f)
     if not previous_file is None:
         with previous_file as f:
-            prev_fail, prev_noticed = examine_stream(f)
+            if base_previous_file:
+                with base_previous_file as base_f:
+                    prev_fail, prev_noticed = examine_stream(f, base_f)
+            else:
+                prev_fail, prev_noticed = examine_stream(f)
 
         # ignore errors from previous API level
         for p in prev_fail: