• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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