1#!/usr/bin/env python
2#
3# Copyright 2014 The Chromium Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7import collections
8import re
9import optparse
10import os
11from string import Template
12import sys
13
14from util import build_utils
15
16class EnumDefinition(object):
17  def __init__(self, class_name=None, class_package=None, entries=None):
18    self.class_name = class_name
19    self.class_package = class_package
20    self.entries = collections.OrderedDict(entries or [])
21    self.prefix_to_strip = ''
22
23  def AppendEntry(self, key, value):
24    if key in self.entries:
25      raise Exception('Multiple definitions of key %s found.' % key)
26    self.entries[key] = value
27
28  def Finalize(self):
29    self._Validate()
30    self._AssignEntryIndices()
31    self._StripPrefix()
32
33  def _Validate(self):
34    assert self.class_name
35    assert self.class_package
36    assert self.entries
37
38  def _AssignEntryIndices(self):
39    # Supporting the same set enum value assignments the compiler does is rather
40    # complicated, so we limit ourselves to these cases:
41    # - all the enum constants have values assigned,
42    # - enum constants reference other enum constants or have no value assigned.
43
44    if not all(self.entries.values()):
45      index = 0
46      for key, value in self.entries.iteritems():
47        if not value:
48          self.entries[key] = index
49          index = index + 1
50        elif value in self.entries:
51          self.entries[key] = self.entries[value]
52        else:
53          raise Exception('You can only reference other enum constants unless '
54                          'you assign values to all of the constants.')
55
56  def _StripPrefix(self):
57    if not self.prefix_to_strip:
58      prefix_to_strip = re.sub('(?!^)([A-Z]+)', r'_\1', self.class_name).upper()
59      prefix_to_strip += '_'
60      if not all([w.startswith(prefix_to_strip) for w in self.entries.keys()]):
61        prefix_to_strip = ''
62    else:
63      prefix_to_strip = self.prefix_to_strip
64    entries = ((k.replace(prefix_to_strip, '', 1), v) for (k, v) in
65               self.entries.iteritems())
66    self.entries = collections.OrderedDict(entries)
67
68class HeaderParser(object):
69  single_line_comment_re = re.compile(r'\s*//')
70  multi_line_comment_start_re = re.compile(r'\s*/\*')
71  enum_start_re = re.compile(r'^\s*enum\s+(\w+)\s+{\s*$')
72  enum_line_re = re.compile(r'^\s*(\w+)(\s*\=\s*([^,\n]+))?,?\s*$')
73  enum_end_re = re.compile(r'^\s*}\s*;\s*$')
74  generator_directive_re = re.compile(
75      r'^\s*//\s+GENERATED_JAVA_(\w+)\s*:\s*([\.\w]+)$')
76
77  def __init__(self, lines):
78    self._lines = lines
79    self._enum_definitions = []
80    self._in_enum = False
81    self._current_definition = None
82    self._generator_directives = {}
83
84  def ParseDefinitions(self):
85    for line in self._lines:
86      self._ParseLine(line)
87    return self._enum_definitions
88
89  def _ParseLine(self, line):
90    if not self._in_enum:
91      self._ParseRegularLine(line)
92    else:
93      self._ParseEnumLine(line)
94
95  def _ParseEnumLine(self, line):
96    if HeaderParser.single_line_comment_re.match(line):
97      return
98    if HeaderParser.multi_line_comment_start_re.match(line):
99      raise Exception('Multi-line comments in enums are not supported.')
100    enum_end = HeaderParser.enum_end_re.match(line)
101    enum_entry = HeaderParser.enum_line_re.match(line)
102    if enum_end:
103      self._ApplyGeneratorDirectives()
104      self._current_definition.Finalize()
105      self._enum_definitions.append(self._current_definition)
106      self._in_enum = False
107    elif enum_entry:
108      enum_key = enum_entry.groups()[0]
109      enum_value = enum_entry.groups()[2]
110      self._current_definition.AppendEntry(enum_key, enum_value)
111
112  def _GetCurrentEnumPackageName(self):
113    return self._generator_directives.get('ENUM_PACKAGE')
114
115  def _GetCurrentEnumPrefixToStrip(self):
116    return self._generator_directives.get('PREFIX_TO_STRIP', '')
117
118  def _ApplyGeneratorDirectives(self):
119    current_definition = self._current_definition
120    current_definition.class_package = self._GetCurrentEnumPackageName()
121    current_definition.prefix_to_strip = self._GetCurrentEnumPrefixToStrip()
122    self._generator_directives = {}
123
124  def _ParseRegularLine(self, line):
125    enum_start = HeaderParser.enum_start_re.match(line)
126    generator_directive = HeaderParser.generator_directive_re.match(line)
127    if enum_start:
128      if not self._GetCurrentEnumPackageName():
129        return
130      self._current_definition = EnumDefinition()
131      self._current_definition.class_name = enum_start.groups()[0]
132      self._in_enum = True
133    elif generator_directive:
134      directive_name = generator_directive.groups()[0]
135      directive_value = generator_directive.groups()[1]
136      self._generator_directives[directive_name] = directive_value
137
138
139def GetScriptName():
140  script_components = os.path.abspath(sys.argv[0]).split(os.path.sep)
141  build_index = script_components.index('build')
142  return os.sep.join(script_components[build_index:])
143
144
145def DoGenerate(options, source_paths):
146  output_paths = []
147  for source_path in source_paths:
148    enum_definitions = DoParseHeaderFile(source_path)
149    for enum_definition in enum_definitions:
150      package_path = enum_definition.class_package.replace('.', os.path.sep)
151      file_name = enum_definition.class_name + '.java'
152      output_path = os.path.join(options.output_dir, package_path, file_name)
153      output_paths.append(output_path)
154      if not options.print_output_only:
155        build_utils.MakeDirectory(os.path.dirname(output_path))
156        DoWriteOutput(source_path, output_path, enum_definition)
157  return output_paths
158
159
160def DoParseHeaderFile(path):
161  with open(path) as f:
162    return HeaderParser(f.readlines()).ParseDefinitions()
163
164
165def GenerateOutput(source_path, enum_definition):
166  template = Template("""
167// Copyright 2014 The Chromium Authors. All rights reserved.
168// Use of this source code is governed by a BSD-style license that can be
169// found in the LICENSE file.
170
171// This file is autogenerated by
172//     ${SCRIPT_NAME}
173// From
174//     ${SOURCE_PATH}
175
176package ${PACKAGE};
177
178public class ${CLASS_NAME} {
179${ENUM_ENTRIES}
180}
181""")
182
183  enum_template = Template('  public static final int ${NAME} = ${VALUE};')
184  enum_entries_string = []
185  for enum_name, enum_value in enum_definition.entries.iteritems():
186    values = {
187        'NAME': enum_name,
188        'VALUE': enum_value,
189    }
190    enum_entries_string.append(enum_template.substitute(values))
191  enum_entries_string = '\n'.join(enum_entries_string)
192
193  values = {
194      'CLASS_NAME': enum_definition.class_name,
195      'ENUM_ENTRIES': enum_entries_string,
196      'PACKAGE': enum_definition.class_package,
197      'SCRIPT_NAME': GetScriptName(),
198      'SOURCE_PATH': source_path,
199  }
200  return template.substitute(values)
201
202
203def DoWriteOutput(source_path, output_path, enum_definition):
204  with open(output_path, 'w') as out_file:
205    out_file.write(GenerateOutput(source_path, enum_definition))
206
207def AssertFilesList(output_paths, assert_files_list):
208  actual = set(output_paths)
209  expected = set(assert_files_list)
210  if not actual == expected:
211    need_to_add = list(actual - expected)
212    need_to_remove = list(expected - actual)
213    raise Exception('Output files list does not match expectations. Please '
214                    'add %s and remove %s.' % (need_to_add, need_to_remove))
215
216def DoMain(argv):
217  parser = optparse.OptionParser()
218
219  parser.add_option('--assert_file', action="append", default=[],
220                    dest="assert_files_list", help='Assert that the given '
221                    'file is an output. There can be multiple occurrences of '
222                    'this flag.')
223  parser.add_option('--output_dir', help='Base path for generated files.')
224  parser.add_option('--print_output_only', help='Only print output paths.',
225                    action='store_true')
226
227  options, args = parser.parse_args(argv)
228
229  output_paths = DoGenerate(options, args)
230
231  if options.assert_files_list:
232    AssertFilesList(output_paths, options.assert_files_list)
233
234  return " ".join(output_paths)
235
236if __name__ == '__main__':
237  DoMain(sys.argv[1:])
238