The expiration time for new job artifacts in CI/CD pipelines is now 30 days (GitLab default). Previously generated artifacts in already completed jobs will not be affected by the change. The latest artifacts for all jobs in the latest successful pipelines will be kept. More information: https://gitlab.lrz.de/help/user/admin_area/settings/continuous_integration.html#default-artifacts-expiration

error.py 6.63 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
import bisect
40
from typing import Iterable, Iterator, Union, Tuple, List
41

42
from DHParser.preprocess import SourceMapFunc
43
from DHParser.stringview import StringView
44
45
46
47
48
49
50

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


class Error:
56
    __slots__ = ['message', 'level', 'code', 'pos', 'orig_pos', 'line', 'column']
57

58
59
    # error levels

60
61
62
63
    WARNING   = 1
    ERROR     = 1000
    HIGHEST   = ERROR

64
65
66
    # warning codes

    REDEFINED_DIRECTIVE_WARNING = 101
67
    REDECLARED_TOKEN_WARNING = 102
68

69
70
    UNDEFINED_SYMBOL_IN_TRANSFORMATION_TABLE = 601

71
72
73
74
    # error codes

    MANDATORY_CONTINUATION = 1001

75
76
    def __init__(self, message: str, code: int = ERROR, pos: int = -1,
                 orig_pos: int = -1, line: int = -1, column: int = -1) -> None:
77
        self.message = message
78
        assert code >= 0
79
        self.code = code
80
        self.pos = pos
81
        self.orig_pos = orig_pos
82
83
        self.line = line
        self.column = column
84
85
86
87

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

91
    def __repr__(self):
92
93
        return 'Error("%s", %s, %i, %i, %i, %i)' \
               % (self.message, repr(self.code), self.pos, self.orig_pos, self.line, self.column)
94

95
    @property
di68kap's avatar
di68kap committed
96
    def severity(self):
97
        """Returns a string representation of the error level, e.g. "warning"."""
98
        return "Warning" if is_warning(self.code) else "Error"
99

100
101
102
103
104
105
106
    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'

107

108
def is_warning(code: int) -> bool:
eckhart's avatar
eckhart committed
109
    """Returns True, if error is merely a warning."""
110
    return code < Error.ERROR
111
112


113
def is_error(code: int) -> bool:
eckhart's avatar
eckhart committed
114
    """Returns True, if error is an error, not just a warning."""
115
    return code >= Error.ERROR
116
117


eckhart's avatar
eckhart committed
118
def has_errors(messages: Iterable[Error], level: int = Error.ERROR) -> bool:
119
120
121
122
123
    """
    Returns True, if at least one entry in `messages` has at
    least the given error `level`.
    """
    for err_obj in messages:
124
        if err_obj.code >= level:
125
126
127
128
            return True
    return False


eckhart's avatar
eckhart committed
129
def only_errors(messages: Iterable[Error], level: int = Error.ERROR) -> Iterator[Error]:
130
131
132
133
    """
    Returns an Iterator that yields only those messages that have
    at least the given error level.
    """
134
    return (err for err in messages if err.code >= level)
135
136


137
138
139
140
141
142
143
#######################################################################
#
# Setting of line, column and position properties of error messages.
#
#######################################################################


eckhart's avatar
eckhart committed
144
def linebreaks(text: Union[StringView, str]) -> List[int]:
eckhart's avatar
eckhart committed
145
146
147
148
    """
    Returns a list of indices all line breaks in the text.
    """
    lbr = [-1]
149
150
    i = text.find('\n', 0)
    while i >= 0:
eckhart's avatar
eckhart committed
151
        lbr.append(i)
152
        i = text.find('\n', i + 1)
eckhart's avatar
eckhart committed
153
154
    lbr.append(len(text))
    return lbr
155
156


157
def line_col(lbreaks: List[int], pos: int) -> Tuple[int, int]:
eckhart's avatar
eckhart committed
158
159
    """
    Returns the position within a text as (line, column)-tuple based
160
161
    on a list of all line breaks, including -1 and EOF.
    """
162
163
    if not lbreaks and pos >= 0:
        return 0, pos
164
165
166
167
168
169
    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

170

171
172
173
# def line_col(text: Union[StringView, str], pos: int) -> Tuple[int, int]:
#     """
#     Returns the position within a text as (line, column)-tuple.
174
#     """
175
176
177
178
179
180
181
#     if pos < 0 or pos > len(text):  # one character behind EOF is still an allowed position!
#         raise ValueError('Position %i outside text of length %s !' % (pos, len(text)))
#     line = text.count("\n", 0, pos) + 1
#     column = pos - text.rfind("\n", 0, pos)
#     return line, column


eckhart's avatar
eckhart committed
182
183
184
def adjust_error_locations(errors: List[Error],
                           original_text: Union[StringView, str],
                           source_mapping: SourceMapFunc=lambda i: i) -> List[Error]:
185
186
187
188
189
190
191
192
193
    """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.
194

195
196
197
198
199
200
201
202
203
204
205
    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