• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3# Copyright 2014 The Chromium Authors
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
8from datetime import date
9import re
10import optparse
11import os
12from string import Template
13import sys
14import textwrap
15import zipfile
16
17from util import build_utils
18from util import java_cpp_utils
19import action_helpers  # build_utils adds //build to sys.path.
20import zip_helpers
21
22
23# List of C++ types that are compatible with the Java code generated by this
24# script.
25#
26# This script can parse .idl files however, at present it ignores special
27# rules such as [cpp_enum_prefix_override="ax_attr"].
28ENUM_FIXED_TYPE_ALLOWLIST = [
29    'char', 'unsigned char', 'short', 'unsigned short', 'int', 'int8_t',
30    'int16_t', 'int32_t', 'uint8_t', 'uint16_t'
31]
32
33
34class EnumDefinition:
35
36  def __init__(self,
37               original_enum_name=None,
38               class_name_override=None,
39               enum_package=None,
40               entries=None,
41               comments=None,
42               fixed_type=None,
43               is_flag=False):
44    self.original_enum_name = original_enum_name
45    self.class_name_override = class_name_override
46    self.enum_package = enum_package
47    self.entries = collections.OrderedDict(entries or [])
48    self.comments = collections.OrderedDict(comments or [])
49    self.prefix_to_strip = None
50    self.fixed_type = fixed_type
51    self.is_flag = is_flag
52
53  def AppendEntry(self, key, value):
54    if key in self.entries:
55      raise Exception('Multiple definitions of key %s found.' % key)
56    self.entries[key] = value
57
58  def AppendEntryComment(self, key, value):
59    if key in self.comments:
60      raise Exception('Multiple definitions of key %s found.' % key)
61    self.comments[key] = value
62
63  @property
64  def class_name(self):
65    return self.class_name_override or self.original_enum_name
66
67  def Finalize(self):
68    self._Validate()
69    self._AssignEntryIndices()
70    self._StripPrefix()
71    self._NormalizeNames()
72
73  def _Validate(self):
74    assert self.class_name
75    assert self.enum_package
76    assert self.entries
77    if self.fixed_type and self.fixed_type not in ENUM_FIXED_TYPE_ALLOWLIST:
78      raise Exception('Fixed type %s for enum %s not in allowlist.' %
79                      (self.fixed_type, self.class_name))
80
81  def _AssignEntryIndices(self):
82    # Enums, if given no value, are given the value of the previous enum + 1.
83    if not all(self.entries.values()):
84      prev_enum_value = -1
85      for key, value in self.entries.items():
86        if not value:
87          self.entries[key] = prev_enum_value + 1
88        elif value in self.entries:
89          self.entries[key] = self.entries[value]
90        else:
91          try:
92            self.entries[key] = int(value)
93          except ValueError as e:
94            raise Exception('Could not interpret integer from enum value "%s" '
95                            'for key %s.' % (value, key)) from e
96        prev_enum_value = self.entries[key]
97
98
99  def _StripPrefix(self):
100    prefix_to_strip = self.prefix_to_strip
101    if not prefix_to_strip:
102      shout_case = self.original_enum_name
103      shout_case = re.sub('(?!^)([A-Z]+)', r'_\1', shout_case).upper()
104      shout_case += '_'
105
106      prefixes = [shout_case, self.original_enum_name,
107                  'k' + self.original_enum_name]
108
109      for prefix in prefixes:
110        if all(w.startswith(prefix) for w in self.entries.keys()):
111          prefix_to_strip = prefix
112          break
113      else:
114        prefix_to_strip = ''
115
116    def StripEntries(entries):
117      ret = collections.OrderedDict()
118      for k, v in entries.items():
119        stripped_key = k.replace(prefix_to_strip, '', 1)
120        if isinstance(v, str):
121          stripped_value = v.replace(prefix_to_strip, '')
122        else:
123          stripped_value = v
124        ret[stripped_key] = stripped_value
125
126      return ret
127
128    self.entries = StripEntries(self.entries)
129    self.comments = StripEntries(self.comments)
130
131  def _NormalizeNames(self):
132    self.entries = _TransformKeys(self.entries, java_cpp_utils.KCamelToShouty)
133    self.comments = _TransformKeys(self.comments, java_cpp_utils.KCamelToShouty)
134
135
136def _TransformKeys(d, func):
137  """Normalize keys in |d| and update references to old keys in |d| values."""
138  keys_map = {k: func(k) for k in d}
139  ret = collections.OrderedDict()
140  for k, v in d.items():
141    # Need to transform values as well when the entry value was explicitly set
142    # (since it could contain references to other enum entry values).
143    if isinstance(v, str):
144      # First check if a full replacement is available. This avoids issues when
145      # one key is a substring of another.
146      if v in d:
147        v = keys_map[v]
148      else:
149        for old_key, new_key in keys_map.items():
150          v = v.replace(old_key, new_key)
151    ret[keys_map[k]] = v
152  return ret
153
154
155class DirectiveSet:
156  class_name_override_key = 'CLASS_NAME_OVERRIDE'
157  enum_package_key = 'ENUM_PACKAGE'
158  prefix_to_strip_key = 'PREFIX_TO_STRIP'
159  is_flag = 'IS_FLAG'
160
161  known_keys = [
162      class_name_override_key, enum_package_key, prefix_to_strip_key, is_flag
163  ]
164
165  def __init__(self):
166    self._directives = {}
167
168  def Update(self, key, value):
169    if key not in DirectiveSet.known_keys:
170      raise Exception("Unknown directive: " + key)
171    self._directives[key] = value
172
173  @property
174  def empty(self):
175    return len(self._directives) == 0
176
177  def UpdateDefinition(self, definition):
178    definition.class_name_override = self._directives.get(
179        DirectiveSet.class_name_override_key, '')
180    definition.enum_package = self._directives.get(
181        DirectiveSet.enum_package_key)
182    definition.prefix_to_strip = self._directives.get(
183        DirectiveSet.prefix_to_strip_key)
184    definition.is_flag = self._directives.get(
185        DirectiveSet.is_flag) not in [None, 'false', '0']
186
187
188class HeaderParser:
189  single_line_comment_re = re.compile(r'\s*//\s*([^\n]*)')
190  multi_line_comment_start_re = re.compile(r'\s*/\*')
191  enum_line_re = re.compile(r'^\s*(\w+)(\s*\=\s*([^,\n]+))?,?')
192  enum_end_re = re.compile(r'^\s*}\s*;\.*$')
193  # Note: For now we only support a very specific `#if` statement to prevent the
194  # possibility of miscalculating whether lines should be ignored when building
195  # for Android.
196  if_buildflag_re = re.compile(
197      r'^#if BUILDFLAG\((\w+)\)(?: \|\| BUILDFLAG\((\w+)\))*$')
198  if_buildflag_end_re = re.compile(r'^#endif.*$')
199  generator_error_re = re.compile(r'^\s*//\s+GENERATED_JAVA_(\w+)\s*:\s*$')
200  generator_directive_re = re.compile(
201      r'^\s*//\s+GENERATED_JAVA_(\w+)\s*:\s*([\.\w]+)$')
202  multi_line_generator_directive_start_re = re.compile(
203      r'^\s*//\s+GENERATED_JAVA_(\w+)\s*:\s*\(([\.\w]*)$')
204  multi_line_directive_continuation_re = re.compile(r'^\s*//\s+([\.\w]+)$')
205  multi_line_directive_end_re = re.compile(r'^\s*//\s+([\.\w]*)\)$')
206
207  optional_class_or_struct_re = r'(class|struct)?'
208  enum_name_re = r'(\w+)'
209  optional_fixed_type_re = r'(\:\s*(\w+\s*\w+?))?'
210  enum_start_re = re.compile(r'^\s*(?:\[cpp.*\])?\s*enum\s+' +
211                             optional_class_or_struct_re + r'\s*' +
212                             enum_name_re + r'\s*' + optional_fixed_type_re +
213                             r'\s*{\s*')
214  enum_single_line_re = re.compile(
215      r'^\s*(?:\[cpp.*\])?\s*enum.*{(?P<enum_entries>.*)}.*$')
216
217  def __init__(self, lines, path=''):
218    self._lines = lines
219    self._path = path
220    self._enum_definitions = []
221    self._in_enum = False
222    # Indicates whether an #if block was encountered on a previous line (until
223    # an #endif block was seen). When nonzero, `_in_buildflag_android` indicates
224    # whether the blocks were `#if BUILDFLAG(IS_ANDROID)` or not.
225    # Note: Currently only statements like `#if BUILDFLAG(IS_<PLATFORM>)` are
226    # supported.
227    self._in_preprocessor_block = 0
228    self._in_buildflag_android = []
229    self._current_definition = None
230    self._current_comments = []
231    self._generator_directives = DirectiveSet()
232    self._multi_line_generator_directive = None
233    self._current_enum_entry = ''
234
235  def _ShouldIgnoreLine(self):
236    return self._in_preprocessor_block and not all(self._in_buildflag_android)
237
238  def _ApplyGeneratorDirectives(self):
239    self._generator_directives.UpdateDefinition(self._current_definition)
240    self._generator_directives = DirectiveSet()
241
242  def ParseDefinitions(self):
243    for line in self._lines:
244      self._ParseLine(line)
245    return self._enum_definitions
246
247  def _ParseLine(self, line):
248    if HeaderParser.if_buildflag_re.match(line):
249      self._in_preprocessor_block += 1
250      self._in_buildflag_android.append('BUILDFLAG(IS_ANDROID)' in line)
251      return
252    if self._in_preprocessor_block and HeaderParser.if_buildflag_end_re.match(
253        line):
254      self._in_preprocessor_block -= 1
255      self._in_buildflag_android.pop()
256      return
257
258    if self._ShouldIgnoreLine():
259      return
260
261    if self._multi_line_generator_directive:
262      self._ParseMultiLineDirectiveLine(line)
263      return
264
265    if not self._in_enum:
266      self._ParseRegularLine(line)
267      return
268
269    self._ParseEnumLine(line)
270
271  def _ParseEnumLine(self, line):
272    if HeaderParser.multi_line_comment_start_re.match(line):
273      raise Exception('Multi-line comments in enums are not supported in ' +
274                      self._path)
275
276    enum_comment = HeaderParser.single_line_comment_re.match(line)
277    if enum_comment:
278      comment = enum_comment.groups()[0]
279      if comment:
280        self._current_comments.append(comment)
281    elif HeaderParser.enum_end_re.match(line):
282      self._FinalizeCurrentEnumDefinition()
283    else:
284      self._AddToCurrentEnumEntry(line)
285      if ',' in line:
286        self._ParseCurrentEnumEntry()
287
288  def _ParseSingleLineEnum(self, line):
289    for entry in line.split(','):
290      self._AddToCurrentEnumEntry(entry)
291      self._ParseCurrentEnumEntry()
292
293    self._FinalizeCurrentEnumDefinition()
294
295  def _ParseCurrentEnumEntry(self):
296    if not self._current_enum_entry:
297      return
298
299    enum_entry = HeaderParser.enum_line_re.match(self._current_enum_entry)
300    if not enum_entry:
301      raise Exception('Unexpected error while attempting to parse %s as enum '
302                      'entry.' % self._current_enum_entry)
303
304    enum_key = enum_entry.groups()[0]
305    enum_value = enum_entry.groups()[2]
306    self._current_definition.AppendEntry(enum_key, enum_value)
307    if self._current_comments:
308      self._current_definition.AppendEntryComment(
309          enum_key, ' '.join(self._current_comments))
310      self._current_comments = []
311    self._current_enum_entry = ''
312
313  def _AddToCurrentEnumEntry(self, line):
314    self._current_enum_entry += ' ' + line.strip()
315
316  def _FinalizeCurrentEnumDefinition(self):
317    if self._current_enum_entry:
318      self._ParseCurrentEnumEntry()
319    self._ApplyGeneratorDirectives()
320    self._current_definition.Finalize()
321    self._enum_definitions.append(self._current_definition)
322    self._current_definition = None
323    self._in_enum = False
324
325  def _ParseMultiLineDirectiveLine(self, line):
326    multi_line_directive_continuation = (
327        HeaderParser.multi_line_directive_continuation_re.match(line))
328    multi_line_directive_end = (
329        HeaderParser.multi_line_directive_end_re.match(line))
330
331    if multi_line_directive_continuation:
332      value_cont = multi_line_directive_continuation.groups()[0]
333      self._multi_line_generator_directive[1].append(value_cont)
334    elif multi_line_directive_end:
335      directive_name = self._multi_line_generator_directive[0]
336      directive_value = "".join(self._multi_line_generator_directive[1])
337      directive_value += multi_line_directive_end.groups()[0]
338      self._multi_line_generator_directive = None
339      self._generator_directives.Update(directive_name, directive_value)
340    else:
341      raise Exception('Malformed multi-line directive declaration in ' +
342                      self._path)
343
344  def _ParseRegularLine(self, line):
345    enum_start = HeaderParser.enum_start_re.match(line)
346    generator_directive_error = HeaderParser.generator_error_re.match(line)
347    generator_directive = HeaderParser.generator_directive_re.match(line)
348    multi_line_generator_directive_start = (
349        HeaderParser.multi_line_generator_directive_start_re.match(line))
350    single_line_enum = HeaderParser.enum_single_line_re.match(line)
351
352    if generator_directive_error:
353      raise Exception('Malformed directive declaration in ' + self._path +
354                      '. Use () for multi-line directives. E.g.\n' +
355                      '// GENERATED_JAVA_ENUM_PACKAGE: (\n' +
356                      '//   foo.package)')
357    if generator_directive:
358      directive_name = generator_directive.groups()[0]
359      directive_value = generator_directive.groups()[1]
360      self._generator_directives.Update(directive_name, directive_value)
361    elif multi_line_generator_directive_start:
362      directive_name = multi_line_generator_directive_start.groups()[0]
363      directive_value = multi_line_generator_directive_start.groups()[1]
364      self._multi_line_generator_directive = (directive_name, [directive_value])
365    elif enum_start or single_line_enum:
366      if self._generator_directives.empty:
367        return
368      self._current_definition = EnumDefinition(
369          original_enum_name=enum_start.groups()[1],
370          fixed_type=enum_start.groups()[3])
371      self._in_enum = True
372      if single_line_enum:
373        self._ParseSingleLineEnum(single_line_enum.group('enum_entries'))
374
375
376def DoGenerate(source_paths):
377  for source_path in source_paths:
378    enum_definitions = DoParseHeaderFile(source_path)
379    if not enum_definitions:
380      raise Exception('No enums found in %s\n'
381                      'Did you forget prefixing enums with '
382                      '"// GENERATED_JAVA_ENUM_PACKAGE: foo"?' %
383                      source_path)
384    for enum_definition in enum_definitions:
385      output_path = java_cpp_utils.GetJavaFilePath(enum_definition.enum_package,
386                                                   enum_definition.class_name)
387      output = GenerateOutput(source_path, enum_definition)
388      yield output_path, output
389
390
391def DoParseHeaderFile(path):
392  with open(path) as f:
393    return HeaderParser(f.readlines(), path).ParseDefinitions()
394
395
396def GenerateOutput(source_path, enum_definition):
397  template = Template("""
398// Copyright ${YEAR} The Chromium Authors
399// Use of this source code is governed by a BSD-style license that can be
400// found in the LICENSE file.
401
402// This file is autogenerated by
403//     ${SCRIPT_NAME}
404// From
405//     ${SOURCE_PATH}
406
407package ${PACKAGE};
408
409import androidx.annotation.IntDef;
410
411import java.lang.annotation.Retention;
412import java.lang.annotation.RetentionPolicy;
413
414@IntDef(${FLAG_DEF}{
415${INT_DEF}
416})
417@Retention(RetentionPolicy.SOURCE)
418public @interface ${CLASS_NAME} {
419${ENUM_ENTRIES}
420}
421""")
422
423  enum_template = Template('  int ${NAME} = ${VALUE};')
424  enum_entries_string = []
425  enum_names = []
426  for enum_name, enum_value in enum_definition.entries.items():
427    values = {
428        'NAME': enum_name,
429        'VALUE': enum_value,
430    }
431    enum_comments = enum_definition.comments.get(enum_name)
432    if enum_comments:
433      enum_comments_indent = '   * '
434      comments_line_wrapper = textwrap.TextWrapper(
435          initial_indent=enum_comments_indent,
436          subsequent_indent=enum_comments_indent,
437          width=100)
438      enum_entries_string.append('  /**')
439      enum_entries_string.append('\n'.join(
440          comments_line_wrapper.wrap(enum_comments)))
441      enum_entries_string.append('   */')
442    enum_entries_string.append(enum_template.substitute(values))
443    if enum_name != "NUM_ENTRIES":
444      enum_names.append(enum_definition.class_name + '.' + enum_name)
445  enum_entries_string = '\n'.join(enum_entries_string)
446
447  enum_names_indent = ' ' * 4
448  wrapper = textwrap.TextWrapper(initial_indent = enum_names_indent,
449                                 subsequent_indent = enum_names_indent,
450                                 width = 100)
451  enum_names_string = '\n'.join(wrapper.wrap(', '.join(enum_names)))
452
453  values = {
454      'CLASS_NAME': enum_definition.class_name,
455      'ENUM_ENTRIES': enum_entries_string,
456      'PACKAGE': enum_definition.enum_package,
457      'FLAG_DEF': 'flag = true, value = ' if enum_definition.is_flag else '',
458      'INT_DEF': enum_names_string,
459      'SCRIPT_NAME': java_cpp_utils.GetScriptName(),
460      'SOURCE_PATH': source_path,
461      'YEAR': str(date.today().year),
462  }
463  return template.substitute(values)
464
465
466def DoMain(argv):
467  usage = 'usage: %prog [options] [output_dir] input_file(s)...'
468  parser = optparse.OptionParser(usage=usage)
469
470  parser.add_option('--srcjar',
471                    help='When specified, a .srcjar at the given path is '
472                    'created instead of individual .java files.')
473
474  options, args = parser.parse_args(argv)
475
476  if not args:
477    parser.error('Need to specify at least one input file')
478  input_paths = args
479
480  with action_helpers.atomic_output(options.srcjar) as f:
481    with zipfile.ZipFile(f, 'w', zipfile.ZIP_STORED) as srcjar:
482      for output_path, data in DoGenerate(input_paths):
483        zip_helpers.add_to_zip_hermetic(srcjar, output_path, data=data)
484
485
486if __name__ == '__main__':
487  DoMain(sys.argv[1:])
488