Merge "Enable -Wambiguous-reversed-operator" into main
diff --git a/bin/soongdbg b/bin/soongdbg
index 132a0f0..bfdbbde 100755
--- a/bin/soongdbg
+++ b/bin/soongdbg
@@ -2,9 +2,12 @@
 
 import argparse
 import fnmatch
+import html
+import io
 import json
 import os
 import pathlib
+import subprocess
 import types
 import sys
 
@@ -27,6 +30,7 @@
                 dep = get_or_make_node(self.nodes, d.id, None)
                 node.deps.add(dep)
                 dep.rdeps.add(node)
+                node.dep_tags.setdefault(dep, list()).append(d)
 
     def find_paths(self, id1, id2):
         # Throws KeyError if one of the names isn't found
@@ -60,6 +64,7 @@
         self.module = module
         self.deps = set()
         self.rdeps = set()
+        self.dep_tags = {}
 
 
 PROVIDERS = [
@@ -68,21 +73,35 @@
 ]
 
 
-def format_node_label(node):
-    if not node.module:
-        return node.id
-    if node.module.debug:
-        module_debug = f"<tr><td>{node.module.debug}</td></tr>"
-    else:
-        module_debug = ""
+def format_dep_label(node, dep):
+    tags = node.dep_tags.get(dep)
+    labels = []
+    if tags:
+        labels = [tag.tag_type.split("/")[-1] for tag in tags]
+        labels = sorted(set(labels))
+    if labels:
+        result = "<<table border=\"0\" cellborder=\"0\" cellspacing=\"0\" cellpadding=\"0\">"
+        for label in labels:
+            result += f"<tr><td>{label}</td></tr>"
+        result += "</table>>"
+        return result
 
-    result = (f"<<table border=\"0\" cellborder=\"0\" cellspacing=\"0\" cellpadding=\"0\">"
-            + f"<tr><td><b>{node.module.name}</b></td></tr>"
-            + module_debug
-            + f"<tr><td>{node.module.type}</td></tr>")
-    for p in node.module.providers:
-        if p.type in PROVIDERS:
-            result += "<tr><td><font color=\"#666666\">" + format_provider(p) + "</font></td></tr>"
+
+def format_node_label(node, module_formatter):
+    result = "<<table border=\"0\" cellborder=\"0\" cellspacing=\"0\" cellpadding=\"0\">"
+
+    # node name
+    result += f"<tr><td><b>{node.module.name if node.module else node.id}</b></td></tr>"
+
+    if node.module:
+        # node_type
+        result += f"<tr><td>{node.module.type}</td></tr>"
+
+        # module_formatter will return a list of rows
+        for row in module_formatter(node.module):
+            row = html.escape(row)
+            result += f"<tr><td><font color=\"#666666\">{row}</font></td></tr>"
+
     result += "</table>>"
     return result
 
@@ -190,22 +209,87 @@
             yield m
 
 
-def print_nodes(nodes):
-    print("digraph {")
+def print_args(parser):
+    parser.add_argument("--label", action="append", metavar="JQ_FILTER",
+                        help="jq query for each module metadata")
+    parser.add_argument("--deptags", action="store_true",
+                        help="show dependency tags (makes the graph much more complex)")
+
+    group = parser.add_argument_group("output formats",
+                                      "If no format is provided, a dot file will be written to"
+                                      + " stdout.")
+    output = group.add_mutually_exclusive_group()
+    output.add_argument("--dot", type=str, metavar="FILENAME",
+                        help="Write the graph to this file as dot (graphviz format)")
+    output.add_argument("--svg", type=str, metavar="FILENAME",
+                        help="Write the graph to this file as svg")
+
+
+def print_nodes(args, nodes, module_formatter):
+    # Generate the graphviz
+    dep_tag_id = 0
+    dot = io.StringIO()
+    dot.write("digraph {\n")
+    dot.write("node [shape=box];")
+
     for node in nodes:
-        print(f"\"{node.id}\"[label={format_node_label(node)}];")
+        dot.write(f"\"{node.id}\" [label={format_node_label(node, module_formatter)}];\n")
         for dep in node.deps:
             if dep in nodes:
-                print(f"\"{node.id}\" -> \"{dep.id}\";")
-    print("}")
+                if args.deptags:
+                    dot.write(f"\"{node.id}\" -> \"__dep_tag_{dep_tag_id}\" [ arrowhead=none ];\n")
+                    dot.write(f"\"__dep_tag_{dep_tag_id}\" -> \"{dep.id}\";\n")
+                    dot.write(f"\"__dep_tag_{dep_tag_id}\""
+                                  + f"[label={format_dep_label(node, dep)} shape=ellipse"
+                                  + " color=\"#666666\" fontcolor=\"#666666\"];\n")
+                else:
+                    dot.write(f"\"{node.id}\" -> \"{dep.id}\";\n")
+                dep_tag_id += 1
+    dot.write("}\n")
+    text = dot.getvalue()
+
+    # Write it somewhere
+    if args.dot:
+        with open(args.dot, "w") as f:
+            f.write(text)
+    elif args.svg:
+        subprocess.run(["dot", "-Tsvg", "-o", args.svg],
+                              input=text, text=True, check=True)
+    else:
+        sys.stdout.write(text)
 
 
-def get_deps(nodes, root):
+def get_deps(nodes, root, maxdepth, reverse):
     if root in nodes:
         return
     nodes.add(root)
-    for dep in root.deps:
-        get_deps(nodes, dep)
+    if maxdepth != 0:
+        for dep in (root.rdeps if reverse else root.deps):
+            get_deps(nodes, dep, maxdepth-1, reverse)
+
+
+def new_module_formatter(args):
+    def module_formatter(module):
+        if not args.label:
+            return []
+        result = []
+        text = json.dumps(module, default=lambda o: o.__dict__)
+        for jq_filter in args.label:
+            proc = subprocess.run(["jq", jq_filter],
+                                  input=text, text=True, check=True, stdout=subprocess.PIPE)
+            if proc.stdout:
+                o = json.loads(proc.stdout)
+                if type(o) == list:
+                    for row in o:
+                        if row:
+                            result.append(row)
+                elif type(o) == dict:
+                    result.append(str(proc.stdout).strip())
+                else:
+                    if o:
+                        result.append(str(o).strip())
+        return result
+    return module_formatter
 
 
 class BetweenCommand:
@@ -213,11 +297,13 @@
 
     def args(self, parser):
         parser.add_argument("module", nargs=2,
-                            help="The two modules")
+                            help="the two modules")
+        print_args(parser)
 
     def run(self, args):
         graph = load_graph()
-        print_nodes(graph.find_paths(args.module[0], args.module[1]))
+        print_nodes(args, graph.find_paths(args.module[0], args.module[1]),
+                    new_module_formatter(args))
 
 
 class DepsCommand:
@@ -226,21 +312,26 @@
     def args(self, parser):
         parser.add_argument("module", nargs="+",
                             help="Module to print dependencies of")
+        parser.add_argument("--reverse", action="store_true",
+                            help="traverse reverse dependencies")
+        parser.add_argument("--depth", type=int, default=-1,
+                            help="max depth of dependencies (can keep the graph size reasonable)")
+        print_args(parser)
 
     def run(self, args):
         graph = load_graph()
         nodes = set()
         err = False
-        for id in sys.argv[3:]:
+        for id in args.module:
             root = graph.nodes.get(id)
             if not root:
                 sys.stderr.write(f"error: Can't find root: {id}\n")
                 err = True
                 continue
-            get_deps(nodes, root)
+            get_deps(nodes, root, args.depth, args.reverse)
         if err:
             sys.exit(1)
-        print_nodes(nodes)
+        print_nodes(args, nodes, new_module_formatter(args))
 
 
 class IdCommand:
@@ -254,6 +345,25 @@
             print(m.id)
 
 
+class JsonCommand:
+    help = "Print metadata about modules in json format"
+
+    def args(self, parser):
+        module_selection_args(parser)
+        parser.add_argument("--list", action="store_true",
+                            help="Print the results in a json list. If not set and multiple"
+                            + " modules are matched, the output won't be valid json.")
+
+    def run(self, args):
+        modules = load_and_filter_modules(args)
+        if args.list:
+            json.dump([m for m in modules], sys.stdout, indent=4, default=lambda o: o.__dict__)
+        else:
+            for m in modules:
+                json.dump(m, sys.stdout, indent=4, default=lambda o: o.__dict__)
+                print()
+
+
 class QueryCommand:
     help = "Query details about modules"
 
@@ -275,6 +385,7 @@
     "between": BetweenCommand(),
     "deps": DepsCommand(),
     "id": IdCommand(),
+    "json": JsonCommand(),
     "query": QueryCommand(),
 }