[fidl][analysis] A script to look for types that are used once.

This looks for structs and unions that are never used or only used once,
inside another type.

This also includes a Python library that's useful for writing these
analyses.

TEST=ran the script

Change-Id: Ic3433164db1f061202a376ecd3821c09e80c37cc
diff --git a/public/lib/fidl/tools/analysis/README.md b/public/lib/fidl/tools/analysis/README.md
new file mode 100644
index 0000000..2e2cd95
--- /dev/null
+++ b/public/lib/fidl/tools/analysis/README.md
@@ -0,0 +1,8 @@
+# Scripts
+
+These are scripts that are useful for analyzing the corpus of FIDL libraries in
+the Fuchsia tree. They operate on the `.fidl.json` IR in the out directory. They
+should be run with `fx exec SCRIPTNAME` so that they can find the out directory.
+
+The library `ir.py` finds and parses the IR files and makes them available in a
+somewhat pythonic interface.
\ No newline at end of file
diff --git a/public/lib/fidl/tools/analysis/ir.py b/public/lib/fidl/tools/analysis/ir.py
new file mode 100644
index 0000000..3de896d
--- /dev/null
+++ b/public/lib/fidl/tools/analysis/ir.py
@@ -0,0 +1,404 @@
+import json
+import os
+import sys
+import typing as t
+
+
+class DeclState(object):
+    def __init__(self, indent: str = '', parents: t.Set[str] = frozenset()):
+        self.indent = indent
+        self.parents = parents
+
+    def nest(self, indent: str = '',
+             identifier: t.Optional[str] = None) -> 'DeclState':
+        indent = self.indent + indent
+        parents = self.parents
+        if identifier:
+            parents = parents.union({identifier})
+        return DeclState(indent, parents)
+
+
+class Declaration(dict):
+    def __init__(self, library: 'Library', value: dict):
+        dict.__init__(self, value)
+        self.library = library
+
+    @property
+    def name(self) -> str:
+        return self['name']
+
+    @property
+    def attributes(self):
+        return dict(
+            (a['name'], a['value']) for a in self.get('maybe_attributes', []))
+
+
+class Type(dict):
+    def __init__(self, library, value):
+        self.library = library
+        dict.__init__(self, value)
+
+    @property
+    def kind(self):
+        return self['kind']
+
+    def is_primitive(self) -> bool:
+        return self.kind == 'primitive'
+
+    def is_nullable(self) -> bool:
+        return self.get('nullable', False)
+
+    @property
+    def element_type(self):
+        if 'element_type' in self:
+            return Type(self.library, self['element_type'])
+
+    def decl(self, state) -> str:
+        if self.is_primitive():
+            return self['subtype']
+        nullable = ''
+        if self.is_nullable():
+            nullable = '?'
+        if self.kind == 'identifier':
+            if self['identifier'] in state.parents:
+                # a cycle
+                return self['identifier'] + nullable
+            value = self.library.libraries.find(self['identifier'])
+            if isinstance(value, Interface):
+                return self['identifier'] + nullable
+            else:
+                return value.decl(
+                    state.nest(identifier=self['identifier'])) + nullable
+        element_count = self.get('element_count',
+                                 self.get('maybe_element_count'))
+        size = ''
+        if element_count is not None:
+            size = ':%d' % element_count
+        if self.kind == 'string':
+            return 'string' + size + nullable
+        if self.kind in ('vector', 'array'):
+            return '%s<%s>%s%s' % (
+                self.kind,
+                Type(self.library, self['element_type']).decl(state), size,
+                nullable)
+        if self.kind == 'handle':
+            if self['subtype'] == 'handle':
+                return 'handle'
+            else:
+                return 'handle<%s>' % self['subtype']
+        if self.kind == 'request':
+            return 'request<%s>' % self['subtype']
+        raise Exception('unknown type %r' % self)
+
+
+class Const(Declaration):
+    @property
+    def type(self) -> Type:
+        return Type(self.library, self['type'])
+
+
+class EnumMember(Declaration):
+    def __init__(self, enum: 'Enum', value: dict):
+        Declaration.__init__(self, enum.library, value)
+        self.enum = enum
+
+    def decl(self, state: DeclState) -> str:
+        return '%s = %s' % (self.name, self['value']['literal']['value'])
+
+
+class Enum(Declaration):
+    @property
+    def type(self) -> Type:
+        return Type(self.library, {
+            'kind': 'primitive',
+            'subtype': self['type']
+        })
+
+    @property
+    def members(self) -> t.List[EnumMember]:
+        return [EnumMember(self, m) for m in self['members']]
+
+    def decl(self, state: DeclState) -> str:
+        state = state.nest(identifier=self.name)
+        return ('enum %s : %s {' %
+                (self.name, self.type.decl(state))) + '\n  ' + state.indent + (
+                    ',\n  ' + state.indent).join(
+                        m.decl(state)
+                        for m in self.members) + '\n' + state.indent + '}'
+
+
+class Argument(Declaration):
+    def __init__(self, method: 'Method', value: dict):
+        Declaration.__init__(self, method.library, value)
+        self.method = method
+
+    @property
+    def type(self) -> Type:
+        return Type(self.library, self['type'])
+
+    def decl(self, state: DeclState) -> str:
+        return '%s %s' % (self.type.decl(state), self['name'])
+
+
+class Method(Declaration):
+    def __init__(self, interface: 'Interface', value: dict):
+        Declaration.__init__(self, interface.library, value)
+        self.interface = interface
+
+    @property
+    def name(self) -> str:
+        return '%s.%s' % (self.interface.name, self['name'])
+
+    def is_event(self) -> bool:
+        return 'maybe_request' not in self
+
+    def is_one_way(self) -> bool:
+        return 'maybe_response' not in self
+
+    def request(self) -> t.Optional[t.List[Argument]]:
+        if self['has_request']:
+            return [Argument(self, a) for a in self['maybe_request']]
+        else:
+            return None
+
+    def response(self) -> t.Optional[t.List[Argument]]:
+        if self['has_response']:
+            return [Argument(self, a) for a in self['maybe_response']]
+        else:
+            return None
+
+    def decl(self, state: DeclState) -> str:
+        if self.is_event():
+            preamble = '%d: -> %s(' % (self['ordinal'], self['name'])
+            state = state.nest(indent=' ' * len(preamble))
+            return preamble + ', '.join(
+                arg.decl(state) for arg in self.response()) + ');'
+        if self.is_one_way():
+            # TODO
+            return 'adsf'
+
+        preamble = '%d: %s(' % (self['ordinal'], self['name'])
+        state = state.nest(indent=' ' * len(preamble))
+
+        return (preamble + ', '.join(
+            arg.decl(state)
+            for arg in self.request()) + ')\n' + state.indent[:-4] + '-> (' +
+                ', '.join(arg.decl(state) for arg in self.response()) + ');')
+
+
+class Interface(Declaration):
+    @property
+    def methods(self) -> t.List[Method]:
+        return [Method(self, m) for m in self.get('methods', [])]
+
+
+class StructMember(Declaration):
+    def __init__(self, struct: 'Struct', value: dict):
+        Declaration.__init__(self, struct.library, value)
+        self.struct = struct
+
+    @property
+    def type(self) -> Type:
+        return Type(self.library, self['type'])
+
+    def decl(self, state: DeclState) -> str:
+        # TODO: defaults?
+        return self.type.decl(state) + ' ' + self.name
+
+
+class Struct(Declaration):
+    @property
+    def members(self) -> t.List[StructMember]:
+        return [StructMember(self, m) for m in self['members']]
+
+    def decl(self, state: DeclState) -> str:
+        sub_state = state.nest(indent='  ', identifier=self.name)
+        return ('struct %s {' % self.name) + '\n' + sub_state.indent + (
+            ';\n' + sub_state.indent).join(
+                m.decl(sub_state)
+                for m in self.members) + ';\n' + state.indent + '}'
+
+
+class TableMember(Declaration):
+    def __init__(self, table: 'Table', value: dict):
+        Declaration.__init__(self, table.library, value)
+        self.table = table
+
+    @property
+    def name(self) -> t.Optional[str]:
+        return self.get('name')
+
+    @property
+    def reserved(self) -> bool:
+        return self['reserved']
+
+    @property
+    def type(self) -> Type:
+        return Type(self.library, self['type'])
+
+    def decl(self, state: DeclState) -> str:
+        ordinal = '%d: ' % self['ordinal']
+        if self.reserved:
+            return ordinal + 'reserved'
+        else:
+            sub_state = state.nest(indent=' ' * len(ordinal))
+            return ordinal + self.type.decl(sub_state) + ' ' + self.name
+
+
+class Table(Declaration):
+    @property
+    def members(self) -> t.List[TableMember]:
+        return [TableMember(self, m) for m in self['members']]
+
+    def decl(self, state: DeclState) -> str:
+        sub_state = state.nest(indent='  ', identifier=self.name)
+        return ('table %s {' % self.name) + '\n' + sub_state.indent + (
+            ';\n' + sub_state.indent).join(
+                m.decl(sub_state)
+                for m in self.members) + ';\n' + state.indent + '}'
+
+
+class UnionMember(Declaration):
+    def __init__(self, union: 'Union', value):
+        Declaration.__init__(self, union.library, value)
+        self.union = union
+
+    @property
+    def type(self) -> Type:
+        return Type(self.library, self['type'])
+
+    def decl(self, state: DeclState) -> str:
+        return self.type.decl(state) + ' ' + self.name
+
+
+class Union(Declaration):
+    @property
+    def members(self) -> t.List[UnionMember]:
+        return [UnionMember(self, m) for m in self['members']]
+
+    def decl(self, state: DeclState) -> str:
+        sub_state = state.nest(indent='  ', identifier=self.name)
+        return 'union %s {\n' % self.name + sub_state.indent + (
+            ';\n' + sub_state.indent).join(
+                m.decl(sub_state)
+                for m in self.members) + ';\n' + state.indent + '}'
+
+
+DECLARATION_TYPES = {
+    'const': ('const_declarations', Const),
+    'enum': ('enum_declarations', Enum),
+    'interface': ('interface_declarations', Interface),
+    'struct': ('struct_declarations', Struct),
+    'table': ('table_declarations', Table),
+    'union': ('union_declarations', Union),
+}
+
+
+class Library(dict):
+    def __init__(self, libraries: 'FidlLibraries', path: str):
+        self.libraries = libraries
+        self._path = path
+        dict.__init__(self, json.load(open(path)))
+        self.name = self['name']
+
+    def __repr__(self):
+        return 'Library<%s>' % self.name
+
+    @property
+    def consts(self) -> t.List[Const]:
+        return [Const(self, value) for value in self['const_declarations']]
+
+    @property
+    def enums(self) -> t.List[Enum]:
+        return [Enum(self, value) for value in self['enum_declarations']]
+
+    @property
+    def interfaces(self) -> t.List[Interface]:
+        return [Interface(self, v) for v in self['interface_declarations']]
+
+    @property
+    def structs(self) -> t.List[Struct]:
+        return [Struct(self, value) for value in self['struct_declarations']]
+
+    @property
+    def tables(self) -> t.List[Table]:
+        return [Table(self, value) for value in self['table_declarations']]
+
+    @property
+    def unions(self) -> t.List[Union]:
+        return [Union(self, value) for value in self['union_declarations']]
+
+    @property
+    def methods(self) -> t.List[Method]:
+        return [
+            method for interface in self.interfaces
+            for method in interface.methods
+        ]
+
+    def find(self, identifier: str) -> t.Union[None, Const, Enum, Interface, Struct, Table, Union]:
+        if identifier not in self['declarations']:
+          return None
+        declaration_type = self['declarations'][identifier]
+        declarations_key, constructor = DECLARATION_TYPES[declaration_type]
+        return self._lookup_declaration(identifier, declarations_key,
+                                        constructor)
+
+    def _lookup_declaration(self, identifier, declarations_key, constructor):
+        value = next(
+            s for s in self[declarations_key] if s['name'] == identifier)
+        return constructor(self, value)
+
+
+class Libraries(list):
+    def __init__(self):
+        build_dir = os.environ.get('FUCHSIA_BUILD_DIR')
+        if build_dir is None:
+            print('FUCHSIA_BUILD_DIR is not set.')
+            print('Run: fx exec %s' % ' '.join(sys.argv))
+            sys.exit(1)
+
+        # find all the .fidl.json files
+        ENDING = '.fidl.json'
+        for root, _, files in os.walk(build_dir):
+            self.extend(
+                Library(self, os.path.join(root, f)) for f in files
+                if f.endswith(ENDING))
+        self.by_name = dict((l.name, l) for l in self)
+
+    @property
+    def consts(self) -> t.List[Const]:
+        return [const for library in self for const in library.consts]
+
+    @property
+    def enums(self) -> t.List[Enum]:
+        return [enum for library in self for enum in library.enums]
+
+    @property
+    def interfaces(self) -> t.List[Interface]:
+        return [
+            interface for library in self for interface in library.interfaces
+        ]
+
+    @property
+    def structs(self) -> t.List[Struct]:
+        return [struct for library in self for struct in library.structs]
+
+    @property
+    def tables(self) -> t.List[Table]:
+        return [table for library in self for table in library.tables]
+
+    @property
+    def unions(self) -> t.List[Union]:
+        return [union for library in self for union in library.unions]
+
+    @property
+    def methods(self) -> t.List[Method]:
+        return [method for library in self for method in library.methods]
+
+    def find(self, identifier: str
+             ) -> t.Union[None, Const, Enum, Interface, Struct, Table, Union]:
+        library_name, _ = identifier.split('/')
+        if library_name not in self.by_name:
+          return None
+        library = self.by_name[library_name]
+        return library.find(identifier)
diff --git a/public/lib/fidl/tools/analysis/single_use.py b/public/lib/fidl/tools/analysis/single_use.py
new file mode 100755
index 0000000..c43ff16
--- /dev/null
+++ b/public/lib/fidl/tools/analysis/single_use.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+
+'''
+Looks for types that are never used or are used only once, as a field within
+another type.
+'''
+
+import sys
+from collections import defaultdict
+
+from ir import Libraries, Enum, Struct, DeclState
+
+fidl_libraries = Libraries()
+
+# count uses of named types
+type_uses_in_type = defaultdict(lambda: 0)
+type_uses_in_argument = defaultdict(lambda: 0)
+
+def count_type(t, d):
+    if t.kind == 'identifier':
+        d[t['identifier']] += 1
+    elif t.kind in ('array', 'vector'):
+        count_type(t.element_type, d)
+    elif t.kind == 'request':
+        d[t['subtype']] += 1
+
+for struct in fidl_libraries.structs:
+    for member in struct.members:
+        count_type(member.type, type_uses_in_type)
+
+for union in fidl_libraries.unions:
+    for member in union.members:
+        count_type(member.type, type_uses_in_argument)
+
+# TODO: tables
+
+for method in fidl_libraries.methods:
+    for arg in (method.request() or []) + (method.response() or []):
+        count_type(arg.type, type_uses_in_argument)
+
+once = []
+never = []
+
+# Look for structs that are only used once
+for struct in fidl_libraries.structs:
+    if type_uses_in_argument[struct.name] == 0:
+        if type_uses_in_type[struct.name] == 0:
+            never.append(struct.name)
+        if type_uses_in_type[struct.name] == 1:
+            once.append(struct.name)
+
+# Look for unions that are only used once
+for union in fidl_libraries.unions:
+    if type_uses_in_argument[union.name] == 0:
+        if type_uses_in_type[union.name] == 0:
+            never.append(union.name)
+        if type_uses_in_type[union.name] == 1:
+            once.append(union.name)
+
+once.sort()
+never.sort()
+
+print('Never:')
+for t in never:
+    print('  ' + t)
+
+print('\nOnce:')
+for t in once:
+    print('  ' + t)
\ No newline at end of file