• 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  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  # Note: For now we only support a very specific `#if` statement to prevent the
181  # possibility of miscalculating whether lines should be ignored when building
182  # for Android.
183  if_buildflag_re = re.compile(r'^#if BUILDFLAG\((\w+)\)$')
184  if_buildflag_end_re = re.compile(r'^#endif.*$')
185  generator_error_re = re.compile(r'^\s*//\s+GENERATED_JAVA_(\w+)\s*:\s*$')
186  generator_directive_re = re.compile(
187      r'^\s*//\s+GENERATED_JAVA_(\w+)\s*:\s*([\.\w]+)$')
188  multi_line_generator_directive_start_re = re.compile(
189      r'^\s*//\s+GENERATED_JAVA_(\w+)\s*:\s*\(([\.\w]*)$')
190  multi_line_directive_continuation_re = re.compile(r'^\s*//\s+([\.\w]+)$')
191  multi_line_directive_end_re = re.compile(r'^\s*//\s+([\.\w]*)\)$')
192
193  optional_class_or_struct_re = r'(class|struct)?'
194  enum_name_re = r'(\w+)'
195  optional_fixed_type_re = r'(\:\s*(\w+\s*\w+?))?'
196  enum_start_re = re.compile(r'^\s*(?:\[cpp.*\])?\s*enum\s+' +
197                             optional_class_or_struct_re + r'\s*' +
198                             enum_name_re + r'\s*' + optional_fixed_type_re +
199                             r'\s*{\s*')
200  enum_single_line_re = re.compile(
201      r'^\s*(?:\[cpp.*\])?\s*enum.*{(?P<enum_entries>.*)}.*$')
202
203  def __init__(self, lines, path=''):
204    self._lines = lines
205    self._path = path
206    self._enum_definitions = []
207    self._in_enum = False
208    # Indicates whether an #if block was encountered on a previous line (until
209    # an #endif block was seen). When True, `_in_buildflag_android` indicates
210    # whether the block was `#if BUILDFLAG(IS_ANDROID)` or not.
211    # Note: Currently only statements like `#if BUILDFLAG(IS_<PLATFORM>)` are
212    # supported.
213    self._in_preprocessor_block = False
214    self._in_buildflag_android = False
215    self._current_definition = None
216    self._current_comments = []
217    self._generator_directives = DirectiveSet()
218    self._multi_line_generator_directive = None
219    self._current_enum_entry = ''
220
221  def _ShouldIgnoreLine(self):
222    return self._in_preprocessor_block and not self._in_buildflag_android
223
224  def _ApplyGeneratorDirectives(self):
225    self._generator_directives.UpdateDefinition(self._current_definition)
226    self._generator_directives = DirectiveSet()
227
228  def ParseDefinitions(self):
229    for line in self._lines:
230      self._ParseLine(line)
231    return self._enum_definitions
232
233  def _ParseLine(self, line):
234    if m := HeaderParser.if_buildflag_re.match(line):
235      if self._in_preprocessor_block:
236        raise Exception('Nested #if statements not supported. Found: ' + line)
237      self._in_preprocessor_block = True
238      self._in_buildflag_android = m.group(1) == "IS_ANDROID"
239      return
240    if HeaderParser.if_buildflag_end_re.match(line):
241      self._in_preprocessor_block = False
242      self._in_buildflag_android = False
243      return
244
245    if self._ShouldIgnoreLine():
246      return
247
248    if self._multi_line_generator_directive:
249      self._ParseMultiLineDirectiveLine(line)
250      return
251
252    if not self._in_enum:
253      self._ParseRegularLine(line)
254      return
255
256    self._ParseEnumLine(line)
257
258  def _ParseEnumLine(self, line):
259    if HeaderParser.multi_line_comment_start_re.match(line):
260      raise Exception('Multi-line comments in enums are not supported in ' +
261                      self._path)
262
263    enum_comment = HeaderParser.single_line_comment_re.match(line)
264    if enum_comment:
265      comment = enum_comment.groups()[0]
266      if comment:
267        self._current_comments.append(comment)
268    elif HeaderParser.enum_end_re.match(line):
269      self._FinalizeCurrentEnumDefinition()
270    else:
271      self._AddToCurrentEnumEntry(line)
272      if ',' in line:
273        self._ParseCurrentEnumEntry()
274
275  def _ParseSingleLineEnum(self, line):
276    for entry in line.split(','):
277      self._AddToCurrentEnumEntry(entry)
278      self._ParseCurrentEnumEntry()
279
280    self._FinalizeCurrentEnumDefinition()
281
282  def _ParseCurrentEnumEntry(self):
283    if not self._current_enum_entry:
284      return
285
286    enum_entry = HeaderParser.enum_line_re.match(self._current_enum_entry)
287    if not enum_entry:
288      raise Exception('Unexpected error while attempting to parse %s as enum '
289                      'entry.' % self._current_enum_entry)
290
291    enum_key = enum_entry.groups()[0]
292    enum_value = enum_entry.groups()[2]
293    self._current_definition.AppendEntry(enum_key, enum_value)
294    if self._current_comments:
295      self._current_definition.AppendEntryComment(
296          enum_key, ' '.join(self._current_comments))
297      self._current_comments = []
298    self._current_enum_entry = ''
299
300  def _AddToCurrentEnumEntry(self, line):
301    self._current_enum_entry += ' ' + line.strip()
302
303  def _FinalizeCurrentEnumDefinition(self):
304    if self._current_enum_entry:
305      self._ParseCurrentEnumEntry()
306    self._ApplyGeneratorDirectives()
307    self._current_definition.Finalize()
308    self._enum_definitions.append(self._current_definition)
309    self._current_definition = None
310    self._in_enum = False
311
312  def _ParseMultiLineDirectiveLine(self, line):
313    multi_line_directive_continuation = (
314        HeaderParser.multi_line_directive_continuation_re.match(line))
315    multi_line_directive_end = (
316        HeaderParser.multi_line_directive_end_re.match(line))
317
318    if multi_line_directive_continuation:
319      value_cont = multi_line_directive_continuation.groups()[0]
320      self._multi_line_generator_directive[1].append(value_cont)
321    elif multi_line_directive_end:
322      directive_name = self._multi_line_generator_directive[0]
323      directive_value = "".join(self._multi_line_generator_directive[1])
324      directive_value += multi_line_directive_end.groups()[0]
325      self._multi_line_generator_directive = None
326      self._generator_directives.Update(directive_name, directive_value)
327    else:
328      raise Exception('Malformed multi-line directive declaration in ' +
329                      self._path)
330
331  def _ParseRegularLine(self, line):
332    enum_start = HeaderParser.enum_start_re.match(line)
333    generator_directive_error = HeaderParser.generator_error_re.match(line)
334    generator_directive = HeaderParser.generator_directive_re.match(line)
335    multi_line_generator_directive_start = (
336        HeaderParser.multi_line_generator_directive_start_re.match(line))
337    single_line_enum = HeaderParser.enum_single_line_re.match(line)
338
339    if generator_directive_error:
340      raise Exception('Malformed directive declaration in ' + self._path +
341                      '. Use () for multi-line directives. E.g.\n' +
342                      '// GENERATED_JAVA_ENUM_PACKAGE: (\n' +
343                      '//   foo.package)')
344    if generator_directive:
345      directive_name = generator_directive.groups()[0]
346      directive_value = generator_directive.groups()[1]
347      self._generator_directives.Update(directive_name, directive_value)
348    elif multi_line_generator_directive_start:
349      directive_name = multi_line_generator_directive_start.groups()[0]
350      directive_value = multi_line_generator_directive_start.groups()[1]
351      self._multi_line_generator_directive = (directive_name, [directive_value])
352    elif enum_start or single_line_enum:
353      if self._generator_directives.empty:
354        return
355      self._current_definition = EnumDefinition(
356          original_enum_name=enum_start.groups()[1],
357          fixed_type=enum_start.groups()[3])
358      self._in_enum = True
359      if single_line_enum:
360        self._ParseSingleLineEnum(single_line_enum.group('enum_entries'))
361
362
363def DoGenerate(source_paths):
364  for source_path in source_paths:
365    enum_definitions = DoParseHeaderFile(source_path)
366    if not enum_definitions:
367      raise Exception('No enums found in %s\n'
368                      'Did you forget prefixing enums with '
369                      '"// GENERATED_JAVA_ENUM_PACKAGE: foo"?' %
370                      source_path)
371    for enum_definition in enum_definitions:
372      output_path = java_cpp_utils.GetJavaFilePath(enum_definition.enum_package,
373                                                   enum_definition.class_name)
374      output = GenerateOutput(source_path, enum_definition)
375      yield output_path, output
376
377
378def DoParseHeaderFile(path):
379  with open(path) as f:
380    return HeaderParser(f.readlines(), path).ParseDefinitions()
381
382
383def GenerateOutput(source_path, enum_definition):
384  template = Template("""
385// Copyright ${YEAR} The Chromium Authors
386// Use of this source code is governed by a BSD-style license that can be
387// found in the LICENSE file.
388
389// This file is autogenerated by
390//     ${SCRIPT_NAME}
391// From
392//     ${SOURCE_PATH}
393
394package ${PACKAGE};
395
396import androidx.annotation.IntDef;
397
398import java.lang.annotation.Retention;
399import java.lang.annotation.RetentionPolicy;
400
401@IntDef({
402${INT_DEF}
403})
404@Retention(RetentionPolicy.SOURCE)
405public @interface ${CLASS_NAME} {
406${ENUM_ENTRIES}
407}
408""")
409
410  enum_template = Template('  int ${NAME} = ${VALUE};')
411  enum_entries_string = []
412  enum_names = []
413  for enum_name, enum_value in enum_definition.entries.items():
414    values = {
415        'NAME': enum_name,
416        'VALUE': enum_value,
417    }
418    enum_comments = enum_definition.comments.get(enum_name)
419    if enum_comments:
420      enum_comments_indent = '   * '
421      comments_line_wrapper = textwrap.TextWrapper(
422          initial_indent=enum_comments_indent,
423          subsequent_indent=enum_comments_indent,
424          width=100)
425      enum_entries_string.append('  /**')
426      enum_entries_string.append('\n'.join(
427          comments_line_wrapper.wrap(enum_comments)))
428      enum_entries_string.append('   */')
429    enum_entries_string.append(enum_template.substitute(values))
430    if enum_name != "NUM_ENTRIES":
431      enum_names.append(enum_definition.class_name + '.' + enum_name)
432  enum_entries_string = '\n'.join(enum_entries_string)
433
434  enum_names_indent = ' ' * 4
435  wrapper = textwrap.TextWrapper(initial_indent = enum_names_indent,
436                                 subsequent_indent = enum_names_indent,
437                                 width = 100)
438  enum_names_string = '\n'.join(wrapper.wrap(', '.join(enum_names)))
439
440  values = {
441      'CLASS_NAME': enum_definition.class_name,
442      'ENUM_ENTRIES': enum_entries_string,
443      'PACKAGE': enum_definition.enum_package,
444      'INT_DEF': enum_names_string,
445      'SCRIPT_NAME': java_cpp_utils.GetScriptName(),
446      'SOURCE_PATH': source_path,
447      'YEAR': str(date.today().year)
448  }
449  return template.substitute(values)
450
451
452def DoMain(argv):
453  usage = 'usage: %prog [options] [output_dir] input_file(s)...'
454  parser = optparse.OptionParser(usage=usage)
455
456  parser.add_option('--srcjar',
457                    help='When specified, a .srcjar at the given path is '
458                    'created instead of individual .java files.')
459
460  options, args = parser.parse_args(argv)
461
462  if not args:
463    parser.error('Need to specify at least one input file')
464  input_paths = args
465
466  with action_helpers.atomic_output(options.srcjar) as f:
467    with zipfile.ZipFile(f, 'w', zipfile.ZIP_STORED) as srcjar:
468      for output_path, data in DoGenerate(input_paths):
469        zip_helpers.add_to_zip_hermetic(srcjar, output_path, data=data)
470
471
472if __name__ == '__main__':
473  DoMain(sys.argv[1:])
474