Coverage for trlc/errors.py: 92%

147 statements  

« 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/>. 

20 

21import sys 

22import enum 

23 

24from trlc import version 

25 

26 

27class Location: 

28 """Reference to a source or virtual location 

29 

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). 

33 

34 :attribute file_name: the name of the file or virtual location 

35 :type: str 

36 

37 :attribute line_no: an optional line number, starting at 1 

38 :type: int 

39 

40 :attribute col_no: an optional column number, starting at 1 

41 :type: int: 

42 """ 

43 

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 

55 

56 def to_string(self, include_column=True): 

57 """Return a nice string representation 

58 

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. 

62 

63 :param include_column: If set, include the column location (if \ 

64 there is one) 

65 :type include_column: bool 

66 

67 :returns: a formatted location 

68 :rtype: str 

69 

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 

77 

78 def context_lines(self): 

79 return [] 

80 

81 def get_end_location(self): 

82 """Get location point to the end of this location 

83 

84 When we generate a location for a longer sequence then this 

85 function gets the "end" of it:: 

86 

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 

91 

92 :returns: a pointer to the last character in a location 

93 :rtype: Location 

94 

95 """ 

96 return self 

97 

98 

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() 

106 

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] 

115 

116 

117class TRLC_Error(Exception): 

118 """The universal exception that TRLC raises if something goes wrong 

119 

120 :attribute location: Where the issue originates from 

121 :type: Location 

122 

123 :attribute kind: The kind of problem (e.g. lex error, error, warning, etc.) 

124 :type: str 

125 

126 :attribute message: Description of the problem 

127 :type: str 

128 """ 

129 

130 def __init__(self, location, kind, message): 

131 assert isinstance(location, Location) 

132 assert isinstance(kind, Kind) 

133 assert isinstance(message, str) 

134 

135 super().__init__() 

136 self.location = location 

137 self.kind = kind 

138 self.message = message 

139 

140 

141class Message_Handler: 

142 """Universal message handler 

143 

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. 

148 

149 If the location comes from the location attribute of 

150 :class:`~trlc.ast.Node` then you also get context provided for 

151 free. 

152 

153 :attribute brief: When true displays as much context as possible 

154 :type: Boolean 

155 

156 :attribute out: Output stream (stdout if None) 

157 :type: file or None 

158 

159 :attribute strip_prefix: Prefix stripped from file paths in messages 

160 :type: str or None 

161 

162 :attribute warnings: Number of system or user warnings raised 

163 :type: int 

164 

165 :attribute errors: Number of system or user errors raised 

166 :type: int 

167 

168 :attribute supressed: Number of messages supressed by policy 

169 :type: int 

170 

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``. 

174 

175 """ 

176 

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 

204 

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 

209 

210 def __enter__(self): 

211 return self 

212 

213 def __exit__(self, *_): 

214 self.close() 

215 

216 def __del__(self): 

217 self.close() 

218 

219 def suppress(self, kind): 

220 assert isinstance(kind, Kind) 

221 self.suppress_kind.add(kind) 

222 

223 def cross_file_reference(self, location): 

224 assert isinstance(location, Location) 

225 

226 if self.sm is None: 

227 return location.to_string(include_column=False) 

228 return self.sm.cross_file_reference(location) 

229 

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 

237 

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 

243 

244 if self.brief: 

245 context = None 

246 msg = "%s: trlc %s: %s" % (_loc_str(), str(kind), message) 

247 

248 else: 

249 context = location.context_lines() 

250 msg = "%s: %s: %s" % (_loc_str(len(context) == 0), str(kind), message) 

251 

252 if category: 

253 msg += " [%s]" % category 

254 

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 

257 

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) 

265 

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) 

273 

274 if fatal: 

275 raise TRLC_Error(location, kind, message) 

276 

277 def lex_error(self, location, message): 

278 assert isinstance(location, Location) 

279 assert isinstance(message, str) 

280 

281 self.errors += 1 

282 self.emit(location=location, kind=Kind.SYS_ERROR, message=message) 

283 

284 def error(self, location, message, explanation=None, fatal=True, user=False): 

285 """ Create an error message 

286 

287 For example:: 

288 

289 mh.error(my_expr.location, "potato") 

290 

291 Might generate this output:: 

292 

293 x = 5 + 2 

294 ^ foo.check:5: error: potato 

295 

296 :param location: where to attach the message 

297 :type location: Location 

298 

299 :param message: the message to print 

300 :type message: str 

301 

302 :param fatal: should we raise an exception in addition to printing \ 

303 the error? 

304 :type fatal: bool 

305 

306 :param user: if set print "check error:" instead of "error:" 

307 :type user: bool 

308 

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) 

316 

317 if user: 

318 kind = Kind.USER_ERROR 

319 else: 

320 kind = Kind.SYS_ERROR 

321 

322 self.errors += 1 

323 self.emit( 

324 location=location, 

325 kind=kind, 

326 message=message, 

327 fatal=fatal, 

328 extrainfo=explanation, 

329 ) 

330 

331 def warning(self, location, message, explanation=None, user=False): 

332 """Create a warning message 

333 

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) 

345 

346 if user: 

347 kind = Kind.USER_WARNING 

348 else: 

349 kind = Kind.SYS_WARNING 

350 

351 self.warnings += 1 

352 self.emit( 

353 location=location, 

354 kind=kind, 

355 message=message, 

356 extrainfo=explanation, 

357 fatal=False, 

358 ) 

359 

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 

365 

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 ) 

375 

376 def ice_loc(self, location, message): # pragma: no cover 

377 assert isinstance(location, Location) 

378 assert isinstance(message, str) 

379 

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)