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

382 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, msg, location):
178 # type: (str, loc_t) -> None
179
180 # exit status is always 1
181 FatalRuntime.__init__(self, 1, msg, location)
182
183
184class Expr(FatalRuntime):
185 """e.g. KeyError, IndexError, ZeroDivisionError."""
186
187 def __init__(self, msg, location):
188 # type: (str, loc_t) -> None
189
190 # Unique status of 3 for expression errors -- for both the caught and
191 # uncaught case.
192 #
193 # Caught: try sets _status register to 3
194 # Uncaught: shell exits with status 3
195 FatalRuntime.__init__(self, 3, msg, location)
196
197
198class Structured(FatalRuntime):
199 """An error that can be exposed via the _error Dict.
200
201 Including:
202 - Errors raised by the 'error' builtin
203 - J8 encode and decode errors.
204 """
205
206 def __init__(self, status, msg, location, properties=None):
207 # type: (int, str, loc_t, Dict[str, value_t]) -> None
208 FatalRuntime.__init__(self, status, msg, location)
209 self.properties = properties
210
211 def ToDict(self):
212 # type: () -> value.Dict
213
214 d = NewDict() # type: Dict[str, value_t]
215
216 # The _error Dict order is odd -- the optional properties come BEFORE
217 # required fields. We always want the required fields to be present so
218 # it makes sense.
219 if self.properties is not None:
220 d.update(self.properties)
221
222 # _error.code is better than _error.status
223 d['code'] = num.ToBig(self.ExitStatus())
224 d['message'] = value.Str(self.msg)
225
226 return value.Dict(d)
227
228
229class AssertionErr(Expr):
230 """An assertion."""
231
232 def __init__(self, msg, location):
233 # type: (str, loc_t) -> None
234 Expr.__init__(self, msg, location)
235
236
237class TypeErrVerbose(Expr):
238 """e.g. ~ on a bool or float, 'not' on an int."""
239
240 def __init__(self, msg, location):
241 # type: (str, loc_t) -> None
242 Expr.__init__(self, msg, location)
243
244
245class TypeErr(TypeErrVerbose):
246
247 def __init__(self, actual_val, msg, location):
248 # type: (value_t, str, loc_t) -> None
249 TypeErrVerbose.__init__(self,
250 "%s, got %s" % (msg, _ValType(actual_val)),
251 location)
252
253
254class Runtime(Exception):
255 """An error that's meant to be caught, i.e. it's non-fatal.
256
257 Thrown by core/state.py and caught by builtins
258 """
259
260 def __init__(self, msg):
261 # type: (str) -> None
262 self.msg = msg
263
264 def UserErrorString(self):
265 # type: () -> str
266 return self.msg
267
268
269class Decode(Exception):
270 """
271 List of J8 errors:
272 - message isn't UTF-8 - Id.Lit_Chars - need loc
273 - Invalid token Id.Unkown_Tok - need loc
274 - Unclosed double quote string -- need loc
275 - Parse error, e.g. [}{]
276
277 - Invalid escapes:
278 - b"" and u"" don't accept \\u1234
279 - u"" doesn't accept \\yff
280 - "" doesn't accept \\yff or \\u{123456}
281 """
282
283 def __init__(self, msg, s, start_pos, end_pos, line_num):
284 # type: (str, str, int, int, int) -> None
285 self.msg = msg
286 self.s = s # string being decoded
287 self.start_pos = start_pos
288 self.end_pos = end_pos
289 self.line_num = line_num
290
291 def Message(self):
292 # type: () -> str
293
294 # Show 10 chars of context for now
295 start = max(0, self.start_pos - 4)
296 end = min(len(self.s), self.end_pos + 4)
297
298 part = self.s[start:end]
299 return self.msg + ' (line %d, offset %d-%d: %r)' % (
300 self.line_num, self.start_pos, self.end_pos, part)
301
302 def __str__(self):
303 # type: () -> str
304 return self.Message()
305
306
307class Encode(Exception):
308 """
309 List of J8 encode errors:
310 - object cycle
311 - unprintable object like Eggex
312 When encoding JSON:
313 - binary data that can't be represented in JSON
314 - if using Unicode replacement char, then it won't fail
315 """
316
317 def __init__(self, msg):
318 # type: (str) -> None
319 self.msg = msg
320
321 def Message(self):
322 # type: () -> str
323 return self.msg
324
325
326def e_usage(msg, location):
327 # type: (str, loc_t) -> NoReturn
328 """Convenience wrapper for arg parsing / validation errors.
329
330 Usually causes a builtin to fail with status 2, but the script can continue
331 if 'set +o errexit'. Main programs like bin/ysh also use this.
332
333 Caught by
334
335 - RunAssignBuiltin and RunBuiltin, with optional LOCATION INFO
336 - various main() programs, without location info
337
338 Probably should separate these two cases?
339
340 - builtins pass Token() or loc::Missing()
341 - tool interfaces don't pass any location info
342 """
343 raise Usage(msg, location)
344
345
346def e_strict(msg, location):
347 # type: (str, loc_t) -> NoReturn
348 """Convenience wrapper for strictness errors.
349
350 Like e_die(), except the script MAY continue executing after these errors.
351
352 TODO: This could have a level too?
353 """
354 raise Strict(msg, location)
355
356
357def p_die(msg, location):
358 # type: (str, loc_t) -> NoReturn
359 """Convenience wrapper for parse errors.
360
361 Exits with status 2. See core/main_loop.py.
362 """
363 raise Parse(msg, location)
364
365
366def e_die(msg, location=None):
367 # type: (str, loc_t) -> NoReturn
368 """Convenience wrapper for fatal runtime errors.
369
370 Usually exits with status 1. See osh/cmd_eval.py.
371 """
372 raise FatalRuntime(1, msg, location)
373
374
375def e_die_status(status, msg, location=None):
376 # type: (int, str, loc_t) -> NoReturn
377 """Wrapper for C++ semantics.
378
379 Note that it doesn't take positional args, so you should use %
380 formatting.
381 """
382 raise FatalRuntime(status, msg, location)