2.12.2021, 9:00 - 11:00: Due to updates GitLab may be unavailable for some minutes between 09:00 and 11:00.

error.py 9.19 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:
62
    __slots__ = ['message', 'code', '_pos', 'orig_pos', 'line', 'column']
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

74
    REDECLARED_TOKEN_WARNING                 = ErrorCode(120)
75
    UNUSED_ERROR_HANDLING_WARNING            = ErrorCode(130)
76
    LEFT_RECURSION_WARNING                   = ErrorCode(140)
77

78
    UNDEFINED_SYMBOL_IN_TRANSTABLE_WARNING   = ErrorCode(610)
79

80
81
    # error codes

82
83
84
85
86
87
88
    MANDATORY_CONTINUATION                   = ErrorCode(1010)
    MANDATORY_CONTINUATION_AT_EOF            = ErrorCode(1015)
    PARSER_DID_NOT_MATCH                     = ErrorCode(1020)
    PARSER_LOOKAHEAD_MATCH_ONLY              = ErrorCode(1030)
    PARSER_STOPPED_BEFORE_END                = ErrorCode(1040)
    CAPTURE_STACK_NOT_EMPTY                  = ErrorCode(1050)
    MALFORMED_ERROR_STRING                   = ErrorCode(1060)
89
    AMBIGUOUS_ERROR_HANDLING                 = ErrorCode(1070)
90
    REDEFINED_DIRECTIVE                      = ErrorCode(1080)
91

Eckhart Arnold's avatar
Eckhart Arnold committed
92
93
94
95
    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)
96
        assert code >= 0
di68kap's avatar
di68kap committed
97
98
99
100
101
102
        self.message = message    # type: str
        self._pos = pos           # type: int
        self.code = code          # type: ErrorCode
        self.orig_pos = orig_pos  # type: int
        self.line = line          # type: int
        self.column = column      # type: int
103
104
105
106

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

110
    def __repr__(self):
111
112
        return 'Error("%s", %s, %i, %i, %i, %i)' \
               % (self.message, repr(self.code), self.pos, self.orig_pos, self.line, self.column)
113

eckhart's avatar
eckhart committed
114
115
116
117
    @property
    def pos(self):
        return self._pos

118
119
120
121
122
123
    @pos.setter
    def pos(self, value: int):
        self._pos = value
        # reset line and column values, because they might now not be valid any more
        self.line, self.column = -1, -1

124
    @property
di68kap's avatar
di68kap committed
125
    def severity(self):
126
        """Returns a string representation of the error level, e.g. "warning"."""
127
        return "Warning" if is_warning(self.code) else "Error"
128

129
130
131
132
133
134
135
    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'

136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
    def to_json_obj(self) -> Dict:
        """Serialize Error object as json-object."""
        return { '__class__': 'DHParser.Error',
                 'data': [self.message, self._pos, self.code, self.orig_pos,
                          self.line, self.column] }

    @static
    def from_json_obj(self, json_obj: Dict) -> Error:
        """Convert a json object representing an Error object back into an
        Error object. Raises a ValueError, if json_obj does not represent
        an error object"""
        if json_obj.get('__class__', '') != 'DHParser.Error':
            raise ValueError('JSON object: ' + str(json_obj) +
                             ' does not represent an Error object.')
        return Error(*json_obj['data'])

152

153
def is_warning(code: int) -> bool:
eckhart's avatar
eckhart committed
154
    """Returns True, if error is merely a warning."""
155
    return code < Error.ERROR
156
157


158
def is_error(code: int) -> bool:
eckhart's avatar
eckhart committed
159
    """Returns True, if error is an error, not just a warning."""
160
    return code >= Error.ERROR
161
162


163
164
165
166
167
168
169
170
171
172
173
174
# def Warning(message: str, pos, code: ErrorCode = Error.WARNING,
#             orig_pos: int = -1, line: int = -1, column: int = -1) -> Error:
#     """
#     Syntactic sugar for creating Error-objects that contain only a warning.
#     Raises a ValueError if `code` is not within the range for warnings.
#     """
#     if not is_warning(code):
#         raise ValueError("Tried to create a warning with a error code {}. "
#                          "Warning codes must be smaller than {}".format(code, Error.ERROR))
#     return Error(message, pos, code, orig_pos, line, column)


eckhart's avatar
eckhart committed
175
def has_errors(messages: Iterable[Error], level: int = Error.ERROR) -> bool:
176
177
178
179
180
    """
    Returns True, if at least one entry in `messages` has at
    least the given error `level`.
    """
    for err_obj in messages:
181
        if err_obj.code >= level:
182
183
184
185
            return True
    return False


eckhart's avatar
eckhart committed
186
def only_errors(messages: Iterable[Error], level: int = Error.ERROR) -> Iterator[Error]:
187
188
189
190
    """
    Returns an Iterator that yields only those messages that have
    at least the given error level.
    """
191
    return (err for err in messages if err.code >= level)
192
193


194
195
196
197
198
199
200
#######################################################################
#
# Setting of line, column and position properties of error messages.
#
#######################################################################


eckhart's avatar
eckhart committed
201
def linebreaks(text: Union[StringView, str]) -> List[int]:
eckhart's avatar
eckhart committed
202
203
204
205
    """
    Returns a list of indices all line breaks in the text.
    """
    lbr = [-1]
206
207
    i = text.find('\n', 0)
    while i >= 0:
eckhart's avatar
eckhart committed
208
        lbr.append(i)
209
        i = text.find('\n', i + 1)
eckhart's avatar
eckhart committed
210
211
    lbr.append(len(text))
    return lbr
212
213


214
def line_col(lbreaks: List[int], pos: int) -> Tuple[int, int]:
eckhart's avatar
eckhart committed
215
216
    """
    Returns the position within a text as (line, column)-tuple based
217
218
    on a list of all line breaks, including -1 and EOF.
    """
219
220
    if not lbreaks and pos >= 0:
        return 0, pos
221
222
223
224
225
226
    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

227

228
229
230
# def line_col(text: Union[StringView, str], pos: int) -> Tuple[int, int]:
#     """
#     Returns the position within a text as (line, column)-tuple.
231
#     """
232
#     if pos < 0 or add_pos > len(text):  # one character behind EOF is still an allowed position!
233
234
#         raise ValueError('Position %i outside text of length %s !' % (pos, len(text)))
#     line = text.count("\n", 0, pos) + 1
235
#     column = pos - text.rfind("\n", 0, add_pos)
236
237
238
#     return line, column


eckhart's avatar
eckhart committed
239
240
def adjust_error_locations(errors: List[Error],
                           original_text: Union[StringView, str],
eckhart's avatar
eckhart committed
241
                           source_mapping: SourceMapFunc = lambda i: i) -> List[Error]:
242
243
244
245
    """Adds (or adjusts) line and column numbers of error messages in place.

    Args:
        errors:  The list of errors as returned by the method
246
            ``errors()`` of a Node object
247
248
249
250
        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.
251

252
253
    Returns:
        The list of errors. (Returning the list of errors is just syntactical
254
        sugar. Be aware that the line, col and orig_pos attr have been
255
256
257
258
259
260
261
262
        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