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