Coverage for trlc/errors.py: 91%
147 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-03-03 13:48 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-03-03 13:48 +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 """
43 def __init__(self, file_name, line_no=None, col_no=None):
44 assert isinstance(file_name, str)
45 if line_no is not None:
46 assert isinstance(line_no, int)
47 assert line_no >= 1
48 if col_no is not None:
49 assert isinstance(col_no, int)
50 assert col_no >= 1
51 self.file_name = file_name
52 self.line_no = line_no
53 self.col_no = col_no
55 def to_string(self, include_column=True):
56 """Return a nice string representation
58 The style is the gcc-style file:line:column format. Note that
59 the filename is stripped of its path in order to make the
60 final message smaller.
62 :param include_column: If set, include the column location (if \
63 there is one)
64 :type include_column: bool
66 :returns: a formatted location
67 :rtype: str
69 """
70 rv = self.file_name
71 if self.line_no:
72 rv += ":%u" % self.line_no
73 if self.col_no and include_column:
74 rv += ":%u" % self.col_no
75 return rv
77 def context_lines(self):
78 return []
80 def get_end_location(self):
81 """Get location point to the end of this location
83 When we generate a location for a longer sequence then this
84 function gets the "end" of it::
86 for example here
87 ^^^^^^^ this is the whole range
88 ^ file/line/col points here
89 ^ file/line/col of end_location points here
91 :returns: a pointer to the last character in a location
92 :rtype: Location
94 """
95 return self
98@enum.unique
99class Kind(enum.Enum):
100 SYS_ERROR = enum.auto()
101 SYS_CHECK = enum.auto()
102 SYS_WARNING = enum.auto()
103 USER_ERROR = enum.auto()
104 USER_WARNING = enum.auto()
106 def __str__(self):
107 return {"SYS_ERROR" : "error",
108 "SYS_CHECK" : "issue",
109 "SYS_WARNING" : "warning",
110 "USER_ERROR" : "check error",
111 "USER_WARNING" : "check warning"}[self.name]
114class TRLC_Error(Exception):
115 """ The universal exception that TRLC raises if something goes wrong
117 :attribute location: Where the issue originates from
118 :type: Location
120 :attribute kind: The kind of problem (e.g. lex error, error, warning, etc.)
121 :type: str
123 :attribute message: Description of the problem
124 :type: str
125 """
126 def __init__(self, location, kind, message):
127 assert isinstance(location, Location)
128 assert isinstance(kind, Kind)
129 assert isinstance(message, str)
131 super().__init__()
132 self.location = location
133 self.kind = kind
134 self.message = message
137class Message_Handler:
138 """Universal message handler
140 All messages from TRLC are processed by this class. If you want to
141 write a tool that emits additional messages then it would be a
142 really good idea to also use this class. Do not use your own print
143 statements.
145 If the location comes from the location attribute of
146 :class:`~trlc.ast.Node` then you also get context provided for
147 free.
149 :attribute brief: When true displays as much context as possible
150 :type: Boolean
152 :attribute out: Output stream (stdout if None)
153 :type: file or None
155 :attribute strip_prefix: Prefix stripped from file paths in messages
156 :type: str or None
158 :attribute warnings: Number of system or user warnings raised
159 :type: int
161 :attribute errors: Number of system or user errors raised
162 :type: int
164 :attribute supressed: Number of messages supressed by policy
165 :type: int
167 Can be used as a context manager (``with Message_Handler(...) as
168 mh:``), which will automatically close any file opened via
169 ``out_path``.
171 """
172 def __init__(self, brief=False, detailed_info=True,
173 out=None, strip_prefix=None, out_path=None):
174 assert isinstance(brief, bool)
175 assert isinstance(strip_prefix, str) or strip_prefix is None
176 assert isinstance(out_path, str) or out_path is None
177 self.brief = brief
178 self.show_details = detailed_info
179 self.warnings = 0
180 self.errors = 0
181 self.suppressed = 0
182 self.sm = None
183 self.suppress_kind = set()
184 self.strip_prefix = strip_prefix
185 if out_path is not None: 185 ↛ 186line 185 didn't jump to line 186 because the condition on line 185 was never true
186 self._owned_file = open( # pylint: disable=consider-using-with
187 out_path, "w", encoding="UTF-8")
188 self.out = self._owned_file
189 else:
190 self._owned_file = None
191 self.out = out if out is not None else sys.stdout
193 def close(self):
194 if self._owned_file is not None: 194 ↛ 195line 194 didn't jump to line 195 because the condition on line 194 was never true
195 self._owned_file.close()
196 self._owned_file = None
198 def __enter__(self):
199 return self
201 def __exit__(self, *_):
202 self.close()
204 def __del__(self):
205 self.close()
207 def suppress(self, kind):
208 assert isinstance(kind, Kind)
209 self.suppress_kind.add(kind)
211 def cross_file_reference(self, location):
212 assert isinstance(location, Location)
214 if self.sm is None: 214 ↛ 215line 214 didn't jump to line 215 because the condition on line 214 was never true
215 return location.to_string(include_column=False)
216 return self.sm.cross_file_reference(location)
218 def emit(self,
219 location,
220 kind,
221 message,
222 fatal=True,
223 extrainfo=None,
224 category=None):
225 assert isinstance(location, Location)
226 assert isinstance(kind, Kind)
227 assert isinstance(message, str)
228 assert isinstance(fatal, bool)
229 assert isinstance(extrainfo, str) or extrainfo is None
230 assert isinstance(category, str) or category is None
232 def _loc_str(include_column=True):
233 loc = location.to_string(include_column)
234 if self.strip_prefix and loc.startswith(self.strip_prefix): 234 ↛ 235line 234 didn't jump to line 235 because the condition on line 234 was never true
235 loc = loc[len(self.strip_prefix):]
236 return loc
238 if self.brief:
239 context = None
240 msg = "%s: trlc %s: %s" % (_loc_str(),
241 str(kind),
242 message)
244 else:
245 context = location.context_lines()
246 msg = "%s: %s: %s" % (_loc_str(len(context) == 0),
247 str(kind),
248 message)
250 if category:
251 msg += " [%s]" % category
253 if kind in self.suppress_kind: 253 ↛ 254line 253 didn't jump to line 254 because the condition on line 253 was never true
254 self.suppressed += 1
256 else:
257 if context:
258 assert len(context) == 2
259 print(context[0].replace("\t", " "), file=self.out)
260 print(context[1].replace("\t", " "), msg, file=self.out)
261 else:
262 print(msg, file=self.out)
264 if not self.brief \
265 and self.show_details \
266 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,
273 line.rstrip()), file=self.out)
275 if fatal:
276 raise TRLC_Error(location, kind, message)
278 def lex_error(self, location, message):
279 assert isinstance(location, Location)
280 assert isinstance(message, str)
282 self.errors += 1
283 self.emit(location = location,
284 kind = Kind.SYS_ERROR,
285 message = message)
287 def error(self,
288 location,
289 message,
290 explanation=None,
291 fatal=True,
292 user=False):
293 """ Create an error message
295 For example::
297 mh.error(my_expr.location, "potato")
299 Might generate this output::
301 x = 5 + 2
302 ^ foo.check:5: error: potato
304 :param location: where to attach the message
305 :type location: Location
307 :param message: the message to print
308 :type message: str
310 :param fatal: should we raise an exception in addition to printing \
311 the error?
312 :type fatal: bool
314 :param user: if set print "check error:" instead of "error:"
315 :type user: bool
317 :raises TRLC_Error: if fatal is true
318 """
319 assert isinstance(location, Location)
320 assert isinstance(message, str)
321 assert isinstance(explanation, str) or explanation is None
322 assert isinstance(fatal, bool)
323 assert isinstance(user, bool)
325 if user:
326 kind = Kind.USER_ERROR
327 else:
328 kind = Kind.SYS_ERROR
330 self.errors += 1
331 self.emit(location = location,
332 kind = kind,
333 message = message,
334 fatal = fatal,
335 extrainfo = explanation)
337 def warning(self, location, message, explanation=None, user=False):
338 """ Create a warning message
340 :param location: where to attach the message
341 :type location: Location
342 :param message: the message to print
343 :type message: str
344 :param user: if set print "check warning:" instead of "warning:"
345 :type user: bool
346 """
347 assert isinstance(location, Location)
348 assert isinstance(message, str)
349 assert isinstance(explanation, str) or explanation is None
350 assert isinstance(user, bool)
352 if user:
353 kind = Kind.USER_WARNING
354 else:
355 kind = Kind.SYS_WARNING
357 self.warnings += 1
358 self.emit(location = location,
359 kind = kind,
360 message = message,
361 extrainfo = explanation,
362 fatal = False)
364 def check(self, location, message, check, explanation=None):
365 assert isinstance(location, Location)
366 assert isinstance(message, str)
367 assert isinstance(check, str)
368 assert isinstance(explanation, str) or explanation is None
370 self.warnings += 1
371 self.emit(location = location,
372 kind = Kind.SYS_CHECK,
373 message = message,
374 fatal = False,
375 extrainfo = explanation,
376 category = check)
378 def ice_loc(self, location, message): # pragma: no cover
379 assert isinstance(location, Location)
380 assert isinstance(message, str)
382 self.errors += 1
383 self.emit(location = location,
384 kind = Kind.SYS_ERROR,
385 message = message,
386 extrainfo = "please report this to %s" % version.BUGS_URL,
387 fatal = False)
388 sys.exit(1)