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