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