Skip to content

Commit 28671ca

Browse files
author
whitequark
committedMar 31, 2015
Add diagnostic module.
1 parent baa39c4 commit 28671ca

File tree

4 files changed

+122
-11
lines changed

4 files changed

+122
-11
lines changed
 

Diff for: ‎doc/index.rst

+7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ for every token.
1313
:members:
1414
:show-inheritance:
1515

16+
:mod:`diagnostic` Module
17+
--------------------
18+
19+
.. automodule:: pyparser.diagnostic
20+
:members:
21+
:show-inheritance:
22+
1623
:mod:`lexer` Module
1724
--------------------
1825

Diff for: ‎pyparser/diagnostic.py

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""
2+
The :mod:`Diagnostic` module concerns itself with processing
3+
and presentation of diagnostic messages.
4+
"""
5+
6+
class Diagnostic:
7+
"""
8+
A diagnostic message highlighting one or more locations
9+
in a single source buffer.
10+
11+
:ivar level: (one of ``LEVELS``) severity level
12+
:ivar reason: (format string) diagnostic message
13+
:ivar arguments: (dictionary) substitutions for ``reason``
14+
:ivar location: (:class:`pyparser.source.Range`) most specific
15+
location of the problem
16+
:ivar highlights: (list of :class:`pyparser.source.Range`)
17+
secondary locations related to the problem that are
18+
likely to be on the same line
19+
:ivar notes: (list of :class:`Diagnostic`)
20+
secondary diagnostics highlighting relevant source
21+
locations that are unlikely to be on the same line
22+
"""
23+
24+
LEVELS = ['note', 'warning', 'error', 'fatal']
25+
"""
26+
Available diagnostic levels:
27+
* ``fatal`` indicates an unrecoverable error.
28+
* ``error`` indicates an error that leaves a possibility of
29+
processing more code, e.g. a recoverable parsing error.
30+
* ``warning`` indicates a potential problem.
31+
* ``note`` level diagnostics do not appear by itself,
32+
but are attached to other diagnostics to refer to
33+
and describe secondary source locations.
34+
"""
35+
36+
def __init__(self, level, reason, arguments, location,
37+
highlights=[], notes=[]):
38+
if level not in self.LEVELS:
39+
raise ValueError, "level must be one of Diagnostic.LEVELS"
Has a conversation. Original line has a conversation.
40+
41+
if len(set(map(lambda x: x.source_buffer,
42+
[location] + highlights))) > 1:
43+
raise ValueError, "location and highlights must refer to the same source buffer"
44+
45+
self.level, self.reason, self.arguments = \
46+
level, reason, arguments
47+
self.location, self.highlights, self.notes = \
48+
location, highlights, notes
49+
50+
def message(self):
51+
"""
52+
Returns the formatted message.
53+
"""
54+
return self.reason.format(**self.arguments)
55+
56+
def render(self):
57+
"""
58+
Returns the human-readable location of the diagnostic in the source,
59+
the formatted message, the source line corresponding
60+
to ``location`` and a line emphasizing the problematic
61+
locations in the source line using ASCII art, as a list of lines.
62+
"""
63+
source_line = self.location.source_line().rstrip(u"\n")
64+
highlight_line = bytearray(' ') * len(source_line)
65+
66+
for hilight in self.highlights:
67+
lft, rgt = hilight.column_range()
68+
highlight_line[lft:rgt] = bytearray('~') * hilight.size()
69+
70+
lft, rgt = self.location.column_range()
71+
highlight_line[lft:rgt] = bytearray('^') * self.location.size()
72+
73+
return [
74+
"%s: %s: %s" % (str(self.location), self.level, self.message()),
75+
source_line,
76+
unicode(highlight_line)
77+
]

Diff for: ‎pyparser/test/test_diagnostic.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import unittest
2+
import pyparser.source as source
3+
import pyparser.diagnostic as diagnostic
4+
5+
class DiagnosticTestCase(unittest.TestCase):
6+
7+
def setUp(self):
8+
self.buffer = source.Buffer(u'x + (1 + "a")\n')
9+
10+
def test_message(self):
11+
diag = diagnostic.Diagnostic(
12+
'error', u"{x} doesn't work", {'x': 'everything'},
13+
source.Range(self.buffer, 0, 0))
14+
self.assertEqual(u"everything doesn't work", diag.message())
15+
16+
def test_render(self):
17+
diag = diagnostic.Diagnostic(
18+
'error', u"cannot add {lft} and {rgt}",
Has conversations. Original line has conversations.
19+
{'lft': u'integer', 'rgt': u'string'},
20+
source.Range(self.buffer, 7, 8),
21+
[source.Range(self.buffer, 5, 6),
22+
source.Range(self.buffer, 9, 12)])
23+
self.assertEqual(
24+
[u'<input>:1:8: error: cannot add integer and string',
25+
u'x + (1 + "a")',
26+
u' ~ ^ ~~~ '],
27+
diag.render())

Diff for: ‎pyparser/test/test_source.py

+11-11
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44
class BufferTestCase(unittest.TestCase):
55

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

99
def test_repr(self):
10-
self.assertEqual(r'Buffer("<input>")', repr(self.buffer))
10+
self.assertEqual(ur'Buffer("<input>")', repr(self.buffer))
1111

1212
def test_source_line(self):
13-
self.assertEqual("line one\n", self.buffer.source_line(1))
14-
self.assertEqual("line two\n", self.buffer.source_line(2))
15-
self.assertEqual("\n", self.buffer.source_line(3))
16-
self.assertEqual("line four", self.buffer.source_line(4))
13+
self.assertEqual(u"line one\n", self.buffer.source_line(1))
Has conversations. Original line has conversations.
14+
self.assertEqual(u"line two\n", self.buffer.source_line(2))
15+
self.assertEqual(u"\n", self.buffer.source_line(3))
16+
self.assertEqual(u"line four", self.buffer.source_line(4))
1717
self.assertRaises(IndexError, lambda: self.buffer.source_line(0))
1818

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

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

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

3434
def test_repr(self):
35-
self.assertEqual(r'Range("<input>", 0, 2)',
35+
self.assertEqual(ur'Range("<input>", 0, 2)',
3636
repr(self.range(0, 2)))
3737

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

7676
def test_source(self):
77-
self.assertEqual("one", self.range(5, 8).source())
77+
self.assertEqual(u"one", self.range(5, 8).source())
7878

7979
def test_source_line(self):
80-
self.assertEqual("line two\n", self.range(9, 9).source_line())
80+
self.assertEqual(u"line two\n", self.range(9, 9).source_line())
8181

8282
def test___str__(self):
83-
self.assertEqual("<input>:2:1", str(self.range(9, 9)))
83+
self.assertEqual(u"<input>:2:1", str(self.range(9, 9)))
8484

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

0 commit comments

Comments
 (0)
Please sign in to comment.