pureshellcheck 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ """AST node model for shell scripts.
2
+
3
+ Node kinds mirror ShellCheck's AST constructors (T_SimpleCommand,
4
+ T_DollarBraced, ...) so that check logic ports naturally. Nodes are generic
5
+ objects with a `kind` tag plus per-kind fields stored in a dict and exposed
6
+ as attributes.
7
+
8
+ Positions `pos`/`end` are absolute character offsets into the source; use
9
+ `Positions` to translate to 1-based (line, column).
10
+ """
11
+
12
+ import bisect
13
+
14
+
15
+ class Node:
16
+ __slots__ = ("kind", "pos", "end", "parent", "fields")
17
+
18
+ def __init__(self, kind, pos, end, **fields):
19
+ self.kind = kind
20
+ self.pos = pos
21
+ self.end = end
22
+ self.parent = None
23
+ self.fields = fields
24
+
25
+ def __getattr__(self, name):
26
+ try:
27
+ return self.fields[name]
28
+ except KeyError:
29
+ raise AttributeError("%s node has no field %r" % (self.kind, name))
30
+
31
+ def get(self, name, default=None):
32
+ return self.fields.get(name, default)
33
+
34
+ def __repr__(self):
35
+ inner = ", ".join(
36
+ "%s=%r" % (k, v) for k, v in self.fields.items() if k != "parent"
37
+ )
38
+ return "%s(%s)" % (self.kind, inner)
39
+
40
+
41
+ # Children traversal: every node field that may hold Node or [Node] or
42
+ # [[Node]] is walked generically.
43
+
44
+ def iter_children(node):
45
+ for value in node.fields.values():
46
+ if isinstance(value, Node):
47
+ yield value
48
+ elif isinstance(value, list):
49
+ for item in value:
50
+ if isinstance(item, Node):
51
+ yield item
52
+ elif isinstance(item, list):
53
+ for sub in item:
54
+ if isinstance(sub, Node):
55
+ yield sub
56
+ elif isinstance(item, tuple):
57
+ for sub in item:
58
+ if isinstance(sub, Node):
59
+ yield sub
60
+ elif isinstance(sub, list):
61
+ for s2 in sub:
62
+ if isinstance(s2, Node):
63
+ yield s2
64
+ elif isinstance(value, tuple):
65
+ for item in value:
66
+ if isinstance(item, Node):
67
+ yield item
68
+
69
+
70
+ def walk(node):
71
+ """Yield node and all descendants in document order."""
72
+ stack = [node]
73
+ while stack:
74
+ n = stack.pop()
75
+ yield n
76
+ children = list(iter_children(n))
77
+ children.reverse()
78
+ stack.extend(children)
79
+
80
+
81
+ def set_parents(root):
82
+ for n in walk(root):
83
+ for c in iter_children(n):
84
+ c.parent = n
85
+
86
+
87
+ def ancestors(node):
88
+ n = node.parent
89
+ while n is not None:
90
+ yield n
91
+ n = n.parent
92
+
93
+
94
+ class Positions:
95
+ """Translate absolute offsets to 1-based (line, col)."""
96
+
97
+ def __init__(self, source):
98
+ self.line_starts = [0]
99
+ idx = source.find("\n")
100
+ while idx != -1:
101
+ self.line_starts.append(idx + 1)
102
+ idx = source.find("\n", idx + 1)
103
+
104
+ def line_col(self, offset):
105
+ line = bisect.bisect_right(self.line_starts, offset)
106
+ return line, offset - self.line_starts[line - 1] + 1
@@ -0,0 +1,584 @@
1
+ """Execution-order variable flow engine.
2
+
3
+ Walks the AST in rough execution order, tracking each variable's "space
4
+ status" (EMPTY / CLEAN / DIRTY) and integer attribute, calling a callback at
5
+ every parameter expansion with the state at that point. This is a pragmatic
6
+ reimplementation of the value tracking ShellCheck performs on its CFG; it
7
+ handles straight-line code, branches (worst-case merge), loops (single pass),
8
+ subshell isolation, function definitions/calls with dynamic scoping and
9
+ `local`, and branch-exit pruning (`if x; then v='a b'; exit; fi`).
10
+ """
11
+
12
+ from .astlib import (
13
+ CLEAN, DIRTY, EMPTY, SPECIAL_INTEGER_VARIABLES,
14
+ VARIABLES_WITHOUT_SPACES, braced_reference, join_status,
15
+ literal_space_status, merge_status,
16
+ )
17
+ from .parser import literal_text
18
+ from .shast import walk
19
+
20
+ EXIT_COMMANDS = {"exit", "return"}
21
+
22
+ DECLARING_COMMANDS = {"declare", "typeset", "local", "export", "readonly"}
23
+
24
+
25
+ class VarInfo:
26
+ __slots__ = ("status", "integer")
27
+
28
+ def __init__(self, status, integer=False):
29
+ self.status = status
30
+ self.integer = integer
31
+
32
+
33
+ class Scope:
34
+ """One level of variable scope (global or function-local)."""
35
+
36
+ __slots__ = ("vars",)
37
+
38
+ def __init__(self, vars=None):
39
+ self.vars = vars if vars is not None else {}
40
+
41
+
42
+ class FuncDef:
43
+ __slots__ = ("node", "conditional", "walked")
44
+
45
+ def __init__(self, node, conditional):
46
+ self.node = node
47
+ self.conditional = conditional
48
+ self.walked = False
49
+
50
+
51
+ class VarFlow:
52
+ """on_reference(braced_node, name, status, integer) is called for every
53
+ T_DollarBraced in execution order."""
54
+
55
+ def __init__(self, on_reference=None, shell="bash", on_assign=None):
56
+ self.on_reference = on_reference or (lambda *a: None)
57
+ self.on_assign = on_assign or (lambda *a: None)
58
+ self.shell = shell
59
+ self.scopes = [Scope()]
60
+ self.functions = {}
61
+ self.call_stack = []
62
+ self.conditional_depth = 0
63
+
64
+ # -- scope management ------------------------------------------------
65
+
66
+ def lookup(self, name):
67
+ for scope in reversed(self.scopes):
68
+ info = scope.vars.get(name)
69
+ if info is not None:
70
+ return info
71
+ return None
72
+
73
+ def assign(self, name, status, integer=None, local=False, global_=False):
74
+ if global_:
75
+ scope = self.scopes[0]
76
+ elif local:
77
+ scope = self.scopes[-1]
78
+ else:
79
+ scope = self.scopes[0]
80
+ for s in reversed(self.scopes):
81
+ if name in s.vars:
82
+ scope = s
83
+ break
84
+ old = scope.vars.get(name)
85
+ if integer is None:
86
+ integer = old.integer if old is not None else False
87
+ elif self.conditional_depth:
88
+ # attribute only maybe applied: keep the weaker assumption
89
+ integer = integer and (old.integer if old is not None else False)
90
+ if integer and status == DIRTY:
91
+ status = CLEAN
92
+ if self.conditional_depth and old is not None:
93
+ status = merge_status(old.status, status)
94
+ elif self.conditional_depth and old is None:
95
+ status = DIRTY
96
+ scope.vars[name] = VarInfo(status, integer)
97
+
98
+ def snapshot(self):
99
+ return [dict((k, VarInfo(v.status, v.integer))
100
+ for k, v in s.vars.items()) for s in self.scopes]
101
+
102
+ def restore(self, snap):
103
+ for scope, vars_ in zip(self.scopes, snap):
104
+ scope.vars = vars_
105
+
106
+ def merge_snapshots(self, snaps):
107
+ """Merge variable states from multiple branches (worst case)."""
108
+ merged = []
109
+ for level in range(len(self.scopes)):
110
+ allnames = set()
111
+ for snap in snaps:
112
+ allnames.update(snap[level])
113
+ vars_ = {}
114
+ for name in allnames:
115
+ infos = [snap[level].get(name) for snap in snaps]
116
+ status = None
117
+ integer = True
118
+ for info in infos:
119
+ s = info.status if info is not None else DIRTY
120
+ i = info.integer if info is not None else False
121
+ status = s if status is None else merge_status(status, s)
122
+ integer = integer and i
123
+ vars_[name] = VarInfo(status, integer)
124
+ merged.append(vars_)
125
+ self.restore(merged)
126
+
127
+ # -- value evaluation --------------------------------------------------
128
+
129
+ def ref_status(self, name):
130
+ if name in SPECIAL_INTEGER_VARIABLES:
131
+ return CLEAN, True
132
+ if name in VARIABLES_WITHOUT_SPACES:
133
+ return CLEAN, False
134
+ if name in ("@", "*") or name.isdigit():
135
+ return DIRTY, False
136
+ info = self.lookup(name)
137
+ if info is None:
138
+ return DIRTY, False
139
+ if info.integer:
140
+ return CLEAN, True
141
+ return info.status, False
142
+
143
+ def word_status(self, word):
144
+ """SpaceStatus of a word's value (assignment RHS semantics)."""
145
+ if word is None:
146
+ return EMPTY
147
+ total = EMPTY
148
+ for part in self._value_parts(word):
149
+ total = join_status(total, self._part_status(part))
150
+ return total
151
+
152
+ def _value_parts(self, word):
153
+ if word.kind == "T_NormalWord":
154
+ return word.parts
155
+ return [word]
156
+
157
+ def _part_status(self, part):
158
+ k = part.kind
159
+ if k == "T_Literal":
160
+ return literal_space_status(part.text)
161
+ if k in ("T_SingleQuoted", "T_DollarSingleQuoted"):
162
+ return literal_space_status(part.text) if part.text else EMPTY
163
+ if k in ("T_DoubleQuoted", "T_DollarDoubleQuoted"):
164
+ total = EMPTY
165
+ for q in part.parts:
166
+ total = join_status(total, self._part_status(q))
167
+ return total
168
+ if k == "T_DollarBraced":
169
+ content = part.content
170
+ name = braced_reference(content)
171
+ if content.startswith("#"):
172
+ return CLEAN
173
+ status, _ = self.ref_status(name)
174
+ return status
175
+ if k in ("T_DollarExpansion", "T_Backticked",
176
+ "T_DollarBraceCommandExpansion"):
177
+ return DIRTY
178
+ if k == "T_DollarArithmetic":
179
+ return CLEAN
180
+ if k in ("T_Glob", "T_Extglob", "T_BraceExpansion"):
181
+ return DIRTY
182
+ if k == "T_ProcSub":
183
+ return CLEAN
184
+ return DIRTY
185
+
186
+ # -- main walk ---------------------------------------------------------
187
+
188
+ def run(self, root):
189
+ self.process_statements(root.commands)
190
+ # walk function bodies that were never called, with the final state
191
+ for name, fd in sorted(self.functions.items()):
192
+ if not fd.walked:
193
+ self.call_function(fd, simulate_only=True)
194
+
195
+ def process_statements(self, statements):
196
+ """Returns True if execution definitely exits within the block."""
197
+ for i, stmt in enumerate(statements):
198
+ if self.process(stmt):
199
+ return True
200
+ return False
201
+
202
+ def process(self, node):
203
+ """Process one statement; returns True if it definitely exits."""
204
+ k = node.kind
205
+ method = getattr(self, "_do_" + k, None)
206
+ if method is not None:
207
+ return method(node)
208
+ # generic: walk children as statements/expressions
209
+ self.visit_word(node)
210
+ return False
211
+
212
+ # each _do_ method returns exits:bool
213
+
214
+ def _do_T_SimpleCommand(self, node):
215
+ cmd_name = None
216
+ if node.words:
217
+ cmd_name = literal_text(node.words[0])
218
+ if cmd_name:
219
+ cmd_name = cmd_name.rsplit("/", 1)[-1]
220
+ is_declaring = cmd_name in DECLARING_COMMANDS
221
+ flags = ""
222
+ unflags = ""
223
+ if is_declaring:
224
+ for w in node.words[1:]:
225
+ t = literal_text(w)
226
+ if t and t.startswith("-"):
227
+ flags += t[1:]
228
+ elif t and t.startswith("+"):
229
+ unflags += t[1:]
230
+ in_function = len(self.scopes) > 1
231
+ is_local = (cmd_name in ("local", "declare", "typeset")
232
+ and in_function and "g" not in flags)
233
+ integer = True if "i" in flags else (False if "i" in unflags
234
+ else None)
235
+
236
+ for assign in node.assigns:
237
+ self.visit_word(assign.get("value"))
238
+ for idx in assign.get("indices", ()):
239
+ self.visit_word(idx)
240
+ value = assign.get("value")
241
+ if value is not None and value.kind == "T_Array":
242
+ status = EMPTY
243
+ for el in value.elements:
244
+ v = el.value if el.kind == "T_IndexedElement" else el
245
+ status = merge_status(status, self.word_status(v)) \
246
+ if status != EMPTY else self.word_status(v)
247
+ else:
248
+ status = self.word_status(value)
249
+ if assign.get("append"):
250
+ old = self.lookup(assign.name)
251
+ if old is not None:
252
+ status = join_status(old.status, status)
253
+ self.assign(assign.name, status, integer=integer,
254
+ local=is_local, global_="g" in flags)
255
+ self.on_assign(assign.name, value, assign)
256
+
257
+ for w in node.words:
258
+ self.visit_word(w)
259
+ for r in node.get("redirects", ()):
260
+ self.visit_word(r)
261
+ self._apply_redirect_assign(r)
262
+
263
+ if is_declaring:
264
+ # bare names: declare -x NAME / local NAME
265
+ for w in node.words[1:]:
266
+ t = literal_text(w)
267
+ if t and not t.startswith("-") and not t.startswith("+") \
268
+ and "=" not in t and t.isidentifier():
269
+ if is_local:
270
+ self.assign(t, EMPTY, integer=integer, local=True)
271
+ elif cmd_name in ("export", "readonly") or "x" in flags:
272
+ self.assign(t, DIRTY, integer=integer)
273
+ elif integer is not None:
274
+ old = self.lookup(t)
275
+ self.assign(t, old.status if old else EMPTY,
276
+ integer=integer)
277
+ elif cmd_name == "read":
278
+ self._apply_read(node)
279
+ elif cmd_name in ("mapfile", "readarray"):
280
+ args = [literal_text(w) for w in node.words[1:]]
281
+ names = [a for a in args if a and not a.startswith("-")]
282
+ if names:
283
+ self.assign(names[-1], DIRTY)
284
+ elif cmd_name == "getopts":
285
+ args = [w for w in node.words[1:]]
286
+ if len(args) >= 2:
287
+ t = literal_text(args[1])
288
+ if t:
289
+ self.assign(t, CLEAN)
290
+ elif cmd_name == "wait":
291
+ args = [literal_text(w) for w in node.words[1:]]
292
+ for i, a in enumerate(args):
293
+ if a == "-p" and i + 1 < len(args) and args[i + 1]:
294
+ self.assign(args[i + 1], CLEAN, integer=True)
295
+ elif cmd_name == "printf":
296
+ args = [literal_text(w) for w in node.words[1:]]
297
+ for i, a in enumerate(args):
298
+ if a == "-v" and i + 1 < len(args) and args[i + 1]:
299
+ self.assign(args[i + 1], DIRTY)
300
+ elif cmd_name == "unset":
301
+ for w in node.words[1:]:
302
+ t = literal_text(w)
303
+ if t and not t.startswith("-"):
304
+ self.assign(t.split("[", 1)[0], DIRTY)
305
+ elif cmd_name in EXIT_COMMANDS:
306
+ return True
307
+ elif cmd_name == "exec" and len(node.words) > 1:
308
+ return True
309
+ elif cmd_name in self.functions:
310
+ self.call_function(self.functions[cmd_name])
311
+ return False
312
+
313
+ def _apply_read(self, node):
314
+ names = []
315
+ args = node.words[1:]
316
+ skip_next = False
317
+ for i, w in enumerate(args):
318
+ t = literal_text(w)
319
+ if skip_next:
320
+ skip_next = False
321
+ if t:
322
+ names.append(t)
323
+ continue
324
+ if t and t.startswith("-"):
325
+ for opt in ("a", "d", "i", "n", "N", "p", "t", "u"):
326
+ if t.endswith(opt):
327
+ skip_next = opt == "a"
328
+ break
329
+ if t.endswith("-a"):
330
+ skip_next = True
331
+ continue
332
+ if t:
333
+ names.append(t)
334
+ if not names:
335
+ names = ["REPLY"]
336
+ for n in names:
337
+ self.assign(n.split("[", 1)[0], DIRTY)
338
+
339
+ def _apply_redirect_assign(self, redirect):
340
+ fd = redirect.get("fd")
341
+ if fd and fd.startswith("{") and fd.endswith("}"):
342
+ self.assign(fd[1:-1], CLEAN, integer=True)
343
+
344
+ def _do_T_Pipeline(self, node):
345
+ for cmd in node.commands:
346
+ snap = self.snapshot()
347
+ self.process(cmd)
348
+ self.restore(snap)
349
+ return False
350
+
351
+ def _do_T_Banged(self, node):
352
+ return self.process(node.command)
353
+
354
+ def _do_T_Timed(self, node):
355
+ return self.process(node.command)
356
+
357
+ def _do_T_Backgrounded(self, node):
358
+ snap = self.snapshot()
359
+ self.process(node.command)
360
+ self.restore(snap)
361
+ return False
362
+
363
+ def _do_T_AndIf(self, node):
364
+ self.process(node.left)
365
+ self.conditional_depth += 1
366
+ try:
367
+ self.process(node.right)
368
+ finally:
369
+ self.conditional_depth -= 1
370
+ return False
371
+
372
+ _do_T_OrIf = _do_T_AndIf
373
+
374
+ def _do_T_IfExpression(self, node):
375
+ snaps = []
376
+ for cond, body in node.branches:
377
+ self.process_statements(cond)
378
+ snap_before = self.snapshot()
379
+ exited = self.process_statements(body)
380
+ if not exited:
381
+ snaps.append(self.snapshot())
382
+ self.restore(snap_before)
383
+ else_body = node.else_body
384
+ if else_body:
385
+ snap_before = self.snapshot()
386
+ exited = self.process_statements(else_body)
387
+ if not exited:
388
+ snaps.append(self.snapshot())
389
+ self.restore(snap_before)
390
+ else:
391
+ snaps.append(self.snapshot())
392
+ if snaps:
393
+ self.merge_snapshots(snaps)
394
+ return False
395
+ return True # all branches exited and else missing? keep current
396
+
397
+ def _do_T_WhileExpression(self, node):
398
+ self.process_statements(node.condition)
399
+ before = self.snapshot()
400
+ self.process_statements(node.body)
401
+ self.merge_snapshots([before, self.snapshot()])
402
+ return False
403
+
404
+ _do_T_UntilExpression = _do_T_WhileExpression
405
+
406
+ def _do_T_ForIn(self, node):
407
+ status = None
408
+ for w in node.words:
409
+ self.visit_word(w)
410
+ s = self.word_status(w)
411
+ status = s if status is None else merge_status(status, s)
412
+ if not node.has_in or status is None:
413
+ status = DIRTY # implicit "$@"
414
+ self.assign(node.variable, status)
415
+ before = self.snapshot()
416
+ self.process_statements(node.body)
417
+ self.merge_snapshots([before, self.snapshot()])
418
+ return False
419
+
420
+ _do_T_SelectIn = _do_T_ForIn
421
+
422
+ def _do_T_ForArithmetic(self, node):
423
+ self.visit_arith(node.init)
424
+ self.visit_arith(node.condition)
425
+ before = self.snapshot()
426
+ self.process_statements(node.body)
427
+ self.visit_arith(node.update)
428
+ self.merge_snapshots([before, self.snapshot()])
429
+ return False
430
+
431
+ def _do_T_CaseExpression(self, node):
432
+ self.visit_word(node.word)
433
+ snaps = [self.snapshot()]
434
+ for item in node.items:
435
+ for p in item.patterns:
436
+ self.visit_word(p)
437
+ before = self.snapshot()
438
+ exited = self.process_statements(item.body)
439
+ if not exited:
440
+ snaps.append(self.snapshot())
441
+ self.restore(before)
442
+ self.merge_snapshots(snaps)
443
+ return False
444
+
445
+ def _do_T_BraceGroup(self, node):
446
+ return self.process_statements(node.commands)
447
+
448
+ def _do_T_Subshell(self, node):
449
+ snap = self.snapshot()
450
+ self.process_statements(node.commands)
451
+ self.restore(snap)
452
+ return False
453
+
454
+ def _do_T_Condition(self, node):
455
+ self.visit_word(node.expr)
456
+ return False
457
+
458
+ def _do_T_Arithmetic(self, node):
459
+ self.visit_arith(node.expr)
460
+ return False
461
+
462
+ def _do_T_Function(self, node):
463
+ conditional = self.conditional_depth > 0
464
+ self.functions[node.name] = FuncDef(node, conditional)
465
+ return False
466
+
467
+ def _do_T_BatsTest(self, node):
468
+ self.assign("status", CLEAN, integer=True)
469
+ self.assign("output", DIRTY)
470
+ self.assign("lines", DIRTY)
471
+ self.assign("stderr", DIRTY)
472
+ snap = self.snapshot()
473
+ self.scopes.append(Scope())
474
+ try:
475
+ self.process_statements(node.body.commands)
476
+ finally:
477
+ self.scopes.pop()
478
+ self.restore(snap)
479
+ return False
480
+
481
+ def _do_T_CoProc(self, node):
482
+ snap = self.snapshot()
483
+ self.process(node.command)
484
+ self.restore(snap)
485
+ return False
486
+
487
+ def call_function(self, fd, simulate_only=False):
488
+ if fd.node in [f for f in self.call_stack]:
489
+ return
490
+ fd.walked = True
491
+ self.call_stack.append(fd.node)
492
+ snap = self.snapshot() if (fd.conditional or simulate_only) else None
493
+ self.scopes.append(Scope())
494
+ try:
495
+ body = fd.node.body
496
+ if body.kind == "T_BraceGroup":
497
+ self.process_statements(body.commands)
498
+ else:
499
+ self.process(body)
500
+ finally:
501
+ self.scopes.pop()
502
+ self.call_stack.pop()
503
+ if snap is not None:
504
+ if fd.conditional and not simulate_only:
505
+ self.merge_snapshots([snap, self.snapshot()])
506
+ else:
507
+ self.restore(snap)
508
+
509
+ # -- expression-level walking ------------------------------------------
510
+
511
+ def visit_word(self, node):
512
+ """Walk any non-statement subtree, processing references and nested
513
+ command substitutions."""
514
+ if node is None or isinstance(node, str):
515
+ return
516
+ k = node.kind
517
+ if k == "T_DollarBraced":
518
+ name = braced_reference(node.content)
519
+ status, integer = self.ref_status(name)
520
+ self.on_reference(node, name, status, integer)
521
+ for p in node.get("arg_parts", ()):
522
+ self.visit_word(p)
523
+ for idx in node.get("indices", ()):
524
+ self.visit_word(idx)
525
+ return
526
+ if k in ("T_DollarExpansion", "T_Backticked", "T_ProcSub",
527
+ "T_DollarBraceCommandExpansion"):
528
+ snap = self.snapshot()
529
+ self.process_statements(node.commands)
530
+ self.restore(snap)
531
+ return
532
+ if k in ("T_DollarArithmetic",):
533
+ self.visit_arith(node.expr)
534
+ return
535
+ if k in ("TA_Sequence", "TA_Assignment", "TA_Binary", "TA_Unary",
536
+ "TA_Trinary", "TA_Variable", "TA_Parenthesis",
537
+ "TA_Expansion", "TA_Literal", "TA_Empty"):
538
+ self.visit_arith(node)
539
+ return
540
+ if k == "T_FdRedirect":
541
+ op = node.op
542
+ if isinstance(op, str):
543
+ return
544
+ self.visit_word(op)
545
+ self._apply_redirect_assign(node)
546
+ return
547
+ from .shast import iter_children
548
+ for c in iter_children(node):
549
+ self.visit_word(c)
550
+
551
+ def visit_arith(self, node):
552
+ if node is None:
553
+ return
554
+ k = node.kind
555
+ if k == "TA_Variable":
556
+ for idx in node.get("indices", ()):
557
+ self.visit_word(idx)
558
+ return
559
+ if k == "TA_Assignment":
560
+ self.visit_arith(node.right)
561
+ left = node.left
562
+ if left.kind == "TA_Variable":
563
+ self.assign(left.name, CLEAN)
564
+ else:
565
+ self.visit_arith(left)
566
+ return
567
+ if k == "TA_Unary":
568
+ op = node.op.lstrip("|")
569
+ operand = node.operand
570
+ if op in ("++", "--") and operand.kind == "TA_Variable":
571
+ self.assign(operand.name, CLEAN)
572
+ else:
573
+ self.visit_arith(operand)
574
+ return
575
+ if k == "TA_Expansion":
576
+ for p in node.parts:
577
+ self.visit_word(p)
578
+ return
579
+ from .shast import iter_children
580
+ for c in iter_children(node):
581
+ if c.kind.startswith("TA_"):
582
+ self.visit_arith(c)
583
+ else:
584
+ self.visit_word(c)