github.com/google/grumpy@v0.0.0-20171122020858-3ec87959189c/third_party/pythonparser/diagnostic.py (about)

     1  """
     2  The :mod:`Diagnostic` module concerns itself with processing
     3  and presentation of diagnostic messages.
     4  """
     5  
     6  from __future__ import absolute_import, division, print_function, unicode_literals
     7  from functools import reduce
     8  from contextlib import contextmanager
     9  import sys, re
    10  
    11  class Diagnostic:
    12      """
    13      A diagnostic message highlighting one or more locations
    14      in a single source buffer.
    15  
    16      :ivar level: (one of ``LEVELS``) severity level
    17      :ivar reason: (format string) diagnostic message
    18      :ivar arguments: (dictionary) substitutions for ``reason``
    19      :ivar location: (:class:`pythonparser.source.Range`) most specific
    20          location of the problem
    21      :ivar highlights: (list of :class:`pythonparser.source.Range`)
    22          secondary locations related to the problem that are
    23          likely to be on the same line
    24      :ivar notes: (list of :class:`Diagnostic`)
    25          secondary diagnostics highlighting relevant source
    26          locations that are unlikely to be on the same line
    27      """
    28  
    29      LEVELS = ["note", "warning", "error", "fatal"]
    30      """
    31      Available diagnostic levels:
    32          * ``fatal`` indicates an unrecoverable error.
    33          * ``error`` indicates an error that leaves a possibility of
    34            processing more code, e.g. a recoverable parsing error.
    35          * ``warning`` indicates a potential problem.
    36          * ``note`` level diagnostics do not appear by itself,
    37            but are attached to other diagnostics to refer to
    38            and describe secondary source locations.
    39      """
    40  
    41      def __init__(self, level, reason, arguments, location,
    42                   highlights=None, notes=None):
    43          if level not in self.LEVELS:
    44              raise ValueError("level must be one of Diagnostic.LEVELS")
    45  
    46          if highlights is None:
    47              highlights = []
    48          if notes is None:
    49              notes = []
    50  
    51          if len(set(map(lambda x: x.source_buffer,
    52                         [location] + highlights))) > 1:
    53              raise ValueError("location and highlights must refer to the same source buffer")
    54  
    55          self.level, self.reason, self.arguments = \
    56              level, reason, arguments
    57          self.location, self.highlights, self.notes = \
    58              location, highlights, notes
    59  
    60      def message(self):
    61          """
    62          Returns the formatted message.
    63          """
    64          return self.reason.format(**self.arguments)
    65  
    66      def render(self, only_line=False, colored=False):
    67          """
    68          Returns the human-readable location of the diagnostic in the source,
    69          the formatted message, the source line corresponding
    70          to ``location`` and a line emphasizing the problematic
    71          locations in the source line using ASCII art, as a list of lines.
    72          Appends the result of calling :meth:`render` on ``notes``, if any.
    73  
    74          For example: ::
    75  
    76              <input>:1:8-9: error: cannot add integer and string
    77              x + (1 + "a")
    78                   ~ ^ ~~~
    79  
    80          :param only_line: (bool) If true, only print line number, not line and column range
    81          """
    82          source_line = self.location.source_line().rstrip("\n")
    83          highlight_line = bytearray(re.sub(r"[^\t]", " ", source_line), "utf-8")
    84  
    85          for hilight in self.highlights:
    86              if hilight.line() == self.location.line():
    87                  lft, rgt = hilight.column_range()
    88                  highlight_line[lft:rgt] = bytearray("~", "utf-8") * (rgt - lft)
    89  
    90          lft, rgt = self.location.column_range()
    91          if rgt == lft: # Expand zero-length ranges to one ^
    92              rgt = lft + 1
    93          highlight_line[lft:rgt] = bytearray("^", "utf-8") * (rgt - lft)
    94  
    95          if only_line:
    96              location = "%s:%s" % (self.location.source_buffer.name, self.location.line())
    97          else:
    98              location = str(self.location)
    99  
   100          notes = list(self.notes)
   101          if self.level != "note":
   102              expanded_location = self.location.expanded_from
   103              while expanded_location is not None:
   104                  notes.insert(0, Diagnostic("note",
   105                      "expanded from here", {},
   106                      self.location.expanded_from))
   107                  expanded_location = expanded_location.expanded_from
   108  
   109          rendered_notes = reduce(list.__add__, [note.render(only_line, colored)
   110                                                 for note in notes], [])
   111          if colored:
   112              if self.level in ("error", "fatal"):
   113                  level_color = 31 # red
   114              elif self.level == "warning":
   115                  level_color = 35 # magenta
   116              else: # level == "note"
   117                  level_color = 30 # gray
   118              return [
   119                  "\x1b[1;37m{}: \x1b[{}m{}:\x1b[37m {}\x1b[0m".
   120                      format(location, level_color, self.level, self.message()),
   121                  source_line,
   122                  "\x1b[1;32m{}\x1b[0m".format(highlight_line.decode("utf-8"))
   123              ] + rendered_notes
   124          else:
   125              return [
   126                  "{}: {}: {}".format(location, self.level, self.message()),
   127                  source_line,
   128                  highlight_line.decode("utf-8")
   129              ] + rendered_notes
   130  
   131  
   132  class Error(Exception):
   133      """
   134      :class:`Error` is an exception which carries a :class:`Diagnostic`.
   135  
   136      :ivar diagnostic: (:class:`Diagnostic`) the diagnostic
   137      """
   138      def __init__(self, diagnostic):
   139          self.diagnostic = diagnostic
   140  
   141      def __str__(self):
   142          return "\n".join(self.diagnostic.render())
   143  
   144  class Engine:
   145      """
   146      :class:`Engine` is a single point through which diagnostics from
   147      lexer, parser and any AST consumer are dispatched.
   148  
   149      :ivar all_errors_are_fatal: if true, an exception is raised not only
   150          for ``fatal`` diagnostic level, but also ``error``
   151      """
   152      def __init__(self, all_errors_are_fatal=False):
   153          self.all_errors_are_fatal = all_errors_are_fatal
   154          self._appended_notes = []
   155  
   156      def process(self, diagnostic):
   157          """
   158          The default implementation of :meth:`process` renders non-fatal
   159          diagnostics to ``sys.stderr``, and raises fatal ones as a :class:`Error`.
   160          """
   161          diagnostic.notes += self._appended_notes
   162          self.render_diagnostic(diagnostic)
   163          if diagnostic.level == "fatal" or \
   164                  (self.all_errors_are_fatal and diagnostic.level == "error"):
   165              raise Error(diagnostic)
   166  
   167      @contextmanager
   168      def context(self, *notes):
   169          """
   170          A context manager that appends ``note`` to every diagnostic processed by
   171          this engine.
   172          """
   173          self._appended_notes += notes
   174          yield
   175          del self._appended_notes[-len(notes):]
   176  
   177      def render_diagnostic(self, diagnostic):
   178          sys.stderr.write("\n".join(diagnostic.render()) + "\n")