sh-ast 0.0.0 → 0.1.0

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.
Files changed (73) hide show
  1. package/README.md +147 -0
  2. package/dist/analyze/ansi-c-escapes.d.ts +53 -0
  3. package/dist/analyze/ansi-c-escapes.d.ts.map +1 -0
  4. package/dist/analyze/ansi-c-escapes.js +255 -0
  5. package/dist/analyze/ansi-c-escapes.js.map +1 -0
  6. package/dist/analyze/decode-lit.d.ts +74 -0
  7. package/dist/analyze/decode-lit.d.ts.map +1 -0
  8. package/dist/analyze/decode-lit.js +114 -0
  9. package/dist/analyze/decode-lit.js.map +1 -0
  10. package/dist/analyze/enumerate-commands.d.ts +159 -0
  11. package/dist/analyze/enumerate-commands.d.ts.map +1 -0
  12. package/dist/analyze/enumerate-commands.js +390 -0
  13. package/dist/analyze/enumerate-commands.js.map +1 -0
  14. package/dist/analyze/index.d.ts +18 -0
  15. package/dist/analyze/index.d.ts.map +1 -0
  16. package/dist/analyze/index.js +27 -0
  17. package/dist/analyze/index.js.map +1 -0
  18. package/dist/analyze/node-helpers.d.ts +28 -0
  19. package/dist/analyze/node-helpers.d.ts.map +1 -0
  20. package/dist/analyze/node-helpers.js +37 -0
  21. package/dist/analyze/node-helpers.js.map +1 -0
  22. package/dist/analyze/resolve-word.d.ts +146 -0
  23. package/dist/analyze/resolve-word.d.ts.map +1 -0
  24. package/dist/analyze/resolve-word.js +202 -0
  25. package/dist/analyze/resolve-word.js.map +1 -0
  26. package/dist/analyze.d.ts +390 -0
  27. package/dist/deep-freeze.d.ts +14 -0
  28. package/dist/deep-freeze.d.ts.map +1 -0
  29. package/dist/deep-freeze.js +25 -0
  30. package/dist/deep-freeze.js.map +1 -0
  31. package/dist/errors.d.ts +115 -0
  32. package/dist/errors.d.ts.map +1 -0
  33. package/dist/errors.js +106 -0
  34. package/dist/errors.js.map +1 -0
  35. package/dist/index.d.ts +18 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +5 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/normalize.d.ts +52 -0
  40. package/dist/normalize.d.ts.map +1 -0
  41. package/dist/normalize.js +260 -0
  42. package/dist/normalize.js.map +1 -0
  43. package/dist/parse.d.ts +45 -0
  44. package/dist/parse.d.ts.map +1 -0
  45. package/dist/parse.js +118 -0
  46. package/dist/parse.js.map +1 -0
  47. package/dist/sh-ast.d.ts +1270 -0
  48. package/dist/types.d.ts +70 -0
  49. package/dist/types.d.ts.map +1 -0
  50. package/dist/types.js +2 -0
  51. package/dist/types.js.map +1 -0
  52. package/dist/visitor-keys.d.ts +21 -0
  53. package/dist/visitor-keys.d.ts.map +1 -0
  54. package/dist/visitor-keys.js +23 -0
  55. package/dist/visitor-keys.js.map +1 -0
  56. package/dist/walk.d.ts +18 -0
  57. package/dist/walk.d.ts.map +1 -0
  58. package/dist/walk.js +40 -0
  59. package/dist/walk.js.map +1 -0
  60. package/dist/wasm-instance.d.ts +13 -0
  61. package/dist/wasm-instance.d.ts.map +1 -0
  62. package/dist/wasm-instance.js +88 -0
  63. package/dist/wasm-instance.js.map +1 -0
  64. package/generated/child-type-schema.d.ts +14 -0
  65. package/generated/child-type-schema.js +188 -0
  66. package/generated/node-types.d.ts +958 -0
  67. package/generated/position-fields.d.ts +21 -0
  68. package/generated/position-fields.js +50 -0
  69. package/generated/visitor-keys.d.ts +11 -0
  70. package/generated/visitor-keys.js +50 -0
  71. package/package.json +93 -10
  72. package/shim/sh-ast.wasm +0 -0
  73. package/shim/wasm_exec.js +654 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode-lit.js","sourceRoot":"","sources":["../../src/analyze/decode-lit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,mBAAmB,GAAwB,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAErE,uGAAuG;AACvG,MAAM,kBAAkB,GAAwB,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAwBpE;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,QAAQ,GAAG,EAAE,CAAC;IAClB,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QACtB,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QAClB,IAAI,EAAE,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACxB,IAAI,IAAI,IAAI,CAAC;YACb,CAAC,IAAI,CAAC,CAAC;YACP,SAAS;QACX,CAAC;QACD,IAAI,mBAAmB,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YAChC,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC;QACD,IAAI,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YAC/B,QAAQ,IAAI,EAAE,CAAC;QACjB,CAAC;QACD,IAAI,IAAI,EAAE,CAAC;QACX,CAAC,IAAI,CAAC,CAAC;IACT,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;AACrC,CAAC;AAED,uFAAuF;AACvF,MAAM,mBAAmB,GAAwB,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;AAEhF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QACtB,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QAClB,IAAI,EAAE,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACxB,IAAI,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBAClC,IAAI,IAAI,IAAI,CAAC;gBACb,CAAC,IAAI,CAAC,CAAC;gBACP,SAAS;YACX,CAAC;YACD,oEAAoE;YACpE,IAAI,IAAI,EAAE,CAAC;YACX,CAAC,IAAI,CAAC,CAAC;YACP,SAAS;QACX,CAAC;QACD,IAAI,IAAI,EAAE,CAAC;QACX,CAAC,IAAI,CAAC,CAAC;IACT,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,159 @@
1
+ import type { ShNode } from '../types.js';
2
+ import type { WordResolution } from './resolve-word.js';
3
+ /**
4
+ * A single frame of the path from the root of the tree down to a
5
+ * {@link CommandSite}, describing *how* the command is reached — never
6
+ * whether it is safe to run. `CommandSite.context` is an ordered stack of
7
+ * these, outermost frame first:
8
+ *
9
+ * - `'and'`/`'or'` (`side: 'right'`) — the right-hand operand of a
10
+ * `BinaryCmd` (mvdan/sh's node for both `&&` and `||`); only the right
11
+ * side is tagged; the left side inherits the surrounding context
12
+ * unchanged, since it runs unconditionally relative to this operator.
13
+ * - `'pipeline'` (`stage: n`) — one stage of a `|`/`|&` chain (also a
14
+ * `BinaryCmd`, left-associatively nested by mvdan/sh); every stage is
15
+ * tagged, 0-indexed left to right, regardless of whether the chain mixes
16
+ * `|` and `|&`.
17
+ * - `'subshell'` — inside a `Subshell` (`( ... )`).
18
+ * - `'cmdSubst'`/`'procSubst'` — inside a `CmdSubst` (`$(...)`/backticks) or
19
+ * `ProcSubst` (`<(...)`/`>(...)`) reached from *any* word-bearing
20
+ * position (an argument, a redirection target, a case subject, a loop's
21
+ * word list, an assignment value, a test/arithmetic operand, …) — not
22
+ * only from `CallExpr.args`.
23
+ * - `'if'` (`branch: 'cond' | 'then' | 'else'`) — inside an `IfClause`'s
24
+ * condition, then-branch, or else-branch (mvdan/sh nests `elif` chains as
25
+ * `IfClause.else` pointing to another `IfClause`, so an `elif`'s own
26
+ * condition/then are reached through an `{kind:'if',branch:'else'}` frame
27
+ * first, then their own `'cond'`/`'then'` frame — reflecting the real
28
+ * nesting rather than collapsing it).
29
+ * - `'case'` — inside one `CaseClause` branch's statement list (not the
30
+ * case subject word or the patterns).
31
+ * - `'loop'` (`role` is `'body'` or `'cond'`) — inside a `ForClause`'s
32
+ * statement list (`role: 'body'` only — a `for` loop has no
33
+ * statement-list condition) or a `WhileClause`'s (`role: 'cond'` for the
34
+ * condition, `role: 'body'` for the loop body).
35
+ * - `'function'` (`name`) — inside a `FuncDecl`'s body; `name` is the
36
+ * function's literal name text.
37
+ * - `'background'`/`'negated'` — the enclosing `Stmt` has mvdan/sh's
38
+ * Background/Negated flag set (`cmd &`, `! cmd`).
39
+ * - `'coproc'` — inside a `CoprocClause`'s statement (a `coproc` block,
40
+ * optionally named).
41
+ *
42
+ * A `Block` grouping (`{ ...; }`) and a `TimeClause` (`time cmd`) are
43
+ * deliberately transparent — grouping and timing a command doesn't change
44
+ * how it's reached, so no frame is added for either.
45
+ *
46
+ * This union may grow in a **minor** release — a future mvdan/sh grammar
47
+ * construct this module starts modeling can add a new `kind` variant
48
+ * without that being a breaking change (mirroring
49
+ * {@link WordResolutionReason}'s semver policy in `resolve-word.ts`). It is
50
+ * deliberately not sealed against extension elsewhere in the codebase.
51
+ * Every existing variant's shape (its extra fields, if any) is stable —
52
+ * only new variants are ever added — so an exhaustive compile-time
53
+ * `switch` over `.kind` should still include a `default` case to stay
54
+ * forward-compatible.
55
+ *
56
+ * @public
57
+ */
58
+ export type CommandContext = {
59
+ readonly kind: 'and';
60
+ readonly side: 'right';
61
+ } | {
62
+ readonly kind: 'or';
63
+ readonly side: 'right';
64
+ } | {
65
+ readonly kind: 'pipeline';
66
+ readonly stage: number;
67
+ } | {
68
+ readonly kind: 'subshell';
69
+ } | {
70
+ readonly kind: 'cmdSubst';
71
+ } | {
72
+ readonly kind: 'procSubst';
73
+ } | {
74
+ readonly kind: 'if';
75
+ readonly branch: 'then' | 'else' | 'cond';
76
+ } | {
77
+ readonly kind: 'case';
78
+ } | {
79
+ readonly kind: 'loop';
80
+ readonly role: 'body' | 'cond';
81
+ } | {
82
+ readonly kind: 'function';
83
+ readonly name: string;
84
+ } | {
85
+ readonly kind: 'background';
86
+ } | {
87
+ readonly kind: 'negated';
88
+ } | {
89
+ readonly kind: 'coproc';
90
+ };
91
+ /**
92
+ * One place in the tree where a command is actually invoked — a `CallExpr`
93
+ * node, together with its resolved words and the path used to reach it.
94
+ * Facts only, matching {@link resolveWord}'s posture: no safety verdict, no
95
+ * hardcoded command/wrapper list. A dynamic (`static: false`) `argv0` is a
96
+ * normal, expected result — not an error and not itself reported as
97
+ * "unknown"/"unsafe".
98
+ *
99
+ * @public
100
+ */
101
+ export interface CommandSite {
102
+ /** The `CallExpr` node this site was found at. */
103
+ readonly node: ShNode;
104
+ /**
105
+ * `resolveWord` applied to the first word (`argv[0]`), with
106
+ * `{ context: 'command-argument' }` — every `CallExpr` word is an
107
+ * ordinary command-argument position, never an assignment value, so
108
+ * only a word-initial unquoted `~` triggers tilde expansion (an
109
+ * unquoted `~` after a `:`, e.g. `a:~/b`, is literal text here — see
110
+ * `ResolveWordOptions.context`'s doc comment).
111
+ */
112
+ readonly argv0: WordResolution;
113
+ /** `resolveWord` applied to every word, in argument order (same `{ context: 'command-argument' }` as {@link CommandSite.argv0}). */
114
+ readonly argv: readonly WordResolution[];
115
+ /** The path from the tree root to this site, outermost frame first. */
116
+ readonly context: readonly CommandContext[];
117
+ }
118
+ /**
119
+ * Enumerates every command invocation (`CallExpr`) reachable from `root`,
120
+ * with its resolved words ({@link resolveWord}) and the path used to reach
121
+ * it ({@link CommandContext}). Descends everywhere a command can occur:
122
+ * statement lists, both sides of `&&`/`||`/pipelines, subshells/blocks,
123
+ * if/case branches *and* conditions, loop bodies *and* conditions, function
124
+ * bodies, background/negated/coproc statements, and — critically —
125
+ * `CmdSubst`/`ProcSubst` nested inside words, wherever those words occur
126
+ * (arguments, redirection targets, case subjects/patterns, loop word
127
+ * lists, assignment values, test/arithmetic operands, …), not only inside
128
+ * `CallExpr.args`.
129
+ *
130
+ * An assignment-only `CallExpr` (`FOO=bar`, `args` empty) has no first word
131
+ * to resolve and is not itself a command invocation — no program runs — so
132
+ * it produces no {@link CommandSite}; any command substitution nested in
133
+ * its assigned value (`FOO=$(sub)`) is still found and reported.
134
+ *
135
+ * Results are sorted by source position (`node.range[0]`), so nested
136
+ * command substitutions and out-of-structural-order redirection targets
137
+ * still come back in source order regardless of traversal order.
138
+ *
139
+ * Facts only, matching {@link resolveWord}'s posture: no safety verdict, no
140
+ * command/wrapper allowlist or denylist, no dataflow. A statically unknown
141
+ * `argv0` (`static: false`) is a normal, expected result.
142
+ *
143
+ * A long *linear* chain — `|`/`|&`/`&&`/`||` of any realistic length — is
144
+ * traversed iteratively and never risks a stack overflow or trips the
145
+ * nesting-depth guard below, regardless of how many stages/links it has.
146
+ *
147
+ * @param root - Any `ShNode` — typically a `File` from `parseSync`, but any
148
+ * subtree (a `Stmt`, a `Command`, or even a bare `Word`) is handled.
149
+ * @throws {@link ShAnalyzeMaxDepthError} if `root`'s genuinely nested
150
+ * structure (subshells within subshells, chained command/process
151
+ * substitutions, deeply nested `if`/`case`/loop/function/`time`/`{ }`
152
+ * bodies, chained `elif`) exceeds this module's defensive recursion-depth
153
+ * guard — see that error's doc comment for why this fails closed instead
154
+ * of returning a partial result, and why a gate-style consumer should treat
155
+ * it as `deny`.
156
+ * @public
157
+ */
158
+ export declare function enumerateCommands(root: ShNode): CommandSite[];
159
+ //# sourceMappingURL=enumerate-commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enumerate-commands.d.ts","sourceRoot":"","sources":["../../src/analyze/enumerate-commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAChD;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC/C;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAC7B;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAC7B;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;CAAE,GAClE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GACzD;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEhC;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAW;IAC1B,kDAAkD;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,oIAAoI;IACpI,QAAQ,CAAC,IAAI,EAAE,SAAS,cAAc,EAAE,CAAC;IACzC,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;CAC7C;AA2aD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAU7D"}
@@ -0,0 +1,390 @@
1
+ import { ShAnalyzeMaxDepthError } from '../errors.js';
2
+ import { isShNodeShape, nodeArray, stringField } from './node-helpers.js';
3
+ import { resolveWord } from './resolve-word.js';
4
+ /**
5
+ * mvdan/sh v3.13.1's `syntax.BinCmdOperator` token values for `BinaryCmd.Op`
6
+ * — `AndStmt` (`&&`), `OrStmt` (`||`), `Pipe` (`|`), `PipeAll` (`|&`). These
7
+ * are not part of any documented wire contract (`normalize()` copies
8
+ * mvdan/sh's raw numeric token straight through — see design/ARCHITECTURE.md
9
+ * open question 4), so they're pinned here by empirical observation against
10
+ * this bridge's own shim rather than derived from an upstream constant, and
11
+ * locked in by a canary test (`enumerate-commands.test.ts`, "binary command
12
+ * operator identification") that would fail loudly if a future mvdan/sh
13
+ * version renumbered them.
14
+ */
15
+ const BIN_CMD_OP_AND = 11;
16
+ const BIN_CMD_OP_OR = 12;
17
+ const BIN_CMD_OP_PIPE = 13;
18
+ const BIN_CMD_OP_PIPE_ALL = 14;
19
+ function isPipeOp(op) {
20
+ return op === BIN_CMD_OP_PIPE || op === BIN_CMD_OP_PIPE_ALL;
21
+ }
22
+ /**
23
+ * The maximum number of *genuinely nested* structural frames (subshells,
24
+ * command/process substitutions, `if`/`case`/loop/function/`time`/`{ }`
25
+ * bodies, chained `elif`) {@link enumerateCommands} descends through before
26
+ * throwing {@link ShAnalyzeMaxDepthError} — a defensive backstop against
27
+ * pathological/adversarial input (a native stack overflow is an
28
+ * uncontrolled crash, not a catchable error). A long *linear* chain
29
+ * (`|`/`&&`/`||` of any length) never grows this counter — see
30
+ * `flattenPipelineStages`'s and `visitBinaryCmd`'s doc comments — only real
31
+ * nesting does.
32
+ *
33
+ * This value is deliberately *not* a round, "generous"-sounding number
34
+ * like 10,000: measured empirically (synthetic nested-`Subshell` trees,
35
+ * this repo's actual `vitest` worker environment, default Node/V8 stack
36
+ * size — see `analyze-enumerate-commands.test.ts`'s depth-guard describe
37
+ * block), this module's own recursive descent through a real
38
+ * `Subshell`-in-`Subshell` chain hits a raw, uncatchable
39
+ * `RangeError: Maximum call stack size exceeded` starting somewhere around
40
+ * depth 1,000–1,500 (the exact point varies run to run — it's a hard
41
+ * native-stack limit, not a clean threshold). A depth guard set at 10,000
42
+ * would never actually fire: the real crash happens first. 500 leaves
43
+ * roughly 2–3x headroom below the *lowest* observed native-crash depth,
44
+ * while still comfortably exceeding any realistic hand-written script's
45
+ * nesting (this is pathological-input protection, not a normal-usage
46
+ * ceiling).
47
+ */
48
+ const MAX_STRUCTURAL_DEPTH = 500;
49
+ function pushContext(context, frame) {
50
+ return [...context, frame];
51
+ }
52
+ /**
53
+ * Scans `value` — a word, an assignment, a test/arithmetic expression, a
54
+ * redirect, or an array of any of these — for `CmdSubst`/`ProcSubst`
55
+ * boundaries reachable from it, however deeply nested (through
56
+ * `DblQuoted`, `ParamExp`'s `Exp`/`Repl`/`Slice`/`nestedparam`, array
57
+ * elements, arithmetic operands, …), and hands each one found off to
58
+ * {@link visitStmtList} with the corresponding context frame pushed. This is
59
+ * a plain structural walk (discover children the same way {@link walk}
60
+ * does) rather than a hand-modeled traversal of every one of mvdan/sh's
61
+ * expression-shaped fields, because that set is large and not the part of
62
+ * the grammar this module's context-aware descent is about — the
63
+ * *command*-bearing structure (`BinaryCmd`, `IfClause`, loops, …) is
64
+ * hand-modeled in {@link visitCommand}; this helper's only job is finding
65
+ * where a word-shaped subtree stops being "just an expression" and starts
66
+ * containing statements again.
67
+ *
68
+ * `depth` is {@link visitStmt}'s structural-nesting counter, passed through
69
+ * unchanged for the generic per-field walk (a purely expression-shaped
70
+ * subtree — e.g. deeply nested `ParamExp` — is a different, out-of-scope
71
+ * risk from the command-structural nesting {@link MAX_STRUCTURAL_DEPTH}
72
+ * guards against) and incremented by one only when a `CmdSubst`/`ProcSubst`
73
+ * boundary is crossed, since that's a genuine additional level of
74
+ * command-bearing nesting (see {@link ShAnalyzeMaxDepthError}'s doc
75
+ * comment).
76
+ */
77
+ function scanForHiddenCommands(value, context, sites, depth) {
78
+ if (Array.isArray(value)) {
79
+ for (const item of value)
80
+ scanForHiddenCommands(item, context, sites, depth);
81
+ return;
82
+ }
83
+ if (!isShNodeShape(value))
84
+ return;
85
+ if (value.type === 'CmdSubst') {
86
+ visitStmtList(nodeArray(value.stmts), pushContext(context, { kind: 'cmdSubst' }), sites, depth + 1);
87
+ return;
88
+ }
89
+ if (value.type === 'ProcSubst') {
90
+ visitStmtList(nodeArray(value.stmts), pushContext(context, { kind: 'procSubst' }), sites, depth + 1);
91
+ return;
92
+ }
93
+ for (const field of Object.values(value)) {
94
+ scanForHiddenCommands(field, context, sites, depth);
95
+ }
96
+ }
97
+ function visitStmtList(stmts, context, sites, depth) {
98
+ for (const stmt of stmts)
99
+ visitStmt(stmt, context, sites, depth);
100
+ }
101
+ /**
102
+ * Visits one `Stmt`. Every genuinely-nested recursive path through this
103
+ * module funnels through here (directly, or via {@link visitStmtList}) —
104
+ * see {@link MAX_STRUCTURAL_DEPTH}'s doc comment — so this is where the
105
+ * depth guard is enforced: a `depth` that has grown past
106
+ * {@link MAX_STRUCTURAL_DEPTH} throws {@link ShAnalyzeMaxDepthError} rather
107
+ * than recursing further.
108
+ */
109
+ function visitStmt(stmt, context, sites, depth) {
110
+ if (depth > MAX_STRUCTURAL_DEPTH) {
111
+ throw new ShAnalyzeMaxDepthError(MAX_STRUCTURAL_DEPTH);
112
+ }
113
+ let stmtContext = context;
114
+ if (stmt.negated === true)
115
+ stmtContext = pushContext(stmtContext, { kind: 'negated' });
116
+ if (stmt.background === true)
117
+ stmtContext = pushContext(stmtContext, { kind: 'background' });
118
+ scanForHiddenCommands(stmt.redirs, stmtContext, sites, depth);
119
+ const cmd = stmt.cmd;
120
+ if (isShNodeShape(cmd))
121
+ visitCommand(cmd, stmtContext, sites, depth);
122
+ }
123
+ /**
124
+ * Recovers the left-to-right stages of a `|`/`|&` pipeline from `cmd` (a
125
+ * `BinaryCmd` whose own `Op` is already known to be a pipe-family
126
+ * operator). mvdan/sh nests these left-associatively — `a | b | c` is
127
+ * `BinaryCmd(x: Stmt{BinaryCmd(x: a, y: b)}, y: Stmt{c})` — so a chain of
128
+ * `n` stages is `n - 1` levels of nested `BinaryCmd`. Recovering stages
129
+ * walks that left ("x") spine with an explicit work stack instead of
130
+ * recursing per level, so a pipeline of any realistic length (this module
131
+ * is tested to 5,000+ stages) uses O(1) native call-stack frames here,
132
+ * regardless of `n` — the array-backed stack lives on the heap, not the
133
+ * call stack. The work-stack ordering (push `y` then `x`, i.e. `x` on top)
134
+ * mirrors what the original recursive left-to-right depth-first walk did
135
+ * (fully expand `x` before `y`), including the — per this bridge's own
136
+ * left-associative-only model — hypothetical case of a further pipe-family
137
+ * `BinaryCmd` on the `y` side. Mixed `|`/`|&` chains flatten into a single
138
+ * pipeline (this bridge doesn't distinguish "stderr also piped" in
139
+ * {@link CommandContext} — only stage position).
140
+ */
141
+ function flattenPipelineStages(cmd) {
142
+ const stages = [];
143
+ const workStack = [cmd.y, cmd.x];
144
+ while (workStack.length > 0) {
145
+ const operandStmt = workStack.pop();
146
+ if (!isShNodeShape(operandStmt))
147
+ continue;
148
+ const innerCmd = operandStmt.cmd;
149
+ if (isShNodeShape(innerCmd) && innerCmd.type === 'BinaryCmd' && isPipeOp(innerCmd.op)) {
150
+ // Push `y` then `x` so `x` — the side that keeps the chain going in
151
+ // the left-associative case — is popped (processed) first.
152
+ workStack.push(innerCmd.y, innerCmd.x);
153
+ }
154
+ else {
155
+ stages.push(operandStmt);
156
+ }
157
+ }
158
+ return stages;
159
+ }
160
+ /**
161
+ * Visits a non-pipe-family `BinaryCmd` (`&&`/`||`, or a future operator
162
+ * this module doesn't specifically recognize). Like pipelines, `&&`/`||`
163
+ * chains nest left-associatively — `a && b && c` is
164
+ * `BinaryCmd(x: Stmt{BinaryCmd(x: a, y: b)}, y: Stmt{c})`, and a mixed
165
+ * `a && b || c` chain is still a single left-nested spine
166
+ * (`(a && b) || c`) — so a long chain is walked iteratively here too,
167
+ * exactly mirroring {@link flattenPipelineStages}'s technique: descend the
168
+ * left ("x") spine with a `while` loop instead of recursion, collecting
169
+ * each level's right ("y") operand (and which context frame, if any, it
170
+ * gets — `'and'`/`'or'`, or none for an unrecognized operator, matching
171
+ * the original per-level logic) along the way, stopping either at a plain
172
+ * (non-`BinaryCmd`) leftmost operand or at a pipe-family `BinaryCmd` (a
173
+ * different sub-structure, visited via the ordinary `visitStmt` →
174
+ * `visitCommand` → `visitBinaryCmd` dispatch below — a single, bounded
175
+ * recursive step, not per-chain-length).
176
+ *
177
+ * Every operand this function ultimately visits (the collected right-hand
178
+ * operands and the leftmost operand) is one genuine structural hop away
179
+ * from this `BinaryCmd` node, so each gets `depth + 1` — the *same*
180
+ * incremented value for every one of them, regardless of chain length:
181
+ * they're siblings under this node's flattened spine, not nested inside
182
+ * one another, so the depth counter must not grow with stage count (only
183
+ * {@link flattenPipelineStages}'s sibling pipeline stages get the same
184
+ * treatment, for the same reason).
185
+ */
186
+ function visitBinaryCmd(cmd, context, sites, depth) {
187
+ const op = cmd.op;
188
+ if (isPipeOp(op)) {
189
+ const stages = flattenPipelineStages(cmd);
190
+ stages.forEach((stage, index) => {
191
+ visitStmt(stage, pushContext(context, { kind: 'pipeline', stage: index }), sites, depth + 1);
192
+ });
193
+ return;
194
+ }
195
+ const rightOperands = [];
196
+ let spine = cmd;
197
+ for (;;) {
198
+ const spineOp = spine.op;
199
+ const frame = spineOp === BIN_CMD_OP_AND
200
+ ? { kind: 'and', side: 'right' }
201
+ : spineOp === BIN_CMD_OP_OR
202
+ ? { kind: 'or', side: 'right' }
203
+ : undefined;
204
+ const y = spine.y;
205
+ if (isShNodeShape(y))
206
+ rightOperands.push({ stmt: y, frame });
207
+ const x = spine.x;
208
+ if (!isShNodeShape(x))
209
+ break;
210
+ const innerCmd = x.cmd;
211
+ if (isShNodeShape(innerCmd) && innerCmd.type === 'BinaryCmd' && !isPipeOp(innerCmd.op)) {
212
+ spine = innerCmd;
213
+ continue;
214
+ }
215
+ // `x` is the leftmost operand: not a further same-family BinaryCmd
216
+ // link, so this is where the spine walk ends — visit it with the
217
+ // *outer* (unmodified) context, exactly as the original recursive
218
+ // implementation visited `cmd.x`.
219
+ visitStmt(x, context, sites, depth + 1);
220
+ break;
221
+ }
222
+ for (const { stmt, frame } of rightOperands) {
223
+ visitStmt(stmt, frame ? pushContext(context, frame) : context, sites, depth + 1);
224
+ }
225
+ }
226
+ function visitIfClause(clause, context, sites, depth) {
227
+ visitStmtList(nodeArray(clause.cond), pushContext(context, { kind: 'if', branch: 'cond' }), sites, depth);
228
+ visitStmtList(nodeArray(clause.then), pushContext(context, { kind: 'if', branch: 'then' }), sites, depth);
229
+ const elseClause = clause.else;
230
+ if (isShNodeShape(elseClause)) {
231
+ // A chained `elif` — mvdan/sh models it as `IfClause.Else` pointing to
232
+ // another `IfClause` — is one more genuine level of nesting than
233
+ // `cond`/`then` above, so it gets its own `depth + 1` (a long `elif`
234
+ // chain must still trip {@link MAX_STRUCTURAL_DEPTH}, since — unlike
235
+ // `&&`/`||`/pipelines — this module does not special-case flattening
236
+ // it).
237
+ visitIfClause(elseClause, pushContext(context, { kind: 'if', branch: 'else' }), sites, depth + 1);
238
+ }
239
+ }
240
+ function visitCommand(cmd, context, sites, depth) {
241
+ switch (cmd.type) {
242
+ case 'CallExpr': {
243
+ scanForHiddenCommands(cmd.assigns, context, sites, depth);
244
+ scanForHiddenCommands(cmd.args, context, sites, depth);
245
+ const args = nodeArray(cmd.args);
246
+ if (args.length > 0) {
247
+ const argv = args.map((word) => resolveWord(word, { context: 'command-argument' }));
248
+ sites.push({ node: cmd, argv0: argv[0], argv, context });
249
+ }
250
+ return;
251
+ }
252
+ case 'BinaryCmd':
253
+ visitBinaryCmd(cmd, context, sites, depth);
254
+ return;
255
+ case 'Block':
256
+ // Transparent grouping (`{ ...; }`) — no context frame, but still one
257
+ // genuine level of structural nesting for depth-guard purposes (a
258
+ // pathological `{ { { ... } } }` chain must still trip the guard).
259
+ visitStmtList(nodeArray(cmd.stmts), context, sites, depth + 1);
260
+ return;
261
+ case 'Subshell':
262
+ visitStmtList(nodeArray(cmd.stmts), pushContext(context, { kind: 'subshell' }), sites, depth + 1);
263
+ return;
264
+ case 'IfClause':
265
+ visitIfClause(cmd, context, sites, depth + 1);
266
+ return;
267
+ case 'CaseClause': {
268
+ scanForHiddenCommands(cmd.word, context, sites, depth);
269
+ for (const item of nodeArray(cmd.items)) {
270
+ scanForHiddenCommands(item.patterns, context, sites, depth);
271
+ visitStmtList(nodeArray(item.stmts), pushContext(context, { kind: 'case' }), sites, depth + 1);
272
+ }
273
+ return;
274
+ }
275
+ case 'ForClause': {
276
+ scanForHiddenCommands(cmd.loop, context, sites, depth);
277
+ visitStmtList(nodeArray(cmd.do), pushContext(context, { kind: 'loop', role: 'body' }), sites, depth + 1);
278
+ return;
279
+ }
280
+ case 'WhileClause': {
281
+ visitStmtList(nodeArray(cmd.cond), pushContext(context, { kind: 'loop', role: 'cond' }), sites, depth + 1);
282
+ visitStmtList(nodeArray(cmd.do), pushContext(context, { kind: 'loop', role: 'body' }), sites, depth + 1);
283
+ return;
284
+ }
285
+ case 'FuncDecl': {
286
+ const nameNode = cmd.name;
287
+ const nameText = isShNodeShape(nameNode) ? stringField(nameNode, 'value') : '';
288
+ const body = cmd.body;
289
+ if (isShNodeShape(body)) {
290
+ visitStmt(body, pushContext(context, { kind: 'function', name: nameText }), sites, depth + 1);
291
+ }
292
+ return;
293
+ }
294
+ case 'CoprocClause': {
295
+ scanForHiddenCommands(cmd.name, context, sites, depth);
296
+ const stmt = cmd.stmt;
297
+ if (isShNodeShape(stmt)) {
298
+ visitStmt(stmt, pushContext(context, { kind: 'coproc' }), sites, depth + 1);
299
+ }
300
+ return;
301
+ }
302
+ case 'DeclClause':
303
+ scanForHiddenCommands(cmd.args, context, sites, depth);
304
+ return;
305
+ case 'LetClause':
306
+ scanForHiddenCommands(cmd.exprs, context, sites, depth);
307
+ return;
308
+ case 'TestClause':
309
+ scanForHiddenCommands(cmd.x, context, sites, depth);
310
+ return;
311
+ case 'ArithmCmd':
312
+ scanForHiddenCommands(cmd.x, context, sites, depth);
313
+ return;
314
+ case 'TestDecl': {
315
+ scanForHiddenCommands(cmd.description, context, sites, depth);
316
+ const body = cmd.body;
317
+ if (isShNodeShape(body))
318
+ visitStmt(body, context, sites, depth + 1);
319
+ return;
320
+ }
321
+ case 'TimeClause': {
322
+ // Transparent (`time cmd`) — no context frame, but still one genuine
323
+ // level of structural nesting for depth-guard purposes.
324
+ const stmt = cmd.stmt;
325
+ if (isShNodeShape(stmt))
326
+ visitStmt(stmt, context, sites, depth + 1);
327
+ return;
328
+ }
329
+ default:
330
+ // Not one of mvdan/sh's `syntax.Command` variants — e.g. a `Word` or
331
+ // `Assign` passed directly as `enumerateCommands`' root, or a future
332
+ // Command type this module doesn't know yet. Fall back to scanning it
333
+ // as an expression subtree rather than silently finding nothing.
334
+ scanForHiddenCommands(cmd, context, sites, depth);
335
+ }
336
+ }
337
+ /**
338
+ * Enumerates every command invocation (`CallExpr`) reachable from `root`,
339
+ * with its resolved words ({@link resolveWord}) and the path used to reach
340
+ * it ({@link CommandContext}). Descends everywhere a command can occur:
341
+ * statement lists, both sides of `&&`/`||`/pipelines, subshells/blocks,
342
+ * if/case branches *and* conditions, loop bodies *and* conditions, function
343
+ * bodies, background/negated/coproc statements, and — critically —
344
+ * `CmdSubst`/`ProcSubst` nested inside words, wherever those words occur
345
+ * (arguments, redirection targets, case subjects/patterns, loop word
346
+ * lists, assignment values, test/arithmetic operands, …), not only inside
347
+ * `CallExpr.args`.
348
+ *
349
+ * An assignment-only `CallExpr` (`FOO=bar`, `args` empty) has no first word
350
+ * to resolve and is not itself a command invocation — no program runs — so
351
+ * it produces no {@link CommandSite}; any command substitution nested in
352
+ * its assigned value (`FOO=$(sub)`) is still found and reported.
353
+ *
354
+ * Results are sorted by source position (`node.range[0]`), so nested
355
+ * command substitutions and out-of-structural-order redirection targets
356
+ * still come back in source order regardless of traversal order.
357
+ *
358
+ * Facts only, matching {@link resolveWord}'s posture: no safety verdict, no
359
+ * command/wrapper allowlist or denylist, no dataflow. A statically unknown
360
+ * `argv0` (`static: false`) is a normal, expected result.
361
+ *
362
+ * A long *linear* chain — `|`/`|&`/`&&`/`||` of any realistic length — is
363
+ * traversed iteratively and never risks a stack overflow or trips the
364
+ * nesting-depth guard below, regardless of how many stages/links it has.
365
+ *
366
+ * @param root - Any `ShNode` — typically a `File` from `parseSync`, but any
367
+ * subtree (a `Stmt`, a `Command`, or even a bare `Word`) is handled.
368
+ * @throws {@link ShAnalyzeMaxDepthError} if `root`'s genuinely nested
369
+ * structure (subshells within subshells, chained command/process
370
+ * substitutions, deeply nested `if`/`case`/loop/function/`time`/`{ }`
371
+ * bodies, chained `elif`) exceeds this module's defensive recursion-depth
372
+ * guard — see that error's doc comment for why this fails closed instead
373
+ * of returning a partial result, and why a gate-style consumer should treat
374
+ * it as `deny`.
375
+ * @public
376
+ */
377
+ export function enumerateCommands(root) {
378
+ const sites = [];
379
+ if (root.type === 'File') {
380
+ visitStmtList(nodeArray(root.stmts), [], sites, 0);
381
+ }
382
+ else if (root.type === 'Stmt') {
383
+ visitStmt(root, [], sites, 0);
384
+ }
385
+ else {
386
+ visitCommand(root, [], sites, 0);
387
+ }
388
+ return [...sites].sort((a, b) => a.node.range[0] - b.node.range[0]);
389
+ }
390
+ //# sourceMappingURL=enumerate-commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enumerate-commands.js","sourceRoot":"","sources":["../../src/analyze/enumerate-commands.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `sh-ast/analyze` — the static-analysis layer built on top of the
3
+ * normalized AST from the package root. Its first primitive,
4
+ * {@link resolveWord}, answers "is this word statically a known string,
5
+ * and if so which?" — the question every downstream lint rule or command
6
+ * inventory asks first. Its second, {@link enumerateCommands}, answers
7
+ * "where are all the commands, and how is each one reached?" — finding
8
+ * every `CallExpr` anywhere in the tree, including ones hidden inside
9
+ * command/process substitutions nested in words. Facts only: no policy, no
10
+ * hardcoded command/wrapper lists (see {@link WordResolution}'s and
11
+ * {@link CommandSite}'s doc comments).
12
+ */
13
+ export { resolveWord } from './resolve-word.js';
14
+ export type { ResolveWordOptions, WordResolution, WordResolutionReason } from './resolve-word.js';
15
+ export { enumerateCommands } from './enumerate-commands.js';
16
+ export type { CommandContext, CommandSite } from './enumerate-commands.js';
17
+ export { ShAnalyzeMaxDepthError } from '../errors.js';
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/analyze/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAElG,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAK3E,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `sh-ast/analyze` — the static-analysis layer built on top of the
3
+ * normalized AST from the package root. Its first primitive,
4
+ * {@link resolveWord}, answers "is this word statically a known string,
5
+ * and if so which?" — the question every downstream lint rule or command
6
+ * inventory asks first. Its second, {@link enumerateCommands}, answers
7
+ * "where are all the commands, and how is each one reached?" — finding
8
+ * every `CallExpr` anywhere in the tree, including ones hidden inside
9
+ * command/process substitutions nested in words. Facts only: no policy, no
10
+ * hardcoded command/wrapper lists (see {@link WordResolution}'s and
11
+ * {@link CommandSite}'s doc comments).
12
+ */
13
+ export { resolveWord } from './resolve-word.js';
14
+ export { enumerateCommands } from './enumerate-commands.js';
15
+ // Re-exported from the root errors module so a consumer of enumerateCommands
16
+ // can `import { ShAnalyzeMaxDepthError } from 'sh-ast/analyze'` without also
17
+ // reaching into the root `sh-ast` entry point.
18
+ export { ShAnalyzeMaxDepthError } from '../errors.js';
19
+ // `ShNode` (resolveWord's parameter type) is deliberately *not* re-exported
20
+ // here: it's the root `sh-ast` entry point's type (see `sh-ast`'s own
21
+ // `index.ts`), and every caller of `resolveWord` already has a `ShNode` in
22
+ // hand from `parseSync`/`walk` there. This is a known, intentional
23
+ // "ae-forgotten-export" in this subpath's api-report — `sh-ast/analyze`
24
+ // consumes the root layer's type without re-exporting it, rather than
25
+ // re-exporting it (and its own transitive doc-linked types) just to
26
+ // silence API Extractor.
27
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/analyze/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAG5D,6EAA6E;AAC7E,6EAA6E;AAC7E,+CAA+C;AAC/C,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAEtD,4EAA4E;AAC5E,sEAAsE;AACtE,2EAA2E;AAC3E,mEAAmE;AACnE,wEAAwE;AACxE,sEAAsE;AACtE,oEAAoE;AACpE,yBAAyB"}