error.py 7.32 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# error.py - error handling for DHParser
#
# Copyright 2016  by Eckhart Arnold (arnold@badw.de)
#                 Bavarian Academy of Sciences an Humanities (badw.de)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.  See the License for the specific language governing
# permissions and limitations under the License.
17

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
"""
Module ``error`` defines class Error and a few helpful functions that are
needed for error reporting of DHParser. Usually, what is of interest are
the string representations of the error objects. For example::

    from DHParser import compile_source, has_errors

    result, errors, ast = compile_source(source, preprocessor, grammar,
                                         transformer, compiler)
    if errors:
        for error in errors:
            print(error)

        if has_errors(errors):
            print("There have been fatal errors!")
            sys.exit(1)
        else:
            print("There have been warnings, but no errors.")
"""

di68kap's avatar
di68kap committed
38

39
40
import bisect

41
from DHParser.preprocess import SourceMapFunc
42
from DHParser.stringview import StringView
eckhart's avatar
eckhart committed
43
from DHParser.toolkit import typing
eckhart's avatar
eckhart committed
44
from typing import Iterable, Iterator, Union, Tuple, List
45

Eckhart Arnold's avatar
Eckhart Arnold committed
46
47
__all__ = ('ErrorCode',
           'Error',
48
49
50
51
52
           'is_error',
           'is_warning',
           'has_errors',
           'only_errors',
           'linebreaks',
53
           'line_col',
eckhart's avatar
eckhart committed
54
           'adjust_error_locations')
55
56


Eckhart Arnold's avatar
Eckhart Arnold committed
57
58
59
60
class ErrorCode(int):
    pass


61
class Error:
eckhart's avatar
eckhart committed
62
    __slots__ = ['message', 'level', 'code', '_pos', 'orig_pos', 'line', 'column', '_node_keep']
63

64
65
    # error levels

Eckhart Arnold's avatar
Eckhart Arnold committed
66
67
    NO_ERROR  = ErrorCode(0)
    MESSAGE   = ErrorCode(1)
68
    WARNING   = ErrorCode(100)
Eckhart Arnold's avatar
Eckhart Arnold committed
69
    ERROR     = ErrorCode(1000)
70
71
    HIGHEST   = ERROR

72
73
    # warning codes

di68kap's avatar
di68kap committed
74
75
    REDEFINED_DIRECTIVE_WARNING              = ErrorCode(101)
    REDECLARED_TOKEN_WARNING                 = ErrorCode(102)
76

Eckhart Arnold's avatar
Eckhart Arnold committed
77
    UNDEFINED_SYMBOL_IN_TRANSFORMATION_TABLE = ErrorCode(601)
78

79
80
    # error codes

di68kap's avatar
di68kap committed
81
82
    MANDATORY_CONTINUATION                   = ErrorCode(1001)
    PARSER_DID_NOT_MATCH                     = ErrorCode(1002)
di68kap's avatar
di68kap committed
83
84
85
    PARSER_LOOKAHEAD_MATCH_ONLY              = ErrorCode(1003)
    PARSER_STOPPED_BEFORE_END                = ErrorCode(1004)
    CAPTURE_STACK_NOT_EMPTY                  = ErrorCode(1005)
86

Eckhart Arnold's avatar
Eckhart Arnold committed
87
88
89
90
91
    def __init__(self, message: str, pos, code: ErrorCode = ERROR,
                 orig_pos: int = -1, line: int = -1, column: int = -1) -> None:
        assert isinstance(code, ErrorCode)
        assert not isinstance(pos, ErrorCode)
        assert pos >= 0
92
        assert code >= 0
Eckhart Arnold's avatar
Eckhart Arnold committed
93
        self.message = message
eckhart's avatar
eckhart committed
94
        self._pos = pos
Eckhart Arnold's avatar
Eckhart Arnold committed
95
        self.code = code
96
        self.orig_pos = orig_pos
97
98
        self.line = line
        self.column = column
99
100
101
102

    def __str__(self):
        prefix = ''
        if self.line > 0:
di68kap's avatar
di68kap committed
103
104
            prefix = "%i:%i: " % (max(self.line, 0), max(self.column, 0))
        return prefix + "%s: %s" % (self.severity, self.message)
105

106
    def __repr__(self):
107
108
        return 'Error("%s", %s, %i, %i, %i, %i)' \
               % (self.message, repr(self.code), self.pos, self.orig_pos, self.line, self.column)
109

eckhart's avatar
eckhart committed
110
111
112
113
    @property
    def pos(self):
        return self._pos

114
    @property
di68kap's avatar
di68kap committed
115
    def severity(self):
116
        """Returns a string representation of the error level, e.g. "warning"."""
117
        return "Warning" if is_warning(self.code) else "Error"
118

119
120
121
122
123
124
125
    def visualize(self, document: str) -> str:
        """Shows the line of the document and the position where the error
        occurred."""
        start = document.rfind('\n', 0, self.pos) + 1
        stop = document.find('\n', self.pos)
        return document[start:stop] + '\n' + ' ' * (self.pos - start) + '^\n'

126

127
def is_warning(code: int) -> bool:
eckhart's avatar
eckhart committed
128
    """Returns True, if error is merely a warning."""
129
    return code < Error.ERROR
130
131


132
def is_error(code: int) -> bool:
eckhart's avatar
eckhart committed
133
    """Returns True, if error is an error, not just a warning."""
134
    return code >= Error.ERROR
135
136


eckhart's avatar
eckhart committed
137
def has_errors(messages: Iterable[Error], level: int = Error.ERROR) -> bool:
138
139
140
141
142
    """
    Returns True, if at least one entry in `messages` has at
    least the given error `level`.
    """
    for err_obj in messages:
143
        if err_obj.code >= level:
144
145
146
147
            return True
    return False


eckhart's avatar
eckhart committed
148
def only_errors(messages: Iterable[Error], level: int = Error.ERROR) -> Iterator[Error]:
149
150
151
152
    """
    Returns an Iterator that yields only those messages that have
    at least the given error level.
    """
153
    return (err for err in messages if err.code >= level)
154
155


156
157
158
159
160
161
162
#######################################################################
#
# Setting of line, column and position properties of error messages.
#
#######################################################################


eckhart's avatar
eckhart committed
163
def linebreaks(text: Union[StringView, str]) -> List[int]:
eckhart's avatar
eckhart committed
164
165
166
167
    """
    Returns a list of indices all line breaks in the text.
    """
    lbr = [-1]
168
169
    i = text.find('\n', 0)
    while i >= 0:
eckhart's avatar
eckhart committed
170
        lbr.append(i)
171
        i = text.find('\n', i + 1)
eckhart's avatar
eckhart committed
172
173
    lbr.append(len(text))
    return lbr
174
175


176
def line_col(lbreaks: List[int], pos: int) -> Tuple[int, int]:
eckhart's avatar
eckhart committed
177
178
    """
    Returns the position within a text as (line, column)-tuple based
179
180
    on a list of all line breaks, including -1 and EOF.
    """
181
182
    if not lbreaks and pos >= 0:
        return 0, pos
183
184
185
186
187
188
    if pos < 0 or pos > lbreaks[-1]:  # one character behind EOF is still an allowed position!
        raise ValueError('Position %i outside text of length %s !' % (pos, lbreaks[-1]))
    line = bisect.bisect_left(lbreaks, pos)
    column = pos - lbreaks[line - 1]
    return line, column

189

190
191
192
# def line_col(text: Union[StringView, str], pos: int) -> Tuple[int, int]:
#     """
#     Returns the position within a text as (line, column)-tuple.
193
#     """
194
#     if pos < 0 or add_pos > len(text):  # one character behind EOF is still an allowed position!
195
196
#         raise ValueError('Position %i outside text of length %s !' % (pos, len(text)))
#     line = text.count("\n", 0, pos) + 1
197
#     column = pos - text.rfind("\n", 0, add_pos)
198
199
200
#     return line, column


eckhart's avatar
eckhart committed
201
202
def adjust_error_locations(errors: List[Error],
                           original_text: Union[StringView, str],
eckhart's avatar
eckhart committed
203
                           source_mapping: SourceMapFunc = lambda i: i) -> List[Error]:
204
205
206
207
208
209
210
211
212
    """Adds (or adjusts) line and column numbers of error messages in place.

    Args:
        errors:  The list of errors as returned by the method
            ``collect_errors()`` of a Node object
        original_text:  The source text on which the errors occurred.
            (Needed in order to determine the line and column numbers.)
        source_mapping:  A function that maps error positions to their
            positions in the original source file.
213

214
215
    Returns:
        The list of errors. (Returning the list of errors is just syntactical
216
        sugar. Be aware that the line, col and orig_pos attr have been
217
218
219
220
221
222
223
224
        changed in place.)
    """
    line_breaks = linebreaks(original_text)
    for err in errors:
        assert err.pos >= 0
        err.orig_pos = source_mapping(err.pos)
        err.line, err.column = line_col(line_breaks, err.orig_pos)
    return errors