error.py 7.42 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, Optional
45
46
47
48
49
50
51

__all__ = ('Error',
           'is_error',
           'is_warning',
           'has_errors',
           'only_errors',
           'linebreaks',
52
           'line_col',
eckhart's avatar
eckhart committed
53
           'adjust_error_locations')
54
55
56


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

59
60
    # error levels

61
    NO_ERROR  = 0
di68kap's avatar
di68kap committed
62
63
    MESSAGE   = 1
    WARNING   = 10
64
65
66
    ERROR     = 1000
    HIGHEST   = ERROR

67
68
69
    # warning codes

    REDEFINED_DIRECTIVE_WARNING = 101
70
    REDECLARED_TOKEN_WARNING = 102
71

72
73
    UNDEFINED_SYMBOL_IN_TRANSFORMATION_TABLE = 601

74
75
76
77
    # error codes

    MANDATORY_CONTINUATION = 1001

78
    def __init__(self, message: str, code: int = ERROR, pos: int = -1,
eckhart's avatar
eckhart committed
79
80
                 orig_pos: int = -1, line: int = -1, column: int = -1,
                 node: Optional['Node'] = None) -> None:
81
        self.message = message
82
        assert code >= 0
83
        self.code = code
eckhart's avatar
eckhart committed
84
        self._pos = pos
85
        self.orig_pos = orig_pos
86
87
        self.line = line
        self.column = column
88
        if node is not None and node._pos >= 0:
eckhart's avatar
eckhart committed
89
90
            assert self._pos < 0 or self._pos == node._pos
            self._pos = node._pos
eckhart's avatar
eckhart committed
91
            self._node_keep = None  # if node is not needed, if pos has been set
92
        else:
eckhart's avatar
eckhart committed
93
            self._node_keep = node  # redundant: consider removing, see RootNode.collect_errors
94
95
96
97

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

101
    def __repr__(self):
102
103
        return 'Error("%s", %s, %i, %i, %i, %i)' \
               % (self.message, repr(self.code), self.pos, self.orig_pos, self.line, self.column)
104

eckhart's avatar
eckhart committed
105
106
107
    @property
    def pos(self):
        if self._pos < 0:
eckhart's avatar
eckhart committed
108
109
110
            assert self._node_keep and self._node_keep.pos >= 0, "pos value not ready yet"
            self._pos = self._node_keep.pos   # lazy evaluation of position
        self._node_keep = None  # forget node to allow GC to free memory
eckhart's avatar
eckhart committed
111
112
        return self._pos

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

118
119
120
121
122
123
124
    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'

125

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


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


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


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


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


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


175
def line_col(lbreaks: List[int], pos: int) -> Tuple[int, int]:
eckhart's avatar
eckhart committed
176
177
    """
    Returns the position within a text as (line, column)-tuple based
178
179
    on a list of all line breaks, including -1 and EOF.
    """
180
181
    if not lbreaks and pos >= 0:
        return 0, pos
182
183
184
185
186
187
    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

188

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


eckhart's avatar
eckhart committed
200
201
202
def adjust_error_locations(errors: List[Error],
                           original_text: Union[StringView, str],
                           source_mapping: SourceMapFunc=lambda i: i) -> List[Error]:
203
204
205
206
207
208
209
210
211
    """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.
212

213
214
215
216
217
218
219
220
221
222
223
    Returns:
        The list of errors. (Returning the list of errors is just syntactical
        sugar. Be aware that the line, col and orig_pos attributes have been
        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