Skip to content

Commit

Permalink
Add diagnostic module.
Browse files Browse the repository at this point in the history
whitequark committed Mar 31, 2015
1 parent baa39c4 commit 28671ca
Showing 4 changed files with 122 additions and 11 deletions.
7 changes: 7 additions & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
@@ -13,6 +13,13 @@ for every token.
:members:
:show-inheritance:

:mod:`diagnostic` Module
--------------------

.. automodule:: pyparser.diagnostic
:members:
:show-inheritance:

:mod:`lexer` Module
--------------------

77 changes: 77 additions & 0 deletions pyparser/diagnostic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""
The :mod:`Diagnostic` module concerns itself with processing
and presentation of diagnostic messages.
"""

class Diagnostic:
"""
A diagnostic message highlighting one or more locations
in a single source buffer.
:ivar level: (one of ``LEVELS``) severity level
:ivar reason: (format string) diagnostic message
:ivar arguments: (dictionary) substitutions for ``reason``
:ivar location: (:class:`pyparser.source.Range`) most specific
location of the problem
:ivar highlights: (list of :class:`pyparser.source.Range`)
secondary locations related to the problem that are
likely to be on the same line
:ivar notes: (list of :class:`Diagnostic`)
secondary diagnostics highlighting relevant source
locations that are unlikely to be on the same line
"""

LEVELS = ['note', 'warning', 'error', 'fatal']
"""
Available diagnostic levels:
* ``fatal`` indicates an unrecoverable error.
* ``error`` indicates an error that leaves a possibility of
processing more code, e.g. a recoverable parsing error.
* ``warning`` indicates a potential problem.
* ``note`` level diagnostics do not appear by itself,
but are attached to other diagnostics to refer to
and describe secondary source locations.
"""

def __init__(self, level, reason, arguments, location,
highlights=[], notes=[]):
if level not in self.LEVELS:
raise ValueError, "level must be one of Diagnostic.LEVELS"

This comment has been minimized.

Copy link
@sbourdeauducq

sbourdeauducq Apr 1, 2015

Member

raise ValueError("level must be one of Diagnostic.LEVELS")


if len(set(map(lambda x: x.source_buffer,
[location] + highlights))) > 1:
raise ValueError, "location and highlights must refer to the same source buffer"

self.level, self.reason, self.arguments = \
level, reason, arguments
self.location, self.highlights, self.notes = \
location, highlights, notes

def message(self):
"""
Returns the formatted message.
"""
return self.reason.format(**self.arguments)

def render(self):
"""
Returns the human-readable location of the diagnostic in the source,
the formatted message, the source line corresponding
to ``location`` and a line emphasizing the problematic
locations in the source line using ASCII art, as a list of lines.
"""
source_line = self.location.source_line().rstrip(u"\n")
highlight_line = bytearray(' ') * len(source_line)

for hilight in self.highlights:
lft, rgt = hilight.column_range()
highlight_line[lft:rgt] = bytearray('~') * hilight.size()

lft, rgt = self.location.column_range()
highlight_line[lft:rgt] = bytearray('^') * self.location.size()

return [
"%s: %s: %s" % (str(self.location), self.level, self.message()),
source_line,
unicode(highlight_line)
]
27 changes: 27 additions & 0 deletions pyparser/test/test_diagnostic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import unittest
import pyparser.source as source
import pyparser.diagnostic as diagnostic

class DiagnosticTestCase(unittest.TestCase):

def setUp(self):
self.buffer = source.Buffer(u'x + (1 + "a")\n')

def test_message(self):
diag = diagnostic.Diagnostic(
'error', u"{x} doesn't work", {'x': 'everything'},
source.Range(self.buffer, 0, 0))
self.assertEqual(u"everything doesn't work", diag.message())

def test_render(self):
diag = diagnostic.Diagnostic(
'error', u"cannot add {lft} and {rgt}",

This comment has been minimized.

Copy link
@sbourdeauducq

sbourdeauducq Apr 1, 2015

Member

Can you use " for all strings?

This comment has been minimized.

Copy link
@whitequark

whitequark Apr 1, 2015

Author Contributor

Is there some specific guideline on that?

This comment has been minimized.

Copy link
@sbourdeauducq

sbourdeauducq Apr 1, 2015

Member

ARTIQ/Migen use " everywhere, and mixing the two is a bit messy.

{'lft': u'integer', 'rgt': u'string'},
source.Range(self.buffer, 7, 8),
[source.Range(self.buffer, 5, 6),
source.Range(self.buffer, 9, 12)])
self.assertEqual(
[u'<input>:1:8: error: cannot add integer and string',
u'x + (1 + "a")',
u' ~ ^ ~~~ '],
diag.render())
22 changes: 11 additions & 11 deletions pyparser/test/test_source.py
Original file line number Diff line number Diff line change
@@ -4,16 +4,16 @@
class BufferTestCase(unittest.TestCase):

def setUp(self):
self.buffer = source.Buffer("line one\nline two\n\nline four")
self.buffer = source.Buffer(u"line one\nline two\n\nline four")

def test_repr(self):
self.assertEqual(r'Buffer("<input>")', repr(self.buffer))
self.assertEqual(ur'Buffer("<input>")', repr(self.buffer))

def test_source_line(self):
self.assertEqual("line one\n", self.buffer.source_line(1))
self.assertEqual("line two\n", self.buffer.source_line(2))
self.assertEqual("\n", self.buffer.source_line(3))
self.assertEqual("line four", self.buffer.source_line(4))
self.assertEqual(u"line one\n", self.buffer.source_line(1))

This comment has been minimized.

Copy link
@sbourdeauducq

sbourdeauducq Apr 1, 2015

Member

u is not necessary for Python 3.

This comment has been minimized.

Copy link
@whitequark

whitequark Apr 1, 2015

Author Contributor

I know. I aim to support and parse anything from 2.6 to 3.4, mainly because it's not a significant problem if you do it from scratch.

This comment has been minimized.

Copy link
@sbourdeauducq

sbourdeauducq Apr 1, 2015

Member

I'm not interested in Python 2, but if you say that supporting it is not a problem...

This comment has been minimized.

Copy link
@whitequark

whitequark Apr 1, 2015

Author Contributor

I know, yeah. I'm testing on 2 and 3 in development, so far it hasn't been an issue. I think this makes it much more useful for other people.

This comment has been minimized.

Copy link
@jordens

jordens Apr 2, 2015

Member

use from __future__ import unicode_literals then (and the usual other set of future imports that cover 2/3 interop)

This comment has been minimized.

Copy link
@whitequark

whitequark Apr 2, 2015

Author Contributor

Which ones are usually used?

This comment has been minimized.

Copy link
@jordens

jordens Apr 2, 2015

Member

from __future__ import (absolute_import, division, print_function, unicode_literals) covers pretty much everything. https://wiki.python.org/moin/PortingPythonToPy3k

self.assertEqual(u"line two\n", self.buffer.source_line(2))
self.assertEqual(u"\n", self.buffer.source_line(3))
self.assertEqual(u"line four", self.buffer.source_line(4))
self.assertRaises(IndexError, lambda: self.buffer.source_line(0))

def test_decompose_position(self):
@@ -26,13 +26,13 @@ def test_decompose_position(self):
class RangeTestCase(unittest.TestCase):

def setUp(self):
self.buffer = source.Buffer("line one\nline two\n\nline four")
self.buffer = source.Buffer(u"line one\nline two\n\nline four")

def range(self, lft, rgt):
return source.Range(self.buffer, lft, rgt)

def test_repr(self):
self.assertEqual(r'Range("<input>", 0, 2)',
self.assertEqual(ur'Range("<input>", 0, 2)',
repr(self.range(0, 2)))

def test_begin(self):
@@ -74,13 +74,13 @@ def test_join(self):
source.Range(source.Buffer(""), 0, 0)))

def test_source(self):
self.assertEqual("one", self.range(5, 8).source())
self.assertEqual(u"one", self.range(5, 8).source())

def test_source_line(self):
self.assertEqual("line two\n", self.range(9, 9).source_line())
self.assertEqual(u"line two\n", self.range(9, 9).source_line())

def test___str__(self):
self.assertEqual("<input>:2:1", str(self.range(9, 9)))
self.assertEqual(u"<input>:2:1", str(self.range(9, 9)))

def test___ne__(self):
self.assertTrue(self.range(0,0) != self.range(0,1))

0 comments on commit 28671ca

Please sign in to comment.