The name of the initial branch for new projects is now "main" instead of "master". Existing projects remain unchanged. More information: https://doku.lrz.de/display/PUBLIC/GitLab

error.py 7.04 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 Arnold's avatar
Eckhart Arnold committed
44
from typing import Iterable, Iterator, Union, Tuple, List, NewType
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 68 69
    NO_ERROR  = ErrorCode(0)
    MESSAGE   = ErrorCode(1)
    WARNING   = ErrorCode(10)
    ERROR     = ErrorCode(1000)
70 71
    HIGHEST   = ERROR

72 73
    # warning codes

Eckhart Arnold's avatar
Eckhart Arnold 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

Eckhart Arnold's avatar
Eckhart Arnold committed
81
    MANDATORY_CONTINUATION = ErrorCode(1001)
82

Eckhart Arnold's avatar
Eckhart Arnold committed
83 84 85 86 87
    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
88
        assert code >= 0
Eckhart Arnold's avatar
Eckhart Arnold committed
89
        self.message = message
eckhart's avatar
eckhart committed
90
        self._pos = pos
Eckhart Arnold's avatar
Eckhart Arnold committed
91
        self.code = code
92
        self.orig_pos = orig_pos
93 94
        self.line = line
        self.column = column
95 96 97 98

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

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

eckhart's avatar
eckhart committed
106 107 108 109
    @property
    def pos(self):
        return self._pos

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

115 116 117 118 119 120 121
    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'

122

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


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


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


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


152 153 154 155 156 157 158
#######################################################################
#
# Setting of line, column and position properties of error messages.
#
#######################################################################


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


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

185

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


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

210 211 212 213 214 215 216 217 218 219 220
    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