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