sourcedr: Add build dependency mapper

This commit adds `sourcedr-map` command which can map source file
review results to compiled binaries.

Test: ./sourcedr/functional_tests.py
Change-Id: I2ea69c982c8096be573174551075872722c13790
diff --git a/vndk/tools/source-deps-reviewer/setup.py b/vndk/tools/source-deps-reviewer/setup.py
index 0b6759e..fee747c 100755
--- a/vndk/tools/source-deps-reviewer/setup.py
+++ b/vndk/tools/source-deps-reviewer/setup.py
@@ -26,6 +26,7 @@
     entry_points={
         'console_scripts': [
             'sourcedr = sourcedr.server:main',
+            'sourcedr-map = sourcedr.map:main'
         ],
     }
 )
diff --git a/vndk/tools/source-deps-reviewer/sourcedr/functional_tests.py b/vndk/tools/source-deps-reviewer/sourcedr/functional_tests.py
index 630ced4..9305228 100755
--- a/vndk/tools/source-deps-reviewer/sourcedr/functional_tests.py
+++ b/vndk/tools/source-deps-reviewer/sourcedr/functional_tests.py
@@ -1,6 +1,11 @@
 #!/usr/bin/env python3
 
+from sourcedr import data_utils
 from sourcedr.data_utils import data_path, load_data, remove_data
+from sourcedr.map import (
+    load_build_dep_file_from_path, load_review_data,
+    link_build_dep_and_review_data,
+)
 from sourcedr.preprocess import CodeSearch
 from sourcedr.server import app, args
 
@@ -62,7 +67,7 @@
         self.assertEqual(codes, cdata[test_arg][1])
 
     def test_save_all(self):
-        label = os.path.abspath('sourcedr//test/dlopen/test.c')
+        label = os.path.abspath('sourcedr/test/dlopen/test.c')
         label += ':10:    handle = dlopen("libm.so.6", RTLD_LAZY);'
         test_arg = {
             'label': label,
@@ -74,5 +79,45 @@
         self.assertEqual(['this_is_a_test.so'],  cdata[test_arg['label']][0])
         self.assertEqual(['arr_0', 'arr_1'], cdata[test_arg['label']][1])
 
+
+class MapTest(unittest.TestCase):
+    def setUp(self):
+        # TODO: Remove this global variable hacks after refactoring process.
+        self.old_data_path = data_utils.data_path
+        data_utils.data_path = 'sourcedr/test/map/data.json'
+
+    def tearDown(self):
+        # TODO: Remove this global variable hacks after refactoring process.
+        data_utils.data_path = self.old_data_path
+
+    def test_load_build_dep_file(self):
+        dep = load_build_dep_file_from_path('sourcedr/test/map/build_dep.json')
+
+        self.assertIn('liba.so', dep)
+        self.assertIn('libb.so', dep)
+        self.assertIn('libc.so', dep)
+
+        self.assertSetEqual({'a.h', 'a1.c', 'a1.o', 'a2.c', 'a2.o'}, dep['liba.so'])
+        self.assertSetEqual({'a.h', 'b.c', 'b.o'}, dep['libb.so'])
+        self.assertSetEqual(set(), dep['libc.so'])
+
+    def test_load_review_data(self):
+        data = load_review_data()
+        self.assertIn('a.h', data)
+        self.assertEqual(['libx.so'], data['a.h'])
+
+    def test_link_build_dep_and_review_data(self):
+        dep = load_build_dep_file_from_path('sourcedr/test/map/build_dep.json')
+        data = load_review_data()
+        result = link_build_dep_and_review_data(dep, data)
+
+        self.assertIn('liba.so', result)
+        self.assertIn('libb.so', result)
+        self.assertIn('libc.so', result)
+
+        self.assertEqual(['libx.so'], result['liba.so'])
+        self.assertEqual(['libx.so'], result['libb.so'])
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/vndk/tools/source-deps-reviewer/sourcedr/map.py b/vndk/tools/source-deps-reviewer/sourcedr/map.py
new file mode 100755
index 0000000..bd8da05
--- /dev/null
+++ b/vndk/tools/source-deps-reviewer/sourcedr/map.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+
+"""This command maps source file review results to compiled binaries.
+"""
+
+from sourcedr.data_utils import load_data
+
+import argparse
+import collections
+import json
+import os
+
+
+def load_build_dep_file(fp):
+    graph = json.load(fp)
+
+    # Collect all shared libraries
+    shared_libs = set()
+    for key, value in graph.items():
+        if key.split('.')[-1] == 'so':
+            shared_libs.add(key)
+        for v in value:
+            if v.split('.')[-1] == 'so':
+                shared_libs.add(v)
+
+    # Collect transitive closures
+    dep = {}
+    for s in shared_libs:
+        visited = set()
+        stack = [s]
+        while stack:
+            v = stack.pop()
+            if v not in visited:
+                visited.add(v)
+                try:
+                    stack.extend(x for x in graph[v]
+                                 if x not in visited and not x.endswith('.so')
+                                 and not x.endswith('.toc'))
+                except KeyError:
+                    pass
+        visited.remove(s)
+        dep[s] = visited
+
+    return dep
+
+
+def load_build_dep_file_from_path(path):
+    with open(path, 'r') as fp:
+        return load_build_dep_file(fp)
+
+
+def load_review_data():
+    table = collections.defaultdict(list)
+    data = load_data()
+    for key, item in data.items():
+        table[key.split(':')[0]] += item[0]
+    return table
+
+
+def link_build_dep_and_review_data(dep, table):
+    res = collections.defaultdict(list)
+    for out, ins in dep.items():
+        try:
+            res[out] += table[out]
+        except KeyError:
+            pass
+
+        for in_file in ins:
+            try:
+                res[out] += table[in_file]
+            except KeyError:
+                pass
+    return res
+
+
+def main():
+    # Parse arguments
+    parser = argparse.ArgumentParser()
+    parser.add_argument('input', help='Build dependency file')
+    parser.add_argument('-o', '--output', required=True)
+    args = parser.parse_args()
+
+    # Load build dependency file
+    try:
+        dep = load_build_dep_file_from_path(args.input)
+    except IOError:
+        print('error: Failed to open build dependency file:', args.input,
+              file=sys.stderr)
+        sys.exit(1)
+
+    # Load review data
+    table = load_review_data()
+
+    # Link build dependency file and review data
+    res = link_build_dep_and_review_data(dep, table)
+
+    # Write the output file
+    with open(args.output, 'w') as f:
+        json.dump(res, f, sort_keys=True, indent=4)
+
+if __name__ == '__main__':
+    main()
diff --git a/vndk/tools/source-deps-reviewer/sourcedr/test/map/build_dep.json b/vndk/tools/source-deps-reviewer/sourcedr/test/map/build_dep.json
new file mode 100644
index 0000000..d60fa89
--- /dev/null
+++ b/vndk/tools/source-deps-reviewer/sourcedr/test/map/build_dep.json
@@ -0,0 +1,8 @@
+{
+  "liba.so": ["libb.so", "libc.so", "a1.o", "a2.o"],
+  "libb.so": ["b.o"],
+  "libc.so": [],
+  "a1.o": ["a.h", "a1.c"],
+  "a2.o": ["a.h", "a2.c"],
+  "b.o": ["a.h", "b.c"]
+}
diff --git a/vndk/tools/source-deps-reviewer/sourcedr/test/map/data.json b/vndk/tools/source-deps-reviewer/sourcedr/test/map/data.json
new file mode 100644
index 0000000..0d1bfe2
--- /dev/null
+++ b/vndk/tools/source-deps-reviewer/sourcedr/test/map/data.json
@@ -0,0 +1,6 @@
+{
+  "a.h:2:dlopen(\"libx.so\",": [
+     ["libx.so"],
+     ["a.h:2:dlopen(\"libx.so\","]
+  ]
+}