OILS / core / error.py View on Github | oils.pub

380 lines, 117 significant
1"""error.py - error types"""
2from __future__ import print_function
3
4from _devbuild.gen.syntax_asdl import loc_e, loc_t, loc
5from _devbuild.gen.value_asdl import (value, value_t, value_str)
6from core import num
7from mycpp.mylib import NewDict
8
9from typing import Dict, Union, NoReturn, TYPE_CHECKING
10
11# For storing errors in List[T]
12if TYPE_CHECKING:
13 IOError_OSError = Union[IOError, OSError]
14
15
16def _ValType(val):
17 # type: (value_t) -> str
18 """Duplicate ui.ValType for now"""
19 return value_str(val.tag(), dot=False)
20
21
22class _ErrorWithLocation(Exception):
23 """A parse error that can be formatted.
24
25 Formatting is in ui.PrintError.
26 """
27
28 def __init__(self, msg, location):
29 # type: (str, loc_t) -> None
30
31 self.msg = msg
32
33 # Ensure that the location field is always populated
34 if location is None:
35 self.location = loc.Missing # type: loc_t
36 else:
37 self.location = location
38
39 def HasLocation(self):
40 # type: () -> bool
41 return self.location.tag() != loc_e.Missing
42
43 def UserErrorString(self):
44 # type: () -> str
45 return self.msg
46
47 def __repr__(self):
48 # type: () -> str
49 return '<%s %r>' % (self.msg, self.location)
50
51
52class Usage(_ErrorWithLocation):
53 """For flag parsing errors in builtins and main()
54
55 Called by e_usage(). TODO: Should settle on a single interface that
56 can be translated. Sometimes we use 'raise error.Usage()'
57 """
58
59 def __init__(self, msg, location):
60 # type: (str, loc_t) -> None
61 _ErrorWithLocation.__init__(self, msg, location)
62
63
64class Parse(_ErrorWithLocation):
65 """Used in the parsers."""
66
67 def __init__(self, msg, location):
68 # type: (str, loc_t) -> None
69 _ErrorWithLocation.__init__(self, msg, location)
70
71
72class WordFailure(_ErrorWithLocation):
73 """Raised for failure of word evaluation
74
75 Meant to be caught.
76 """
77
78 def __init__(self, msg, location):
79 # type: (str, loc_t) -> None
80 _ErrorWithLocation.__init__(self, msg, location)
81
82
83class FailGlob(WordFailure):
84 """Raised when a glob matches nothing when failglob is set.
85
86 Meant to be caught.
87 """
88
89 def __init__(self, msg, location):
90 # type: (str, loc_t) -> None
91 WordFailure.__init__(self, 'failglob: ' + msg, location)
92
93
94class VarSubFailure(WordFailure):
95 """Raised when a variable substitution fails. For example, this
96 is thrown with ${!ref} when the variable "ref" contains invalid
97 variable name such as ref="a b c".
98
99 Meant to be caught.
100 """
101
102 def __init__(self, msg, location):
103 # type: (str, loc_t) -> None
104 WordFailure.__init__(self, msg, location)
105
106
107class RedirectEval(_ErrorWithLocation):
108 """Used in the CommandEvaluator.
109
110 A bad redirect causes the SimpleCommand to return with status 1. To
111 make it fatal, use set -o errexit.
112 """
113
114 def __init__(self, msg, location):
115 # type: (str, loc_t) -> None
116 _ErrorWithLocation.__init__(self, msg, location)
117
118
119class FatalRuntime(_ErrorWithLocation):
120 """An exception that propagates to the top level.
121
122 Used in the evaluators, and also also used in test builtin for
123 invalid argument.
124 """
125
126 def __init__(self, exit_status, msg, location):
127 # type: (int, str, loc_t) -> None
128 _ErrorWithLocation.__init__(self, msg, location)
129 self.exit_status = exit_status
130
131 def ExitStatus(self):
132 # type: () -> int
133 return self.exit_status
134
135
136class Strict(FatalRuntime):
137 """Depending on shell options, these errors may be caught and ignored.
138
139 For example, if options like these are ON:
140
141 set -o strict_arith
142 set -o strict_word_eval
143
144 then we re-raise the error so it's caught by the top level. Otherwise
145 we catch it and return a dummy value like '' or -1 (i.e. what bash commonly
146 does.)
147
148 TODO: Have levels, like:
149
150 OILS_STRICT_PRINT=2 # print warnings at level 2 and above
151 OILS_STRICT_DIE=1 # abort the program at level 1 and above
152 """
153
154 def __init__(self, msg, location):
155 # type: (str, loc_t) -> None
156 FatalRuntime.__init__(self, 1, msg, location)
157
158
159class ErrExit(FatalRuntime):
160 """For set -e.
161
162 Travels between WordEvaluator and CommandEvaluator.
163 """
164
165 def __init__(self, exit_status, msg, location, show_code=False):
166 # type: (int, str, loc_t, bool) -> None
167 FatalRuntime.__init__(self, exit_status, msg, location)
168 self.show_code = show_code
169
170
171class NoUnset(FatalRuntime):
172 """For set -u.
173
174 Is raised in WordEvaluator then caught in CommandEvaluator.
175 """
176
177 def __init__(self, exit_status, msg, location):
178 # type: (int, str, loc_t) -> None
179 FatalRuntime.__init__(self, exit_status, msg, location)
180
181
182class Expr(FatalRuntime):
183 """e.g. KeyError, IndexError, ZeroDivisionError."""
184
185 def __init__(self, msg, location):
186 # type: (str, loc_t) -> None
187
188 # Unique status of 3 for expression errors -- for both the caught and
189 # uncaught case.
190 #
191 # Caught: try sets _status register to 3
192 # Uncaught: shell exits with status 3
193 FatalRuntime.__init__(self, 3, msg, location)
194
195
196class Structured(FatalRuntime):
197 """An error that can be exposed via the _error Dict.
198
199 Including:
200 - Errors raised by the 'error' builtin
201 - J8 encode and decode errors.
202 """
203
204 def __init__(self, status, msg, location, properties=None):
205 # type: (int, str, loc_t, Dict[str, value_t]) -> None
206 FatalRuntime.__init__(self, status, msg, location)
207 self.properties = properties
208
209 def ToDict(self):
210 # type: () -> value.Dict
211
212 d = NewDict() # type: Dict[str, value_t]
213
214 # The _error Dict order is odd -- the optional properties come BEFORE
215 # required fields. We always want the required fields to be present so
216 # it makes sense.
217 if self.properties is not None:
218 d.update(self.properties)
219
220 # _error.code is better than _error.status
221 d['code'] = num.ToBig(self.ExitStatus())
222 d['message'] = value.Str(self.msg)
223
224 return value.Dict(d)
225
226
227class AssertionErr(Expr):
228 """An assertion."""
229
230 def __init__(self, msg, location):
231 # type: (str, loc_t) -> None
232 Expr.__init__(self, msg, location)
233
234
235class TypeErrVerbose(Expr):
236 """e.g. ~ on a bool or float, 'not' on an int."""
237
238 def __init__(self, msg, location):
239 # type: (str, loc_t) -> None
240 Expr.__init__(self, msg, location)
241
242
243class TypeErr(TypeErrVerbose):
244
245 def __init__(self, actual_val, msg, location):
246 # type: (value_t, str, loc_t) -> None
247 TypeErrVerbose.__init__(self,
248 "%s, got %s" % (msg, _ValType(actual_val)),
249 location)
250
251
252class Runtime(Exception):
253 """An error that's meant to be caught, i.e. it's non-fatal.
254
255 Thrown by core/state.py and caught by builtins
256 """
257
258 def __init__(self, msg):
259 # type: (str) -> None
260 self.msg = msg
261
262 def UserErrorString(self):
263 # type: () -> str
264 return self.msg
265
266
267class Decode(Exception):
268 """
269 List of J8 errors:
270 - message isn't UTF-8 - Id.Lit_Chars - need loc
271 - Invalid token Id.Unkown_Tok - need loc
272 - Unclosed double quote string -- need loc
273 - Parse error, e.g. [}{]
274
275 - Invalid escapes:
276 - b"" and u"" don't accept \\u1234
277 - u"" doesn't accept \\yff
278 - "" doesn't accept \\yff or \\u{123456}
279 """
280
281 def __init__(self, msg, s, start_pos, end_pos, line_num):
282 # type: (str, str, int, int, int) -> None
283 self.msg = msg
284 self.s = s # string being decoded
285 self.start_pos = start_pos
286 self.end_pos = end_pos
287 self.line_num = line_num
288
289 def Message(self):
290 # type: () -> str
291
292 # Show 10 chars of context for now
293 start = max(0, self.start_pos - 4)
294 end = min(len(self.s), self.end_pos + 4)
295
296 part = self.s[start:end]
297 return self.msg + ' (line %d, offset %d-%d: %r)' % (
298 self.line_num, self.start_pos, self.end_pos, part)
299
300 def __str__(self):
301 # type: () -> str
302 return self.Message()
303
304
305class Encode(Exception):
306 """
307 List of J8 encode errors:
308 - object cycle
309 - unprintable object like Eggex
310 When encoding JSON:
311 - binary data that can't be represented in JSON
312 - if using Unicode replacement char, then it won't fail
313 """
314
315 def __init__(self, msg):
316 # type: (str) -> None
317 self.msg = msg
318
319 def Message(self):
320 # type: () -> str
321 return self.msg
322
323
324def e_usage(msg, location):
325 # type: (str, loc_t) -> NoReturn
326 """Convenience wrapper for arg parsing / validation errors.
327
328 Usually causes a builtin to fail with status 2, but the script can continue
329 if 'set +o errexit'. Main programs like bin/ysh also use this.
330
331 Caught by
332
333 - RunAssignBuiltin and RunBuiltin, with optional LOCATION INFO
334 - various main() programs, without location info
335
336 Probably should separate these two cases?
337
338 - builtins pass Token() or loc::Missing()
339 - tool interfaces don't pass any location info
340 """
341 raise Usage(msg, location)
342
343
344def e_strict(msg, location):
345 # type: (str, loc_t) -> NoReturn
346 """Convenience wrapper for strictness errors.
347
348 Like e_die(), except the script MAY continue executing after these errors.
349
350 TODO: This could have a level too?
351 """
352 raise Strict(msg, location)
353
354
355def p_die(msg, location):
356 # type: (str, loc_t) -> NoReturn
357 """Convenience wrapper for parse errors.
358
359 Exits with status 2. See core/main_loop.py.
360 """
361 raise Parse(msg, location)
362
363
364def e_die(msg, location=None):
365 # type: (str, loc_t) -> NoReturn
366 """Convenience wrapper for fatal runtime errors.
367
368 Usually exits with status 1. See osh/cmd_eval.py.
369 """
370 raise FatalRuntime(1, msg, location)
371
372
373def e_die_status(status, msg, location=None):
374 # type: (int, str, loc_t) -> NoReturn
375 """Wrapper for C++ semantics.
376
377 Note that it doesn't take positional args, so you should use %
378 formatting.
379 """
380 raise FatalRuntime(status, msg, location)