Coverage for trlc/errors.py: 95%

124 statements  

« prev     ^ index     » next       coverage.py v7.7.1, created at 2025-03-27 00:52 +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 warnings: Number of system or user warnings raised 

153 :type: int 

154 

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

156 :type: int 

157 

158 :attribute supressed: Number of messages supressed by policy 

159 :type: int 

160 

161 """ 

162 def __init__(self, brief=False, detailed_info=True): 

163 assert isinstance(brief, bool) 

164 self.brief = brief 

165 self.show_details = detailed_info 

166 self.warnings = 0 

167 self.errors = 0 

168 self.suppressed = 0 

169 self.sm = None 

170 self.suppress_kind = set() 

171 

172 def suppress(self, kind): 

173 assert isinstance(kind, Kind) 

174 self.suppress_kind.add(kind) 

175 

176 def cross_file_reference(self, location): 

177 assert isinstance(location, Location) 

178 

179 if self.sm is None: 179 ↛ 180line 179 didn't jump to line 180 because the condition on line 179 was never true

180 return location.to_string(include_column=False) 

181 else: 

182 return self.sm.cross_file_reference(location) 

183 

184 def emit(self, 

185 location, 

186 kind, 

187 message, 

188 fatal=True, 

189 extrainfo=None, 

190 category=None): 

191 assert isinstance(location, Location) 

192 assert isinstance(kind, Kind) 

193 assert isinstance(message, str) 

194 assert isinstance(fatal, bool) 

195 assert isinstance(extrainfo, str) or extrainfo is None 

196 assert isinstance(category, str) or category is None 

197 

198 if self.brief: 

199 context = None 

200 msg = "%s: trlc %s: %s" % (location.to_string(), 

201 str(kind), 

202 message) 

203 

204 else: 

205 context = location.context_lines() 

206 msg = "%s: %s: %s" % (location.to_string(len(context) == 0), 

207 str(kind), 

208 message) 

209 

210 if category: 

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

212 

213 if kind in self.suppress_kind: 213 ↛ 214line 213 didn't jump to line 214 because the condition on line 213 was never true

214 self.suppressed += 1 

215 

216 else: 

217 if context: 

218 assert len(context) == 2 

219 print(context[0].replace("\t", " ")) 

220 print(context[1].replace("\t", " "), msg) 

221 else: 

222 print(msg) 

223 

224 if not self.brief \ 

225 and self.show_details \ 

226 and extrainfo: 

227 if context: 

228 indent = len(context[1]) - 1 

229 else: 

230 indent = 0 

231 for line in extrainfo.splitlines(): 

232 print("%s| %s" % (" " * indent, 

233 line.rstrip())) 

234 

235 if fatal: 

236 raise TRLC_Error(location, kind, message) 

237 

238 def lex_error(self, location, message): 

239 assert isinstance(location, Location) 

240 assert isinstance(message, str) 

241 

242 self.errors += 1 

243 self.emit(location = location, 

244 kind = Kind.SYS_ERROR, 

245 message = message) 

246 

247 def error(self, 

248 location, 

249 message, 

250 explanation=None, 

251 fatal=True, 

252 user=False): 

253 """ Create an error message 

254 

255 For example:: 

256 

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

258 

259 Might generate this output:: 

260 

261 x = 5 + 2 

262 ^ foo.check:5: error: potato 

263 

264 :param location: where to attach the message 

265 :type location: Location 

266 

267 :param message: the message to print 

268 :type message: str 

269 

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

271 the error? 

272 :type fatal: bool 

273 

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

275 :type user: bool 

276 

277 :raises TRLC_Error: if fatal is true 

278 """ 

279 assert isinstance(location, Location) 

280 assert isinstance(message, str) 

281 assert isinstance(explanation, str) or explanation is None 

282 assert isinstance(fatal, bool) 

283 assert isinstance(user, bool) 

284 

285 if user: 

286 kind = Kind.USER_ERROR 

287 else: 

288 kind = Kind.SYS_ERROR 

289 

290 self.errors += 1 

291 self.emit(location = location, 

292 kind = kind, 

293 message = message, 

294 fatal = fatal, 

295 extrainfo = explanation) 

296 

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

298 """ Create a warning message 

299 

300 :param location: where to attach the message 

301 :type location: Location 

302 :param message: the message to print 

303 :type message: str 

304 :param user: if set print "check warning:" instead of "warning:" 

305 :type user: bool 

306 """ 

307 assert isinstance(location, Location) 

308 assert isinstance(message, str) 

309 assert isinstance(explanation, str) or explanation is None 

310 assert isinstance(user, bool) 

311 

312 if user: 

313 kind = Kind.USER_WARNING 

314 else: 

315 kind = Kind.SYS_WARNING 

316 

317 self.warnings += 1 

318 self.emit(location = location, 

319 kind = kind, 

320 message = message, 

321 extrainfo = explanation, 

322 fatal = False) 

323 

324 def check(self, location, message, check, explanation=None): 

325 assert isinstance(location, Location) 

326 assert isinstance(message, str) 

327 assert isinstance(check, str) 

328 assert isinstance(explanation, str) or explanation is None 

329 

330 self.warnings += 1 

331 self.emit(location = location, 

332 kind = Kind.SYS_CHECK, 

333 message = message, 

334 fatal = False, 

335 extrainfo = explanation, 

336 category = check) 

337 

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

339 assert isinstance(location, Location) 

340 assert isinstance(message, str) 

341 

342 self.errors += 1 

343 self.emit(location = location, 

344 kind = Kind.SYS_ERROR, 

345 message = message, 

346 extrainfo = "please report this to %s" % version.BUGS_URL, 

347 fatal = False) 

348 sys.exit(1)