Coverage for trlc/errors.py: 91%

147 statements  

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

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

54 

55 def to_string(self, include_column=True): 

56 """Return a nice string representation 

57 

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. 

61 

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

63 there is one) 

64 :type include_column: bool 

65 

66 :returns: a formatted location 

67 :rtype: str 

68 

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 

76 

77 def context_lines(self): 

78 return [] 

79 

80 def get_end_location(self): 

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

82 

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

84 function gets the "end" of it:: 

85 

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 

90 

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

92 :rtype: Location 

93 

94 """ 

95 return self 

96 

97 

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

105 

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] 

112 

113 

114class TRLC_Error(Exception): 

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

116 

117 :attribute location: Where the issue originates from 

118 :type: Location 

119 

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

121 :type: str 

122 

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) 

130 

131 super().__init__() 

132 self.location = location 

133 self.kind = kind 

134 self.message = message 

135 

136 

137class Message_Handler: 

138 """Universal message handler 

139 

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. 

144 

145 If the location comes from the location attribute of 

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

147 free. 

148 

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

150 :type: Boolean 

151 

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

153 :type: file or None 

154 

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

156 :type: str or None 

157 

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

159 :type: int 

160 

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

162 :type: int 

163 

164 :attribute supressed: Number of messages supressed by policy 

165 :type: int 

166 

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

170 

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 

192 

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 

197 

198 def __enter__(self): 

199 return self 

200 

201 def __exit__(self, *_): 

202 self.close() 

203 

204 def __del__(self): 

205 self.close() 

206 

207 def suppress(self, kind): 

208 assert isinstance(kind, Kind) 

209 self.suppress_kind.add(kind) 

210 

211 def cross_file_reference(self, location): 

212 assert isinstance(location, Location) 

213 

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) 

217 

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 

231 

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 

237 

238 if self.brief: 

239 context = None 

240 msg = "%s: trlc %s: %s" % (_loc_str(), 

241 str(kind), 

242 message) 

243 

244 else: 

245 context = location.context_lines() 

246 msg = "%s: %s: %s" % (_loc_str(len(context) == 0), 

247 str(kind), 

248 message) 

249 

250 if category: 

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

252 

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 

255 

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) 

263 

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) 

274 

275 if fatal: 

276 raise TRLC_Error(location, kind, message) 

277 

278 def lex_error(self, location, message): 

279 assert isinstance(location, Location) 

280 assert isinstance(message, str) 

281 

282 self.errors += 1 

283 self.emit(location = location, 

284 kind = Kind.SYS_ERROR, 

285 message = message) 

286 

287 def error(self, 

288 location, 

289 message, 

290 explanation=None, 

291 fatal=True, 

292 user=False): 

293 """ Create an error message 

294 

295 For example:: 

296 

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

298 

299 Might generate this output:: 

300 

301 x = 5 + 2 

302 ^ foo.check:5: error: potato 

303 

304 :param location: where to attach the message 

305 :type location: Location 

306 

307 :param message: the message to print 

308 :type message: str 

309 

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

311 the error? 

312 :type fatal: bool 

313 

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

315 :type user: bool 

316 

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) 

324 

325 if user: 

326 kind = Kind.USER_ERROR 

327 else: 

328 kind = Kind.SYS_ERROR 

329 

330 self.errors += 1 

331 self.emit(location = location, 

332 kind = kind, 

333 message = message, 

334 fatal = fatal, 

335 extrainfo = explanation) 

336 

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

338 """ Create a warning message 

339 

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) 

351 

352 if user: 

353 kind = Kind.USER_WARNING 

354 else: 

355 kind = Kind.SYS_WARNING 

356 

357 self.warnings += 1 

358 self.emit(location = location, 

359 kind = kind, 

360 message = message, 

361 extrainfo = explanation, 

362 fatal = False) 

363 

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 

369 

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) 

377 

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

379 assert isinstance(location, Location) 

380 assert isinstance(message, str) 

381 

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)