tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

java_cpp_enum.py (18555B)


      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 
      7 import collections
      8 from datetime import date
      9 import re
     10 import optparse
     11 import os
     12 from string import Template
     13 import sys
     14 import textwrap
     15 import zipfile
     16 
     17 from util import build_utils
     18 from util import java_cpp_utils
     19 import action_helpers  # build_utils adds //build to sys.path.
     20 import 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"].
     28 ENUM_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 
     34 class 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 
    159 def _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 
    178 class 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 
    211 class 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 
    399 def 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 
    414 def DoParseHeaderFile(path):
    415  with open(path) as f:
    416    return HeaderParser(f.readlines(), path).ParseDefinitions()
    417 
    418 
    419 def 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 
    430 package ${PACKAGE};
    431 
    432 import androidx.annotation.IntDef;
    433 
    434 import java.lang.annotation.ElementType;
    435 import java.lang.annotation.Retention;
    436 import java.lang.annotation.RetentionPolicy;
    437 import java.lang.annotation.Target;
    438 
    439 @IntDef(${FLAG_DEF}{
    440 ${INT_DEF}
    441 })
    442 @Target(ElementType.TYPE_USE)
    443 @Retention(RetentionPolicy.SOURCE)
    444 public @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 
    492 def 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 
    512 if __name__ == '__main__':
    513  DoMain(sys.argv[1:])