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