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