Add script to parse VehicleProperty.aidl to CSV.

Allow parsing the VHAL property definitions/annotations in
VehicleProperty.aidl into a CSV file so that we can generate
documentation.

Test: python generate_annotation_enums.py --output_csv /tmp/prop.csv
Bug: 288331747
Change-Id: Icc5d023c8efdc01ead220313d6a7c66d22c63a0a
diff --git a/automotive/vehicle/tools/generate_annotation_enums.py b/automotive/vehicle/tools/generate_annotation_enums.py
index 7276fe6..c432e9d 100755
--- a/automotive/vehicle/tools/generate_annotation_enums.py
+++ b/automotive/vehicle/tools/generate_annotation_enums.py
@@ -49,6 +49,8 @@
 RE_COMMENT_END = re.compile('\s*\*\/')
 RE_CHANGE_MODE = re.compile('\s*\* @change_mode (\S+)\s*')
 RE_ACCESS = re.compile('\s*\* @access (\S+)\s*')
+RE_DATA_ENUM = re.compile('\s*\* @data_enum (\S+)\s*')
+RE_UNIT = re.compile('\s*\* @unit (\S+)\s+')
 RE_VALUE = re.compile('\s*(\w+)\s*=(.*)')
 
 LICENSE = """/*
@@ -166,55 +168,121 @@
 """
 
 
-class Converter:
+class PropertyConfig:
+    """Represents one VHAL property definition in VehicleProperty.aidl."""
 
-    def __init__(self, name, annotation_re):
-        self.name = name
-        self.annotation_re = annotation_re
+    def __init__(self):
+        self.name = None
+        self.description = None
+        self.change_mode = None
+        self.access_modes = []
+        self.enum_types = []
+        self.unit_type = None
 
-    def convert(self, input, output, header, footer, cpp):
+    def __repr__(self):
+        return self.__str__()
+
+    def __str__(self):
+        return ('PropertyConfig{{' +
+            'name: {}, description: {}, change_mode: {}, access_modes: {}, enum_types: {}' +
+            ', unit_type: {}}}').format(self.name, self.description, self.change_mode,
+                self.access_modes, self.enum_types, self.unit_type)
+
+
+class FileParser:
+
+    def __init__(self):
+        self.configs = None
+
+    def parseFile(self, input_file):
+        """Parses the input VehicleProperty.aidl file into a list of property configs."""
         processing = False
         in_comment = False
-        content = LICENSE + header
-        annotation = None
-        id = 0
-        with open(input, 'r') as f:
+        configs = []
+        config = None
+        with open(input_file, 'r') as f:
             for line in f.readlines():
                 if RE_ENUM_START.match(line):
                     processing = True
-                    annotation = None
                 elif RE_ENUM_END.match(line):
                     processing = False
                 if not processing:
                     continue
                 if RE_COMMENT_BEGIN.match(line):
                     in_comment = True
-                    annotation = None
+                    config = PropertyConfig()
+                    description = ''
                 if RE_COMMENT_END.match(line):
                     in_comment = False
                 if in_comment:
-                    match = self.annotation_re.match(line)
-                    if match and not annotation:
-                        annotation = match.group(1)
+                    if not config.description:
+                        sline = line.strip()
+                        # Skip the first line of comment
+                        if sline.startswith('*'):
+                            # Remove the '*'.
+                            sline = sline[1:].strip()
+                            # We reach an empty line of comment, the description part is ending.
+                            if sline == '':
+                                config.description = description
+                            else:
+                                if description != '':
+                                    description += ' '
+                                description += sline
+                    match = RE_CHANGE_MODE.match(line)
+                    if match:
+                        config.change_mode = match.group(1).replace('VehiclePropertyChangeMode.', '')
+                    match = RE_ACCESS.match(line)
+                    if match:
+                        config.access_modes.append(match.group(1).replace('VehiclePropertyAccess.', ''))
+                    match = RE_UNIT.match(line)
+                    if match:
+                        config.unit_type = match.group(1)
+                    match = RE_DATA_ENUM.match(line)
+                    if match:
+                        config.enum_types.append(match.group(1))
                 else:
                     match = RE_VALUE.match(line)
                     if match:
                         prop_name = match.group(1)
                         if prop_name == 'INVALID':
                             continue
-                        if not annotation:
+                        if not config.change_mode:
                             raise Exception(
-                                    'No @' + self.name + ' annotation for property: ' + prop_name)
-                        if id != 0:
-                            content += '\n'
-                        if cpp:
-                            annotation = annotation.replace('.', '::')
-                            content += (TAB + TAB + '{VehicleProperty::' + prop_name + ', ' +
-                                        annotation + '},')
-                        else:
-                            content += (TAB + TAB + 'Map.entry(VehicleProperty.' + prop_name + ', ' +
-                                        annotation + '),')
-                        id += 1
+                                    'No change_mode annotation for property: ' + prop_name)
+                        if not config.access_modes:
+                            raise Exception(
+                                    'No access_mode annotation for property: ' + prop_name)
+                        config.name = prop_name
+                        configs.append(config)
+
+        self.configs = configs
+
+    def convert(self, output, header, footer, cpp, field):
+        """Converts the property config file to C++/Java output file."""
+        counter = 0
+        content = LICENSE + header
+        for config in self.configs:
+            if field == 'change_mode':
+                if cpp:
+                    annotation = "VehiclePropertyChangeMode::" + config.change_mode
+                else:
+                    annotation = "VehiclePropertyChangeMode." + config.change_mode
+            elif field == 'access_mode':
+                if cpp:
+                    annotation = "VehiclePropertyAccess::" + config.access_modes[0]
+                else:
+                    annotation = "VehiclePropertyAccess." + config.access_modes[0]
+            else:
+                raise Exception('Unknown field: ' + field)
+            if counter != 0:
+                content += '\n'
+            if cpp:
+                content += (TAB + TAB + '{VehicleProperty::' + config.name + ', ' +
+                            annotation + '},')
+            else:
+                content += (TAB + TAB + 'Map.entry(VehicleProperty.' + config.name + ', ' +
+                            annotation + '),')
+            counter += 1
 
         # Remove the additional ',' at the end for the Java file.
         if not cpp:
@@ -225,6 +293,30 @@
         with open(output, 'w') as f:
             f.write(content)
 
+    def outputAsCsv(self, output):
+        content = 'name,description,change mode,access mode,enum type,unit type\n'
+        for config in self.configs:
+            enum_types = None
+            if not config.enum_types:
+                enum_types = '/'
+            else:
+                enum_types = '/'.join(config.enum_types)
+            unit_type = config.unit_type
+            if not unit_type:
+                unit_type = '/'
+            access_modes = ''
+            content += '"{}","{}","{}","{}","{}","{}"\n'.format(
+                    config.name,
+                    # Need to escape quote as double quote.
+                    config.description.replace('"', '""'),
+                    config.change_mode,
+                    '/'.join(config.access_modes),
+                    enum_types,
+                    unit_type)
+
+        with open(output, 'w+') as f:
+            f.write(content)
+
 
 def createTempFile():
     f = tempfile.NamedTemporaryFile(delete=False);
@@ -239,6 +331,8 @@
     parser.add_argument('--preupload_files', nargs='+', required=False, help='modified files')
     parser.add_argument('--check_only', required=False, action='store_true',
             help='only check whether the generated files need update')
+    parser.add_argument('--output_csv', required=False,
+            help='Path to the parsing result in CSV style, useful for doc generation')
     args = parser.parse_args();
     android_top = None
     output_folder = None
@@ -258,6 +352,12 @@
             'at the android root')
 
     aidl_file = os.path.join(android_top, PROP_AIDL_FILE_PATH)
+    f = FileParser();
+    f.parseFile(aidl_file)
+
+    if args.output_csv:
+        f.outputAsCsv(args.output_csv)
+        return
 
     change_mode_cpp_file = os.path.join(android_top, CHANGE_MODE_CPP_FILE_PATH);
     access_cpp_file = os.path.join(android_top, ACCESS_CPP_FILE_PATH);
@@ -281,14 +381,12 @@
         temp_files.append(access_java_output)
 
     try:
-        c = Converter('change_mode', RE_CHANGE_MODE);
-        c.convert(aidl_file, change_mode_cpp_output, CHANGE_MODE_CPP_HEADER, CHANGE_MODE_CPP_FOOTER,
-                True)
-        c.convert(aidl_file, change_mode_java_output, CHANGE_MODE_JAVA_HEADER,
-                CHANGE_MODE_JAVA_FOOTER, False)
-        c = Converter('access', RE_ACCESS)
-        c.convert(aidl_file, access_cpp_output, ACCESS_CPP_HEADER, ACCESS_CPP_FOOTER, True)
-        c.convert(aidl_file, access_java_output, ACCESS_JAVA_HEADER, ACCESS_JAVA_FOOTER, False)
+        f.convert(change_mode_cpp_output, CHANGE_MODE_CPP_HEADER, CHANGE_MODE_CPP_FOOTER,
+                True, 'change_mode')
+        f.convert(change_mode_java_output, CHANGE_MODE_JAVA_HEADER,
+                CHANGE_MODE_JAVA_FOOTER, False, 'change_mode')
+        f.convert(access_cpp_output, ACCESS_CPP_HEADER, ACCESS_CPP_FOOTER, True, 'access_mode')
+        f.convert(access_java_output, ACCESS_JAVA_HEADER, ACCESS_JAVA_FOOTER, False, 'access_mode')
 
         if not args.check_only:
             return