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