Coverage for trlc/errors.py: 92%
147 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-30 11:03 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-30 11:03 +0000
1#!/usr/bin/env python3
2#
3# TRLC - Treat Requirements Like Code
4# Copyright (C) 2022-2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
5#
6# This file is part of the TRLC Python Reference Implementation.
7#
8# TRLC is free software: you can redistribute it and/or modify it
9# under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# TRLC is distributed in the hope that it will be useful, but WITHOUT
14# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
16# License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with TRLC. If not, see <https://www.gnu.org/licenses/>.
21import sys
22import enum
24from trlc import version
27class Location:
28 """Reference to a source or virtual location
30 Any message raised by the :class:`Message_Handler` will be
31 attached to a given location. This location can be real
32 (i.e. something in a file) or virtual (i.e. a builtin function).
34 :attribute file_name: the name of the file or virtual location
35 :type: str
37 :attribute line_no: an optional line number, starting at 1
38 :type: int
40 :attribute col_no: an optional column number, starting at 1
41 :type: int:
42 """
44 def __init__(self, file_name, line_no=None, col_no=None):
45 assert isinstance(file_name, str)
46 if line_no is not None:
47 assert isinstance(line_no, int)
48 assert line_no >= 1
49 if col_no is not None:
50 assert isinstance(col_no, int)
51 assert col_no >= 1
52 self.file_name = file_name
53 self.line_no = line_no
54 self.col_no = col_no
56 def to_string(self, include_column=True):
57 """Return a nice string representation
59 The style is the gcc-style file:line:column format. Note that
60 the filename is stripped of its path in order to make the
61 final message smaller.
63 :param include_column: If set, include the column location (if \
64 there is one)
65 :type include_column: bool
67 :returns: a formatted location
68 :rtype: str
70 """
71 rv = self.file_name
72 if self.line_no:
73 rv += ":%u" % self.line_no
74 if self.col_no and include_column:
75 rv += ":%u" % self.col_no
76 return rv
78 def context_lines(self):
79 return []
81 def get_end_location(self):
82 """Get location point to the end of this location
84 When we generate a location for a longer sequence then this
85 function gets the "end" of it::
87 for example here
88 ^^^^^^^ this is the whole range
89 ^ file/line/col points here
90 ^ file/line/col of end_location points here
92 :returns: a pointer to the last character in a location
93 :rtype: Location
95 """
96 return self
99@enum.unique
100class Kind(enum.Enum):
101 SYS_ERROR = enum.auto()
102 SYS_CHECK = enum.auto()
103 SYS_WARNING = enum.auto()
104 USER_ERROR = enum.auto()
105 USER_WARNING = enum.auto()
107 def __str__(self):
108 return {
109 "SYS_ERROR": "error",
110 "SYS_CHECK": "issue",
111 "SYS_WARNING": "warning",
112 "USER_ERROR": "check error",
113 "USER_WARNING": "check warning",
114 }[self.name]
117class TRLC_Error(Exception):
118 """The universal exception that TRLC raises if something goes wrong
120 :attribute location: Where the issue originates from
121 :type: Location
123 :attribute kind: The kind of problem (e.g. lex error, error, warning, etc.)
124 :type: str
126 :attribute message: Description of the problem
127 :type: str
128 """
130 def __init__(self, location, kind, message):
131 assert isinstance(location, Location)
132 assert isinstance(kind, Kind)
133 assert isinstance(message, str)
135 super().__init__()
136 self.location = location
137 self.kind = kind
138 self.message = message
141class Message_Handler:
142 """Universal message handler
144 All messages from TRLC are processed by this class. If you want to
145 write a tool that emits additional messages then it would be a
146 really good idea to also use this class. Do not use your own print
147 statements.
149 If the location comes from the location attribute of
150 :class:`~trlc.ast.Node` then you also get context provided for
151 free.
153 :attribute brief: When true displays as much context as possible
154 :type: Boolean
156 :attribute out: Output stream (stdout if None)
157 :type: file or None
159 :attribute strip_prefix: Prefix stripped from file paths in messages
160 :type: str or None
162 :attribute warnings: Number of system or user warnings raised
163 :type: int
165 :attribute errors: Number of system or user errors raised
166 :type: int
168 :attribute supressed: Number of messages supressed by policy
169 :type: int
171 Can be used as a context manager (``with Message_Handler(...) as
172 mh:``), which will automatically close any file opened via
173 ``out_path``.
175 """
177 def __init__(
178 self,
179 brief=False,
180 detailed_info=True,
181 out=None,
182 strip_prefix=None,
183 out_path=None,
184 ):
185 assert isinstance(brief, bool)
186 assert isinstance(strip_prefix, str) or strip_prefix is None
187 assert isinstance(out_path, str) or out_path is None
188 self.brief = brief
189 self.show_details = detailed_info
190 self.warnings = 0
191 self.errors = 0
192 self.suppressed = 0
193 self.sm = None
194 self.suppress_kind = set()
195 self.strip_prefix = strip_prefix
196 if out_path is not None: 196 ↛ 197line 196 didn't jump to line 197 because the condition on line 196 was never true
197 self._owned_file = open( # pylint: disable=consider-using-with
198 out_path, "w", encoding="UTF-8"
199 )
200 self.out = self._owned_file
201 else:
202 self._owned_file = None
203 self.out = out if out is not None else sys.stdout
205 def close(self):
206 if self._owned_file is not None: 206 ↛ 207line 206 didn't jump to line 207 because the condition on line 206 was never true
207 self._owned_file.close()
208 self._owned_file = None
210 def __enter__(self):
211 return self
213 def __exit__(self, *_):
214 self.close()
216 def __del__(self):
217 self.close()
219 def suppress(self, kind):
220 assert isinstance(kind, Kind)
221 self.suppress_kind.add(kind)
223 def cross_file_reference(self, location):
224 assert isinstance(location, Location)
226 if self.sm is None:
227 return location.to_string(include_column=False)
228 return self.sm.cross_file_reference(location)
230 def emit(self, location, kind, message, fatal=True, extrainfo=None, category=None):
231 assert isinstance(location, Location)
232 assert isinstance(kind, Kind)
233 assert isinstance(message, str)
234 assert isinstance(fatal, bool)
235 assert isinstance(extrainfo, str) or extrainfo is None
236 assert isinstance(category, str) or category is None
238 def _loc_str(include_column=True):
239 loc = location.to_string(include_column)
240 if self.strip_prefix and loc.startswith(self.strip_prefix): 240 ↛ 241line 240 didn't jump to line 241 because the condition on line 240 was never true
241 loc = loc[len(self.strip_prefix) :]
242 return loc
244 if self.brief:
245 context = None
246 msg = "%s: trlc %s: %s" % (_loc_str(), str(kind), message)
248 else:
249 context = location.context_lines()
250 msg = "%s: %s: %s" % (_loc_str(len(context) == 0), str(kind), message)
252 if category:
253 msg += " [%s]" % category
255 if kind in self.suppress_kind: 255 ↛ 256line 255 didn't jump to line 256 because the condition on line 255 was never true
256 self.suppressed += 1
258 else:
259 if context:
260 assert len(context) == 2
261 print(context[0].replace("\t", " "), file=self.out)
262 print(context[1].replace("\t", " "), msg, file=self.out)
263 else:
264 print(msg, file=self.out)
266 if not self.brief and self.show_details and extrainfo:
267 if context:
268 indent = len(context[1]) - 1
269 else:
270 indent = 0
271 for line in extrainfo.splitlines():
272 print("%s| %s" % (" " * indent, line.rstrip()), file=self.out)
274 if fatal:
275 raise TRLC_Error(location, kind, message)
277 def lex_error(self, location, message):
278 assert isinstance(location, Location)
279 assert isinstance(message, str)
281 self.errors += 1
282 self.emit(location=location, kind=Kind.SYS_ERROR, message=message)
284 def error(self, location, message, explanation=None, fatal=True, user=False):
285 """ Create an error message
287 For example::
289 mh.error(my_expr.location, "potato")
291 Might generate this output::
293 x = 5 + 2
294 ^ foo.check:5: error: potato
296 :param location: where to attach the message
297 :type location: Location
299 :param message: the message to print
300 :type message: str
302 :param fatal: should we raise an exception in addition to printing \
303 the error?
304 :type fatal: bool
306 :param user: if set print "check error:" instead of "error:"
307 :type user: bool
309 :raises TRLC_Error: if fatal is true
310 """
311 assert isinstance(location, Location)
312 assert isinstance(message, str)
313 assert isinstance(explanation, str) or explanation is None
314 assert isinstance(fatal, bool)
315 assert isinstance(user, bool)
317 if user:
318 kind = Kind.USER_ERROR
319 else:
320 kind = Kind.SYS_ERROR
322 self.errors += 1
323 self.emit(
324 location=location,
325 kind=kind,
326 message=message,
327 fatal=fatal,
328 extrainfo=explanation,
329 )
331 def warning(self, location, message, explanation=None, user=False):
332 """Create a warning message
334 :param location: where to attach the message
335 :type location: Location
336 :param message: the message to print
337 :type message: str
338 :param user: if set print "check warning:" instead of "warning:"
339 :type user: bool
340 """
341 assert isinstance(location, Location)
342 assert isinstance(message, str)
343 assert isinstance(explanation, str) or explanation is None
344 assert isinstance(user, bool)
346 if user:
347 kind = Kind.USER_WARNING
348 else:
349 kind = Kind.SYS_WARNING
351 self.warnings += 1
352 self.emit(
353 location=location,
354 kind=kind,
355 message=message,
356 extrainfo=explanation,
357 fatal=False,
358 )
360 def check(self, location, message, check, explanation=None):
361 assert isinstance(location, Location)
362 assert isinstance(message, str)
363 assert isinstance(check, str)
364 assert isinstance(explanation, str) or explanation is None
366 self.warnings += 1
367 self.emit(
368 location=location,
369 kind=Kind.SYS_CHECK,
370 message=message,
371 fatal=False,
372 extrainfo=explanation,
373 category=check,
374 )
376 def ice_loc(self, location, message): # pragma: no cover
377 assert isinstance(location, Location)
378 assert isinstance(message, str)
380 self.errors += 1
381 self.emit(
382 location=location,
383 kind=Kind.SYS_ERROR,
384 message=message,
385 extrainfo="please report this to %s" % version.BUGS_URL,
386 fatal=False,
387 )
388 sys.exit(1)