1#! /usr/bin/env python3 2# 3# Protocol Buffers - Google's data interchange format 4# Copyright 2015 Google Inc. All rights reserved. 5# 6# Use of this source code is governed by a BSD-style 7# license that can be found in the LICENSE file or at 8# https://developers.google.com/open-source/licenses/bsd 9 10"""PDDM - Poor Developers' Debug-able Macros 11 12A simple markup that can be added in comments of source so they can then be 13expanded out into code. Most of this could be done with CPP macros, but then 14developers can't really step through them in the debugger, this way they are 15expanded to the same code, but you can debug them. 16 17Any file can be processed, but the syntax is designed around a C based compiler. 18Processed lines start with "//%". There are three types of sections you can 19create: Text (left alone), Macro Definitions, and Macro Expansions. There is 20no order required between definitions and expansions, all definitions are read 21before any expansions are processed (thus, if desired, definitions can be put 22at the end of the file to keep them out of the way of the code). 23 24Macro Definitions are started with "//%PDDM-DEFINE Name(args)" and all lines 25afterwards that start with "//%" are included in the definition. Multiple 26macros can be defined in one block by just using a another "//%PDDM-DEFINE" 27line to start the next macro. Optionally, a macro can be ended with 28"//%PDDM-DEFINE-END", this can be useful when you want to make it clear that 29trailing blank lines are included in the macro. You can also end a definition 30with an expansion. 31 32Macro Expansions are started by single lines containing 33"//%PDDM-EXPAND Name(args)" and then with "//%PDDM-EXPAND-END" or another 34expansions. All lines in-between are replaced by the result of the expansion. 35The first line of the expansion is always a blank like just for readability. 36 37Expansion itself is pretty simple, one macro can invoke another macro, but 38you cannot nest the invoke of a macro in another macro (i.e. - can't do 39"foo(bar(a))", but you can define foo(a) and bar(b) where bar invokes foo() 40within its expansion. 41 42When macros are expanded, the arg references can also add "$O" suffix to the 43name (i.e. - "NAME$O") to specify an option to be applied. The options are: 44 45 $S - Replace each character in the value with a space. 46 $l - Lowercase the first letter of the value. 47 $L - Lowercase the whole value. 48 $u - Uppercase the first letter of the value. 49 $U - Uppercase the whole value. 50 51Within a macro you can use ## to cause things to get joined together after 52expansion (i.e. - "a##b" within a macro will become "ab"). 53 54Example: 55 56 int foo(MyEnum x) { 57 switch (x) { 58 //%PDDM-EXPAND case(Enum_Left, 1) 59 //%PDDM-EXPAND case(Enum_Center, 2) 60 //%PDDM-EXPAND case(Enum_Right, 3) 61 //%PDDM-EXPAND-END 62 } 63 64 //%PDDM-DEFINE case(_A, _B) 65 //% case _A: 66 //% return _B; 67 68 A macro ends at the start of the next one, or an optional %PDDM-DEFINE-END 69 can be used to avoid adding extra blank lines/returns (or make it clear when 70 it is desired). 71 72 One macro can invoke another by simply using its name NAME(ARGS). You cannot 73 nest an invoke inside another (i.e. - NAME1(NAME2(ARGS)) isn't supported). 74 75 Within a macro you can use ## to cause things to get joined together after 76 processing (i.e. - "a##b" within a macro will become "ab"). 77 78 79""" 80 81import optparse 82import os 83import re 84import sys 85 86 87# Regex for macro definition. 88_MACRO_RE = re.compile(r'(?P<name>\w+)\((?P<args>.*?)\)') 89# Regex for macro's argument definition. 90_MACRO_ARG_NAME_RE = re.compile(r'^\w+$') 91 92# Line inserted after each EXPAND. 93_GENERATED_CODE_LINE = ( 94 '// This block of code is generated, do not edit it directly.' 95) 96 97 98def _MacroRefRe(macro_names): 99 # Takes in a list of macro names and makes a regex that will match invokes 100 # of those macros. 101 return re.compile(r'\b(?P<macro_ref>(?P<name>(%s))\((?P<args>.*?)\))' % 102 '|'.join(macro_names)) 103 104 105def _MacroArgRefRe(macro_arg_names): 106 # Takes in a list of macro arg names and makes a regex that will match 107 # uses of those args. 108 return re.compile(r'\b(?P<name>(%s))(\$(?P<option>.))?\b' % 109 '|'.join(macro_arg_names)) 110 111 112class PDDMError(Exception): 113 """Error thrown by pddm.""" 114 115 def __init__(self, message="Error"): 116 self.message = message 117 super().__init__(self.message) 118 119 120class MacroCollection(object): 121 """Hold a set of macros and can resolve/expand them.""" 122 123 def __init__(self, a_file=None): 124 """Initializes the collection. 125 126 Args: 127 a_file: The file like stream to parse. 128 129 Raises: 130 PDDMError if there are any issues. 131 """ 132 self._macros = dict() 133 if a_file: 134 self.ParseInput(a_file) 135 136 class MacroDefinition(object): 137 """Holds a macro definition.""" 138 139 def __init__(self, name, arg_names): 140 self._name = name 141 self._args = tuple(arg_names) 142 self._body = '' 143 self._needNewLine = False 144 145 def AppendLine(self, line): 146 if self._needNewLine: 147 self._body += '\n' 148 self._body += line 149 self._needNewLine = not line.endswith('\n') 150 151 @property 152 def name(self): 153 return self._name 154 155 @property 156 def args(self): 157 return self._args 158 159 @property 160 def body(self): 161 return self._body 162 163 def ParseInput(self, a_file): 164 """Consumes input extracting definitions. 165 166 Args: 167 a_file: The file like stream to parse. 168 169 Raises: 170 PDDMError if there are any issues. 171 """ 172 input_lines = a_file.read().splitlines() 173 self.ParseLines(input_lines) 174 175 def ParseLines(self, input_lines): 176 """Parses list of lines. 177 178 Args: 179 input_lines: A list of strings of input to parse (no newlines on the 180 strings). 181 182 Raises: 183 PDDMError if there are any issues. 184 """ 185 current_macro = None 186 for line in input_lines: 187 if line.startswith('PDDM-'): 188 directive = line.split(' ', 1)[0] 189 if directive == 'PDDM-DEFINE': 190 name, args = self._ParseDefineLine(line) 191 if self._macros.get(name): 192 raise PDDMError('Attempt to redefine macro: "%s"' % line) 193 current_macro = self.MacroDefinition(name, args) 194 self._macros[name] = current_macro 195 continue 196 if directive == 'PDDM-DEFINE-END': 197 if not current_macro: 198 raise PDDMError('Got DEFINE-END directive without an active macro:' 199 ' "%s"' % line) 200 current_macro = None 201 continue 202 raise PDDMError('Hit a line with an unknown directive: "%s"' % line) 203 204 if current_macro: 205 current_macro.AppendLine(line) 206 continue 207 208 # Allow blank lines between macro definitions. 209 if line.strip() == '': 210 continue 211 212 raise PDDMError('Hit a line that wasn\'t a directive and no open macro' 213 ' definition: "%s"' % line) 214 215 def _ParseDefineLine(self, input_line): 216 assert input_line.startswith('PDDM-DEFINE') 217 line = input_line[12:].strip() 218 match = _MACRO_RE.match(line) 219 # Must match full line 220 if match is None or match.group(0) != line: 221 raise PDDMError('Failed to parse macro definition: "%s"' % input_line) 222 name = match.group('name') 223 args_str = match.group('args').strip() 224 args = [] 225 if args_str: 226 for part in args_str.split(','): 227 arg = part.strip() 228 if arg == '': 229 raise PDDMError('Empty arg name in macro definition: "%s"' 230 % input_line) 231 if not _MACRO_ARG_NAME_RE.match(arg): 232 raise PDDMError('Invalid arg name "%s" in macro definition: "%s"' 233 % (arg, input_line)) 234 if arg in args: 235 raise PDDMError('Arg name "%s" used more than once in macro' 236 ' definition: "%s"' % (arg, input_line)) 237 args.append(arg) 238 return (name, tuple(args)) 239 240 def Expand(self, macro_ref_str): 241 """Expands the macro reference. 242 243 Args: 244 macro_ref_str: String of a macro reference (i.e. foo(a, b)). 245 246 Returns: 247 The text from the expansion. 248 249 Raises: 250 PDDMError if there are any issues. 251 """ 252 match = _MACRO_RE.match(macro_ref_str) 253 if match is None or match.group(0) != macro_ref_str: 254 raise PDDMError('Failed to parse macro reference: "%s"' % macro_ref_str) 255 if match.group('name') not in self._macros: 256 raise PDDMError('No macro named "%s".' % match.group('name')) 257 return self._Expand(match, [], macro_ref_str) 258 259 def _FormatStack(self, macro_ref_stack): 260 result = '' 261 for _, macro_ref in reversed(macro_ref_stack): 262 result += '\n...while expanding "%s".' % macro_ref 263 return result 264 265 def _Expand(self, macro_ref_match, macro_stack, macro_ref_str=None): 266 if macro_ref_str is None: 267 macro_ref_str = macro_ref_match.group('macro_ref') 268 name = macro_ref_match.group('name') 269 for prev_name, prev_macro_ref in macro_stack: 270 if name == prev_name: 271 raise PDDMError('Found macro recursion, invoking "%s":%s' % 272 (macro_ref_str, self._FormatStack(macro_stack))) 273 macro = self._macros[name] 274 args_str = macro_ref_match.group('args').strip() 275 args = [] 276 if args_str or len(macro.args): 277 args = [x.strip() for x in args_str.split(',')] 278 if len(args) != len(macro.args): 279 raise PDDMError('Expected %d args, got: "%s".%s' % 280 (len(macro.args), macro_ref_str, 281 self._FormatStack(macro_stack))) 282 # Replace args usages. 283 result = self._ReplaceArgValues(macro, args, macro_ref_str, macro_stack) 284 # Expand any macro invokes. 285 new_macro_stack = macro_stack + [(name, macro_ref_str)] 286 while True: 287 eval_result = self._EvalMacrosRefs(result, new_macro_stack) 288 # Consume all ## directives to glue things together. 289 eval_result = eval_result.replace('##', '') 290 if eval_result == result: 291 break 292 result = eval_result 293 return result 294 295 def _ReplaceArgValues(self, 296 macro, arg_values, macro_ref_to_report, macro_stack): 297 if len(arg_values) == 0: 298 # Nothing to do 299 return macro.body 300 assert len(arg_values) == len(macro.args) 301 args = dict(list(zip(macro.args, arg_values))) 302 303 def _lookupArg(match): 304 val = args[match.group('name')] 305 opt = match.group('option') 306 if opt: 307 if opt == 'S': # Spaces for the length 308 return ' ' * len(val) 309 elif opt == 'l': # Lowercase first character 310 if val: 311 return val[0].lower() + val[1:] 312 else: 313 return val 314 elif opt == 'L': # All Lowercase 315 return val.lower() 316 elif opt == 'u': # Uppercase first character 317 if val: 318 return val[0].upper() + val[1:] 319 else: 320 return val 321 elif opt == 'U': # All Uppercase 322 return val.upper() 323 else: 324 raise PDDMError('Unknown arg option "%s$%s" while expanding "%s".%s' 325 % (match.group('name'), match.group('option'), 326 macro_ref_to_report, 327 self._FormatStack(macro_stack))) 328 return val 329 # Let the regex do the work! 330 macro_arg_ref_re = _MacroArgRefRe(macro.args) 331 return macro_arg_ref_re.sub(_lookupArg, macro.body) 332 333 def _EvalMacrosRefs(self, text, macro_stack): 334 macro_ref_re = _MacroRefRe(list(self._macros.keys())) 335 336 def _resolveMacro(match): 337 return self._Expand(match, macro_stack) 338 return macro_ref_re.sub(_resolveMacro, text) 339 340 341class SourceFile(object): 342 """Represents a source file with PDDM directives in it.""" 343 344 def __init__(self, a_file, import_resolver=None): 345 """Initializes the file reading in the file. 346 347 Args: 348 a_file: The file to read in. 349 import_resolver: a function that given a path will return a stream for 350 the contents. 351 352 Raises: 353 PDDMError if there are any issues. 354 """ 355 self._sections = [] 356 self._original_content = a_file.read() 357 self._import_resolver = import_resolver 358 self._processed_content = None 359 360 class SectionBase(object): 361 362 def __init__(self, first_line_num): 363 self._lines = [] 364 self._first_line_num = first_line_num 365 366 def TryAppend(self, line, line_num): 367 """Try appending a line. 368 369 Args: 370 line: The line to append. 371 line_num: The number of the line. 372 373 Returns: 374 A tuple of (SUCCESS, CAN_ADD_MORE). If SUCCESS if False, the line 375 wasn't append. If SUCCESS is True, then CAN_ADD_MORE is True/False to 376 indicate if more lines can be added after this one. 377 """ 378 assert False, "subclass should have overridden" 379 return (False, False) 380 381 def HitEOF(self): 382 """Called when the EOF was reached for for a given section.""" 383 pass 384 385 def BindMacroCollection(self, macro_collection): 386 """Binds the chunk to a macro collection. 387 388 Args: 389 macro_collection: The collection to bind too. 390 """ 391 pass 392 393 def Append(self, line): 394 self._lines.append(line) 395 396 @property 397 def lines(self): 398 return self._lines 399 400 @property 401 def num_lines_captured(self): 402 return len(self._lines) 403 404 @property 405 def first_line_num(self): 406 return self._first_line_num 407 408 @property 409 def first_line(self): 410 if not self._lines: 411 return '' 412 return self._lines[0] 413 414 @property 415 def text(self): 416 return '\n'.join(self.lines) + '\n' 417 418 class TextSection(SectionBase): 419 """Text section that is echoed out as is.""" 420 421 def TryAppend(self, line, line_num): 422 if line.startswith('//%PDDM'): 423 return (False, False) 424 self.Append(line) 425 return (True, True) 426 427 class ExpansionSection(SectionBase): 428 """Section that is the result of an macro expansion.""" 429 430 def __init__(self, first_line_num): 431 SourceFile.SectionBase.__init__(self, first_line_num) 432 self._macro_collection = None 433 434 def TryAppend(self, line, line_num): 435 if line.startswith('//%PDDM'): 436 directive = line.split(' ', 1)[0] 437 if directive == '//%PDDM-EXPAND': 438 self.Append(line) 439 return (True, True) 440 if directive == '//%PDDM-EXPAND-END': 441 assert self.num_lines_captured > 0 442 return (True, False) 443 raise PDDMError('Ran into directive ("%s", line %d) while in "%s".' % 444 (directive, line_num, self.first_line)) 445 # Eat other lines. 446 return (True, True) 447 448 def HitEOF(self): 449 raise PDDMError('Hit the end of the file while in "%s".' % 450 self.first_line) 451 452 def BindMacroCollection(self, macro_collection): 453 self._macro_collection = macro_collection 454 455 @property 456 def lines(self): 457 captured_lines = SourceFile.SectionBase.lines.fget(self) 458 directive_len = len('//%PDDM-EXPAND') 459 result = [] 460 for line in captured_lines: 461 result.append(line) 462 if self._macro_collection: 463 # Always add a blank line, seems to read better. (If need be, add an 464 # option to the EXPAND to indicate if this should be done.) 465 result.extend([_GENERATED_CODE_LINE, '']) 466 macro = line[directive_len:].strip() 467 try: 468 expand_result = self._macro_collection.Expand(macro) 469 # Since expansions are line oriented, strip trailing whitespace 470 # from the lines. 471 lines = [x.rstrip() for x in expand_result.split('\n')] 472 result.append('\n'.join(lines)) 473 except PDDMError as e: 474 raise PDDMError('%s\n...while expanding "%s" from the section' 475 ' that started:\n Line %d: %s' % 476 (e.message, macro, 477 self.first_line_num, self.first_line)) 478 479 # Add the ending marker. 480 if len(captured_lines) == 1: 481 result.append('//%%PDDM-EXPAND-END %s' % 482 captured_lines[0][directive_len:].strip()) 483 else: 484 result.append('//%%PDDM-EXPAND-END (%s expansions)' % 485 len(captured_lines)) 486 return result 487 488 class DefinitionSection(SectionBase): 489 """Section containing macro definitions""" 490 491 def TryAppend(self, line, line_num): 492 if not line.startswith('//%'): 493 return (False, False) 494 if line.startswith('//%PDDM'): 495 directive = line.split(' ', 1)[0] 496 if directive == "//%PDDM-EXPAND": 497 return False, False 498 if directive not in ('//%PDDM-DEFINE', '//%PDDM-DEFINE-END'): 499 raise PDDMError('Ran into directive ("%s", line %d) while in "%s".' % 500 (directive, line_num, self.first_line)) 501 self.Append(line) 502 return (True, True) 503 504 def BindMacroCollection(self, macro_collection): 505 if macro_collection: 506 try: 507 # Parse the lines after stripping the prefix. 508 macro_collection.ParseLines([x[3:] for x in self.lines]) 509 except PDDMError as e: 510 raise PDDMError('%s\n...while parsing section that started:\n' 511 ' Line %d: %s' % 512 (e.message, self.first_line_num, self.first_line)) 513 514 class ImportDefinesSection(SectionBase): 515 """Section containing an import of PDDM-DEFINES from an external file.""" 516 517 def __init__(self, first_line_num, import_resolver): 518 SourceFile.SectionBase.__init__(self, first_line_num) 519 self._import_resolver = import_resolver 520 521 def TryAppend(self, line, line_num): 522 if not line.startswith('//%PDDM-IMPORT-DEFINES '): 523 return (False, False) 524 assert self.num_lines_captured == 0 525 self.Append(line) 526 return (True, False) 527 528 def BindMacroCollection(self, macro_collection): 529 if not macro_collection: 530 return 531 if self._import_resolver is None: 532 raise PDDMError('Got an IMPORT-DEFINES without a resolver (line %d):' 533 ' "%s".' % (self.first_line_num, self.first_line)) 534 import_name = self.first_line.split(' ', 1)[1].strip() 535 imported_file = self._import_resolver(import_name) 536 if imported_file is None: 537 raise PDDMError('Resolver failed to find "%s" (line %d):' 538 ' "%s".' % 539 (import_name, self.first_line_num, self.first_line)) 540 try: 541 imported_src_file = SourceFile(imported_file, self._import_resolver) 542 imported_src_file._ParseFile() 543 for section in imported_src_file._sections: 544 section.BindMacroCollection(macro_collection) 545 except PDDMError as e: 546 raise PDDMError('%s\n...while importing defines:\n' 547 ' Line %d: %s' % 548 (e.message, self.first_line_num, self.first_line)) 549 550 def _ParseFile(self): 551 self._sections = [] 552 lines = self._original_content.splitlines() 553 cur_section = None 554 for line_num, line in enumerate(lines, 1): 555 if not cur_section: 556 cur_section = self._MakeSection(line, line_num) 557 was_added, accept_more = cur_section.TryAppend(line, line_num) 558 if not was_added: 559 cur_section = self._MakeSection(line, line_num) 560 was_added, accept_more = cur_section.TryAppend(line, line_num) 561 assert was_added 562 if not accept_more: 563 cur_section = None 564 565 if cur_section: 566 cur_section.HitEOF() 567 568 def _MakeSection(self, line, line_num): 569 if not line.startswith('//%PDDM'): 570 section = self.TextSection(line_num) 571 else: 572 directive = line.split(' ', 1)[0] 573 if directive == '//%PDDM-EXPAND': 574 section = self.ExpansionSection(line_num) 575 elif directive == '//%PDDM-DEFINE': 576 section = self.DefinitionSection(line_num) 577 elif directive == '//%PDDM-IMPORT-DEFINES': 578 section = self.ImportDefinesSection(line_num, self._import_resolver) 579 else: 580 raise PDDMError('Unexpected line %d: "%s".' % (line_num, line)) 581 self._sections.append(section) 582 return section 583 584 def ProcessContent(self, strip_expansion=False): 585 """Processes the file contents.""" 586 self._ParseFile() 587 if strip_expansion: 588 # Without a collection the expansions become blank, removing them. 589 collection = None 590 else: 591 collection = MacroCollection() 592 for section in self._sections: 593 section.BindMacroCollection(collection) 594 result = '' 595 for section in self._sections: 596 result += section.text 597 self._processed_content = result 598 599 @property 600 def original_content(self): 601 return self._original_content 602 603 @property 604 def processed_content(self): 605 return self._processed_content 606 607 608def main(args): 609 usage = '%prog [OPTIONS] PATH ...' 610 description = ( 611 'Processes PDDM directives in the given paths and write them back out.' 612 ) 613 parser = optparse.OptionParser(usage=usage, description=description) 614 parser.add_option('--dry-run', 615 default=False, action='store_true', 616 help='Don\'t write back to the file(s), just report if the' 617 ' contents needs an update and exit with a value of 1.') 618 parser.add_option('--verbose', 619 default=False, action='store_true', 620 help='Reports is a file is already current.') 621 parser.add_option('--collapse', 622 default=False, action='store_true', 623 help='Removes all the generated code.') 624 opts, extra_args = parser.parse_args(args) 625 626 if not extra_args: 627 parser.error('Need at least one file to process') 628 629 result = 0 630 for a_path in extra_args: 631 if not os.path.exists(a_path): 632 sys.stderr.write('ERROR: File not found: %s\n' % a_path) 633 return 100 634 635 def _ImportResolver(name): 636 # resolve based on the file being read. 637 a_dir = os.path.dirname(a_path) 638 import_path = os.path.join(a_dir, name) 639 if not os.path.exists(import_path): 640 return None 641 return open(import_path, 'r') 642 643 with open(a_path, 'r') as f: 644 src_file = SourceFile(f, _ImportResolver) 645 646 try: 647 src_file.ProcessContent(strip_expansion=opts.collapse) 648 except PDDMError as e: 649 sys.stderr.write('ERROR: %s\n...While processing "%s"\n' % 650 (e.message, a_path)) 651 return 101 652 653 if src_file.processed_content != src_file.original_content: 654 if not opts.dry_run: 655 print('Updating for "%s".' % a_path) 656 with open(a_path, 'w') as f: 657 f.write(src_file.processed_content) 658 else: 659 # Special result to indicate things need updating. 660 print('Update needed for "%s".' % a_path) 661 result = 1 662 elif opts.verbose: 663 print('No update for "%s".' % a_path) 664 665 return result 666 667 668if __name__ == '__main__': 669 sys.exit(main(sys.argv[1:])) 670