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