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,390 @@
1
+ /**
2
+ * A single frame of the path from the root of the tree down to a
3
+ * {@link CommandSite}, describing *how* the command is reached — never
4
+ * whether it is safe to run. `CommandSite.context` is an ordered stack of
5
+ * these, outermost frame first:
6
+ *
7
+ * - `'and'`/`'or'` (`side: 'right'`) — the right-hand operand of a
8
+ * `BinaryCmd` (mvdan/sh's node for both `&&` and `||`); only the right
9
+ * side is tagged; the left side inherits the surrounding context
10
+ * unchanged, since it runs unconditionally relative to this operator.
11
+ * - `'pipeline'` (`stage: n`) — one stage of a `|`/`|&` chain (also a
12
+ * `BinaryCmd`, left-associatively nested by mvdan/sh); every stage is
13
+ * tagged, 0-indexed left to right, regardless of whether the chain mixes
14
+ * `|` and `|&`.
15
+ * - `'subshell'` — inside a `Subshell` (`( ... )`).
16
+ * - `'cmdSubst'`/`'procSubst'` — inside a `CmdSubst` (`$(...)`/backticks) or
17
+ * `ProcSubst` (`<(...)`/`>(...)`) reached from *any* word-bearing
18
+ * position (an argument, a redirection target, a case subject, a loop's
19
+ * word list, an assignment value, a test/arithmetic operand, …) — not
20
+ * only from `CallExpr.args`.
21
+ * - `'if'` (`branch: 'cond' | 'then' | 'else'`) — inside an `IfClause`'s
22
+ * condition, then-branch, or else-branch (mvdan/sh nests `elif` chains as
23
+ * `IfClause.else` pointing to another `IfClause`, so an `elif`'s own
24
+ * condition/then are reached through an `{kind:'if',branch:'else'}` frame
25
+ * first, then their own `'cond'`/`'then'` frame — reflecting the real
26
+ * nesting rather than collapsing it).
27
+ * - `'case'` — inside one `CaseClause` branch's statement list (not the
28
+ * case subject word or the patterns).
29
+ * - `'loop'` (`role` is `'body'` or `'cond'`) — inside a `ForClause`'s
30
+ * statement list (`role: 'body'` only — a `for` loop has no
31
+ * statement-list condition) or a `WhileClause`'s (`role: 'cond'` for the
32
+ * condition, `role: 'body'` for the loop body).
33
+ * - `'function'` (`name`) — inside a `FuncDecl`'s body; `name` is the
34
+ * function's literal name text.
35
+ * - `'background'`/`'negated'` — the enclosing `Stmt` has mvdan/sh's
36
+ * Background/Negated flag set (`cmd &`, `! cmd`).
37
+ * - `'coproc'` — inside a `CoprocClause`'s statement (a `coproc` block,
38
+ * optionally named).
39
+ *
40
+ * A `Block` grouping (`{ ...; }`) and a `TimeClause` (`time cmd`) are
41
+ * deliberately transparent — grouping and timing a command doesn't change
42
+ * how it's reached, so no frame is added for either.
43
+ *
44
+ * This union may grow in a **minor** release — a future mvdan/sh grammar
45
+ * construct this module starts modeling can add a new `kind` variant
46
+ * without that being a breaking change (mirroring
47
+ * {@link WordResolutionReason}'s semver policy in `resolve-word.ts`). It is
48
+ * deliberately not sealed against extension elsewhere in the codebase.
49
+ * Every existing variant's shape (its extra fields, if any) is stable —
50
+ * only new variants are ever added — so an exhaustive compile-time
51
+ * `switch` over `.kind` should still include a `default` case to stay
52
+ * forward-compatible.
53
+ *
54
+ * @public
55
+ */
56
+ export declare type CommandContext = {
57
+ readonly kind: 'and';
58
+ readonly side: 'right';
59
+ } | {
60
+ readonly kind: 'or';
61
+ readonly side: 'right';
62
+ } | {
63
+ readonly kind: 'pipeline';
64
+ readonly stage: number;
65
+ } | {
66
+ readonly kind: 'subshell';
67
+ } | {
68
+ readonly kind: 'cmdSubst';
69
+ } | {
70
+ readonly kind: 'procSubst';
71
+ } | {
72
+ readonly kind: 'if';
73
+ readonly branch: 'then' | 'else' | 'cond';
74
+ } | {
75
+ readonly kind: 'case';
76
+ } | {
77
+ readonly kind: 'loop';
78
+ readonly role: 'body' | 'cond';
79
+ } | {
80
+ readonly kind: 'function';
81
+ readonly name: string;
82
+ } | {
83
+ readonly kind: 'background';
84
+ } | {
85
+ readonly kind: 'negated';
86
+ } | {
87
+ readonly kind: 'coproc';
88
+ };
89
+
90
+ /**
91
+ * One place in the tree where a command is actually invoked — a `CallExpr`
92
+ * node, together with its resolved words and the path used to reach it.
93
+ * Facts only, matching {@link resolveWord}'s posture: no safety verdict, no
94
+ * hardcoded command/wrapper list. A dynamic (`static: false`) `argv0` is a
95
+ * normal, expected result — not an error and not itself reported as
96
+ * "unknown"/"unsafe".
97
+ *
98
+ * @public
99
+ */
100
+ export declare interface CommandSite {
101
+ /** The `CallExpr` node this site was found at. */
102
+ readonly node: ShNode;
103
+ /**
104
+ * `resolveWord` applied to the first word (`argv[0]`), with
105
+ * `{ context: 'command-argument' }` — every `CallExpr` word is an
106
+ * ordinary command-argument position, never an assignment value, so
107
+ * only a word-initial unquoted `~` triggers tilde expansion (an
108
+ * unquoted `~` after a `:`, e.g. `a:~/b`, is literal text here — see
109
+ * `ResolveWordOptions.context`'s doc comment).
110
+ */
111
+ readonly argv0: WordResolution;
112
+ /** `resolveWord` applied to every word, in argument order (same `{ context: 'command-argument' }` as {@link CommandSite.argv0}). */
113
+ readonly argv: readonly WordResolution[];
114
+ /** The path from the tree root to this site, outermost frame first. */
115
+ readonly context: readonly CommandContext[];
116
+ }
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
+
160
+ /**
161
+ * A 1-based line/column position. Columns are UTF-16 code units, matching
162
+ * JavaScript string indexing (see {@link ShNode.range}).
163
+ *
164
+ * @public
165
+ */
166
+ declare interface Position {
167
+ readonly line: number;
168
+ readonly column: number;
169
+ }
170
+
171
+ /**
172
+ * Determines whether a `Word` node is statically a known string, and if
173
+ * so, what that string is — the atom of shell-AST static analysis (e.g.
174
+ * "is this word literally `rm`"). A word is static iff every part is a
175
+ * literal after shell unquoting: a bare `Lit` (backslash escapes
176
+ * processed), a `SglQuoted` (including `$'...'` ANSI-C escapes decoded),
177
+ * or a `DblQuoted` containing only literal parts (and not itself a
178
+ * `$"..."` locale translation) — including concatenations of these (`'r'm`
179
+ * resolves to `"rm"`). A word containing any expansion (`ParamExp`,
180
+ * `CmdSubst`, `ArithmExp`, `ProcSubst`), a triggering unquoted `~` (see
181
+ * `options.context`), an unquoted glob metacharacter/bracket
182
+ * expression/`ExtGlob`, a `$"..."` locale translation, or an unrepresentable
183
+ * `$'\U...'` escape is `static: false` with a reason — never an error, and
184
+ * never a safety judgment (see {@link WordResolution}).
185
+ *
186
+ * @param word - A `Word` node, e.g. `parseSync(...).stmts[0].cmd.args[0]`.
187
+ * @param options - See {@link ResolveWordOptions}.
188
+ * @throws TypeError if `word.type` is not `"Word"` — a programmer-error
189
+ * misuse of the API, not a "malformed shell source" case (well-formed
190
+ * shell source, however dynamic, never throws; see {@link WordResolution}).
191
+ *
192
+ * @public
193
+ */
194
+ export declare function resolveWord(word: ShNode, options?: ResolveWordOptions): WordResolution;
195
+
196
+ /**
197
+ * Options controlling {@link resolveWord}'s tilde-expansion detection.
198
+ *
199
+ * @public
200
+ */
201
+ export declare interface ResolveWordOptions {
202
+ /**
203
+ * Which Bash grammar position `word` occupies — the two positions Bash
204
+ * §3.5.2 ("Tilde Expansion") documents distinct tilde-expansion triggers
205
+ * for:
206
+ *
207
+ * - `'command-argument'` — an ordinary simple-command argument word (e.g.
208
+ * `echo <word>`). Only a *word-initial* unquoted `~` triggers tilde
209
+ * expansion; an unquoted `~` elsewhere in the word (including right
210
+ * after a `:`) is an ordinary character (`echo a:~b` prints `a:~b`
211
+ * literally in real bash).
212
+ * - `'assignment-value'` — the value side of a shell assignment (e.g.
213
+ * `PATH=<word>`, an `Assign` node's `value`). Bash additionally
214
+ * tilde-expands an unquoted `~` immediately following an unquoted `:`
215
+ * anywhere in the value (`PATH=/foo:~/bar` expands the `~/bar`
216
+ * segment) — this is documented for `PATH`-like colon-separated
217
+ * assignment values specifically.
218
+ *
219
+ * **Default: `'assignment-value'` semantics apply whenever this option is
220
+ * omitted.** `resolveWord` takes a bare `Word` node with no grammar
221
+ * context attached, so it cannot see whether its caller extracted that
222
+ * word from a command argument or an assignment value — omitting this
223
+ * option is deliberately treated as the *more conservative* of the two
224
+ * (the one that reports `'tilde'` in more cases), so a caller that
225
+ * doesn't know or care about the distinction never under-reports a real
226
+ * tilde-expansion site. Pass `'command-argument'` explicitly to opt into
227
+ * the narrower, word-initial-only check that matches real bash's
228
+ * behavior for ordinary arguments.
229
+ *
230
+ * @see https://www.gnu.org/software/bash/manual/bash.html#Tilde-Expansion
231
+ */
232
+ readonly context?: 'command-argument' | 'assignment-value';
233
+ }
234
+
235
+ /**
236
+ * Thrown by `sh-ast/analyze`'s `enumerateCommands` when a tree's genuinely
237
+ * nested structure — subshells within subshells, chained command/process
238
+ * substitutions, deeply nested `if`/`case`/loop/function/`time`/`{ }`
239
+ * bodies, deeply chained `elif` — exceeds its defensive recursion-depth
240
+ * guard. **Not** thrown for a long *linear* chain (`|`/`|&`/`&&`/`||` of
241
+ * any realistic length): `enumerateCommands` traverses those iteratively,
242
+ * so chain length alone never grows this guard's depth counter — only
243
+ * genuine tree nesting does.
244
+ *
245
+ * `enumerateCommands` deliberately fails closed here rather than returning
246
+ * a truncated, partial result: a partial `CommandSite[]` silently omits
247
+ * real command sites, which is a false negative for a permission-hook-style
248
+ * consumer that treats "command not found in the enumeration" as "nothing
249
+ * to worry about" — an explicit, documented throw is safer than a silent
250
+ * under-report. Gate-style consumers should treat this error as `deny`,
251
+ * not fall back to "no commands found, so allow".
252
+ *
253
+ * @public
254
+ */
255
+ export declare class ShAnalyzeMaxDepthError extends ShBridgeError {
256
+ readonly code = "ESLINT_SH_ANALYZE_MAX_DEPTH";
257
+ /** The maximum nesting depth `enumerateCommands` supports; the same value every time (not caller-configurable). */
258
+ readonly maxDepth: number;
259
+ constructor(maxDepth: number);
260
+ }
261
+
262
+ /**
263
+ * Common base class for every error this package throws — originally just
264
+ * {@link parseSync}'s errors, now also the `sh-ast/analyze` layer's (see
265
+ * {@link ShAnalyzeMaxDepthError}). Provides a stable, documented `code`
266
+ * discriminator (e.g. `"ESLINT_SH_PARSE_ERROR"`) alongside the usual
267
+ * `instanceof` narrowing, so consumers can branch on failure kind
268
+ * programmatically without parsing `.message` strings. Never thrown
269
+ * directly — only via its concrete subclasses ({@link ShParseError},
270
+ * {@link ShInvalidDialectError}, {@link ShBridgeInternalError},
271
+ * {@link ShAnalyzeMaxDepthError}).
272
+ *
273
+ * @public
274
+ */
275
+ declare abstract class ShBridgeError extends Error {
276
+ /**
277
+ * Stable, machine-readable discriminator for this error kind. Distinct
278
+ * per subclass and will not change once published.
279
+ */
280
+ abstract readonly code: string;
281
+ }
282
+
283
+ /**
284
+ * A normalized AST node produced by {@link parseSync}.
285
+ *
286
+ * `range` and `loc` are expressed in UTF-16 code units, so
287
+ * `code.slice(node.range[0], node.range[1])` reproduces the node's exact
288
+ * source text — even though mvdan/sh itself reports byte offsets and
289
+ * byte-counting columns internally (see design/ARCHITECTURE.md's
290
+ * "Serialization contract"). Every other field copied over from mvdan/sh's
291
+ * typedjson tree keeps its original value but with the field name
292
+ * lowercased (`Stmts` becomes `stmts`, `CondLast` becomes `condlast`, etc.).
293
+ *
294
+ * @public
295
+ */
296
+ declare interface ShNode {
297
+ readonly type: string;
298
+ readonly range: readonly [number, number];
299
+ readonly loc: {
300
+ readonly start: Position;
301
+ readonly end: Position;
302
+ };
303
+ [field: string]: unknown;
304
+ }
305
+
306
+ /**
307
+ * The result of statically resolving a `Word` node's text. Reports facts
308
+ * only — never a safety verdict, never a hardcoded command/wrapper list.
309
+ * `static: false` ("unknowable") is a first-class, valid result, not an
310
+ * error condition.
311
+ *
312
+ * @public
313
+ */
314
+ export declare type WordResolution = {
315
+ /** The word is a compile-time-known string. */
316
+ readonly static: true;
317
+ /** The word's exact resolved text (quotes and escapes removed). */
318
+ readonly text: string;
319
+ } | {
320
+ /** The word's value depends on program state, the environment, or the filesystem. */
321
+ readonly static: false;
322
+ /** Why this word cannot be statically resolved. */
323
+ readonly reason: WordResolutionReason;
324
+ };
325
+
326
+ /**
327
+ * Why a {@link resolveWord} result is `static: false`: the word's exact
328
+ * text cannot be determined from source alone.
329
+ *
330
+ * - `'expansion'` — the word contains a `ParamExp` (`$x`, `${x}`), a
331
+ * `CmdSubst` (command substitution, either `$(...)` or backtick form), an
332
+ * `ArithmExp` (`$((expr))`), or a `ProcSubst` (`<(cmd)`/`>(cmd)`) part:
333
+ * its value depends on program state (variables, command output, process
334
+ * substitution) that only exists at run time.
335
+ * - `'tilde'` — an unquoted, unescaped `~` triggers Bash tilde expansion
336
+ * (Bash Reference Manual §3.5.2, "Tilde Expansion") at the start of the
337
+ * word, or — when `resolveWord`'s `context` option is `'assignment-value'`
338
+ * or omitted — immediately after an unquoted `:` anywhere in the word too
339
+ * (§3.5.2 also documents this for assignment-statement values, e.g.
340
+ * `PATH=/foo:~/bar`). Either way: statically shaped, but its value
341
+ * depends on the filesystem/environment (`$HOME`, NSS user database
342
+ * lookups) at run time. See `resolveWord`'s `context` parameter doc for
343
+ * the full command-argument-vs-assignment-value distinction.
344
+ * - `'glob'` — the word contains an unquoted, unescaped glob metacharacter
345
+ * (`*`/`?`), an unquoted bracket expression (`[...]` — an unquoted `[`
346
+ * with a later unquoted `]` in the same word, POSIX 2.13.1 / Bash
347
+ * Reference Manual §3.5.8.1, "Pattern Matching"), or an `ExtGlob` part
348
+ * (Bash Reference Manual §3.5.8, "Filename Expansion"): its value depends
349
+ * on which filesystem entries exist at run time.
350
+ * - `'brace'` — the word contains a `BraceExp` part (`{a,b}`/`{1..10}`,
351
+ * Bash Reference Manual §3.5.1, "Brace Expansion"). mvdan/sh v3.13.1 only
352
+ * ever produces this node type when its caller applies `syntax.SplitBraces`
353
+ * to the parsed tree; this bridge's shim does not call it, so today a
354
+ * brace expression like `{a,b}` reaches {@link resolveWord} as an
355
+ * ordinary `Lit` (indistinguishable, in the AST this bridge produces,
356
+ * from literal `{`/`,`/`}` characters) rather than as a `BraceExp` part.
357
+ * This reason is handled here for forward-compatibility with that node
358
+ * type and is not reachable through {@link resolveWord} today.
359
+ * - `'locale'` — the word contains a `$"..."` locale-translated string
360
+ * (Bash Reference Manual §3.1.2.5, "Locale-Specific Translation"): its
361
+ * value is looked up in the current locale's translation catalog at run
362
+ * time via `gettext`, regardless of whether its contents also contain an
363
+ * expansion. `'locale'` takes precedence over any `'expansion'` a
364
+ * `$"..."` word's *contents* would otherwise report — the `DblQuoted`
365
+ * node's own `dollar` flag is checked before its children are visited at
366
+ * all, so e.g. `$"hi $x"` reports `'locale'`, never `'expansion'`.
367
+ * - `'unsupported'` — the word contains a `$'...'` (ANSI-C quoted) `\u`/`\U`
368
+ * escape whose value has no corresponding Unicode character (greater than
369
+ * `U+10FFFF`, e.g. `$'\UFFFFFFFF'`): real bash's output for this case is
370
+ * an undocumented, non-UTF-8 byte sequence with no exact representation
371
+ * as a JavaScript string (see `ansi-c-escapes.ts`'s
372
+ * `MAX_UNICODE_CODE_POINT` doc comment for the empirical detail) — this
373
+ * package declines to claim a `static: true` text bash never actually
374
+ * produces.
375
+ *
376
+ * This union may grow in a **minor** release — new mvdan/sh word-part node
377
+ * types, or new statically-shaped-but-dynamic categories, can add new reason
378
+ * strings without that being a breaking change. It is deliberately not
379
+ * sealed against extension elsewhere in the codebase. A reason string this
380
+ * version of the package doesn't yet know about only ever accompanies
381
+ * `static: false`, never changes the meaning of `static: true`, so runtime
382
+ * consumers that only branch on `.static` are safe by construction across
383
+ * such additions; an exhaustive compile-time `switch` over `.reason` should
384
+ * still include a `default` case to stay forward-compatible.
385
+ *
386
+ * @public
387
+ */
388
+ export declare type WordResolutionReason = 'expansion' | 'tilde' | 'glob' | 'brace' | 'locale' | 'unsupported';
389
+
390
+ export { }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Recursively freezes `value` and every object/array it (transitively)
3
+ * contains, so a frozen shared table's immutability holds at every level —
4
+ * not just its own top-level keys — rather than relying on every caller to
5
+ * treat it as read-only by convention.
6
+ *
7
+ * Generic, structural recursion over an unknown shape needs a cast to index
8
+ * into `value`'s own properties; contained to this one small, well-tested
9
+ * utility rather than spread across every table that uses it.
10
+ *
11
+ * @internal
12
+ */
13
+ export declare function deepFreeze<T>(value: T): Readonly<T>;
14
+ //# sourceMappingURL=deep-freeze.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deep-freeze.d.ts","sourceRoot":"","sources":["../src/deep-freeze.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAWnD"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Recursively freezes `value` and every object/array it (transitively)
3
+ * contains, so a frozen shared table's immutability holds at every level —
4
+ * not just its own top-level keys — rather than relying on every caller to
5
+ * treat it as read-only by convention.
6
+ *
7
+ * Generic, structural recursion over an unknown shape needs a cast to index
8
+ * into `value`'s own properties; contained to this one small, well-tested
9
+ * utility rather than spread across every table that uses it.
10
+ *
11
+ * @internal
12
+ */
13
+ export function deepFreeze(value) {
14
+ if (value !== null && (typeof value === 'object' || typeof value === 'function')) {
15
+ for (const key of Object.getOwnPropertyNames(value)) {
16
+ const prop = value[key];
17
+ if (prop !== null && typeof prop === 'object') {
18
+ deepFreeze(prop);
19
+ }
20
+ }
21
+ Object.freeze(value);
22
+ }
23
+ return value;
24
+ }
25
+ //# sourceMappingURL=deep-freeze.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deep-freeze.js","sourceRoot":"","sources":["../src/deep-freeze.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CAAI,KAAQ;IACpC,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,UAAU,CAAC,EAAE,CAAC;QACjF,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,mBAAmB,CAAC,KAAK,CAAC,EAAE,CAAC;YACpD,MAAM,IAAI,GAAI,KAAiC,CAAC,GAAG,CAAC,CAAC;YACrD,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC9C,UAAU,CAAC,IAAI,CAAC,CAAC;YACnB,CAAC;QACH,CAAC;QACD,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,115 @@
1
+ import type { ShellDialect } from './types.js';
2
+ /**
3
+ * Common base class for every error this package throws — originally just
4
+ * {@link parseSync}'s errors, now also the `sh-ast/analyze` layer's (see
5
+ * {@link ShAnalyzeMaxDepthError}). Provides a stable, documented `code`
6
+ * discriminator (e.g. `"ESLINT_SH_PARSE_ERROR"`) alongside the usual
7
+ * `instanceof` narrowing, so consumers can branch on failure kind
8
+ * programmatically without parsing `.message` strings. Never thrown
9
+ * directly — only via its concrete subclasses ({@link ShParseError},
10
+ * {@link ShInvalidDialectError}, {@link ShBridgeInternalError},
11
+ * {@link ShAnalyzeMaxDepthError}).
12
+ *
13
+ * @public
14
+ */
15
+ export declare abstract class ShBridgeError extends Error {
16
+ /**
17
+ * Stable, machine-readable discriminator for this error kind. Distinct
18
+ * per subclass and will not change once published.
19
+ */
20
+ abstract readonly code: string;
21
+ }
22
+ /**
23
+ * Position info attached to a {@link ShParseError}.
24
+ *
25
+ * @public
26
+ */
27
+ export interface ShParseErrorInfo {
28
+ /** 1-based line number, exactly as reported by mvdan/sh. */
29
+ line: number;
30
+ /**
31
+ * 1-based column number, in **UTF-16 code units** — converted from
32
+ * mvdan/sh's own byte-counting column so it agrees with
33
+ * {@link ShNode.loc}'s columns (mvdan/sh reports `column` as a byte count
34
+ * on the source line, not a UTF-16 code unit count; see
35
+ * design/ARCHITECTURE.md's "Byte→UTF-16 conversion"). For ASCII-only
36
+ * source text this is numerically identical to mvdan/sh's own column.
37
+ */
38
+ column: number;
39
+ /** The filename used for the parse (see {@link ParseOptions.filename}). */
40
+ filename: string;
41
+ }
42
+ /**
43
+ * Thrown by {@link parseSync} when shell source fails to parse. Carries the
44
+ * real position mvdan/sh reported — never 1:1 placeholders: `.line` is
45
+ * 1-based exactly as mvdan/sh reports it, and `.column` is converted to
46
+ * UTF-16 code units so it agrees with {@link ShNode.loc}'s columns (mvdan/sh
47
+ * itself reports `column` as a byte count on the source line — see
48
+ * {@link ShParseErrorInfo.column}). The unconverted, raw byte-column
49
+ * position appears only inside `.message`, mvdan/sh's own formatted string,
50
+ * verbatim.
51
+ *
52
+ * @public
53
+ */
54
+ export declare class ShParseError extends ShBridgeError {
55
+ readonly code = "ESLINT_SH_PARSE_ERROR";
56
+ readonly line: number;
57
+ readonly column: number;
58
+ readonly filename: string;
59
+ constructor(message: string, info: ShParseErrorInfo);
60
+ }
61
+ /**
62
+ * Thrown by {@link parseSync} when {@link ParseOptions.dialect} is not one
63
+ * of the supported {@link ShellDialect} values. Carries the rejected value
64
+ * as `.dialect`; `.message` lists the supported dialects. A foreseeable,
65
+ * actionable user mistake — never an instance of {@link ShParseError}.
66
+ *
67
+ * @public
68
+ */
69
+ export declare class ShInvalidDialectError extends ShBridgeError {
70
+ readonly code = "ESLINT_SH_INVALID_DIALECT";
71
+ /** The rejected dialect value, exactly as passed to {@link parseSync}. */
72
+ readonly dialect: string;
73
+ constructor(dialect: string, supportedDialects: readonly ShellDialect[]);
74
+ }
75
+ /**
76
+ * Thrown by {@link parseSync} for failures that should never happen given a
77
+ * correctly-behaving WASM shim: a malformed result envelope, an unexpected
78
+ * root node type, or any other shim contract violation. Distinct from
79
+ * {@link ShInvalidDialectError} and {@link ShParseError}, both of which
80
+ * indicate a foreseeable, actionable user mistake rather than an internal
81
+ * defect.
82
+ *
83
+ * @public
84
+ */
85
+ export declare class ShBridgeInternalError extends ShBridgeError {
86
+ readonly code = "ESLINT_SH_BRIDGE_INTERNAL";
87
+ constructor(message: string);
88
+ }
89
+ /**
90
+ * Thrown by `sh-ast/analyze`'s `enumerateCommands` when a tree's genuinely
91
+ * nested structure — subshells within subshells, chained command/process
92
+ * substitutions, deeply nested `if`/`case`/loop/function/`time`/`{ }`
93
+ * bodies, deeply chained `elif` — exceeds its defensive recursion-depth
94
+ * guard. **Not** thrown for a long *linear* chain (`|`/`|&`/`&&`/`||` of
95
+ * any realistic length): `enumerateCommands` traverses those iteratively,
96
+ * so chain length alone never grows this guard's depth counter — only
97
+ * genuine tree nesting does.
98
+ *
99
+ * `enumerateCommands` deliberately fails closed here rather than returning
100
+ * a truncated, partial result: a partial `CommandSite[]` silently omits
101
+ * real command sites, which is a false negative for a permission-hook-style
102
+ * consumer that treats "command not found in the enumeration" as "nothing
103
+ * to worry about" — an explicit, documented throw is safer than a silent
104
+ * under-report. Gate-style consumers should treat this error as `deny`,
105
+ * not fall back to "no commands found, so allow".
106
+ *
107
+ * @public
108
+ */
109
+ export declare class ShAnalyzeMaxDepthError extends ShBridgeError {
110
+ readonly code = "ESLINT_SH_ANALYZE_MAX_DEPTH";
111
+ /** The maximum nesting depth `enumerateCommands` supports; the same value every time (not caller-configurable). */
112
+ readonly maxDepth: number;
113
+ constructor(maxDepth: number);
114
+ }
115
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;;;;;;GAYG;AACH,8BAAsB,aAAc,SAAQ,KAAK;IAC/C;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CAChC;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,YAAa,SAAQ,aAAa;IAC7C,QAAQ,CAAC,IAAI,2BAA2B;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAEd,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB;CAOpD;AAED;;;;;;;GAOG;AACH,qBAAa,qBAAsB,SAAQ,aAAa;IACtD,QAAQ,CAAC,IAAI,+BAA+B;IAC5C,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEb,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,SAAS,YAAY,EAAE;CAOxE;AAED;;;;;;;;;GASG;AACH,qBAAa,qBAAsB,SAAQ,aAAa;IACtD,QAAQ,CAAC,IAAI,+BAA+B;gBAEhC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,sBAAuB,SAAQ,aAAa;IACvD,QAAQ,CAAC,IAAI,iCAAiC;IAC9C,mHAAmH;IACnH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAEd,QAAQ,EAAE,MAAM;CAO7B"}
package/dist/errors.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Common base class for every error this package throws — originally just
3
+ * {@link parseSync}'s errors, now also the `sh-ast/analyze` layer's (see
4
+ * {@link ShAnalyzeMaxDepthError}). Provides a stable, documented `code`
5
+ * discriminator (e.g. `"ESLINT_SH_PARSE_ERROR"`) alongside the usual
6
+ * `instanceof` narrowing, so consumers can branch on failure kind
7
+ * programmatically without parsing `.message` strings. Never thrown
8
+ * directly — only via its concrete subclasses ({@link ShParseError},
9
+ * {@link ShInvalidDialectError}, {@link ShBridgeInternalError},
10
+ * {@link ShAnalyzeMaxDepthError}).
11
+ *
12
+ * @public
13
+ */
14
+ export class ShBridgeError extends Error {
15
+ }
16
+ /**
17
+ * Thrown by {@link parseSync} when shell source fails to parse. Carries the
18
+ * real position mvdan/sh reported — never 1:1 placeholders: `.line` is
19
+ * 1-based exactly as mvdan/sh reports it, and `.column` is converted to
20
+ * UTF-16 code units so it agrees with {@link ShNode.loc}'s columns (mvdan/sh
21
+ * itself reports `column` as a byte count on the source line — see
22
+ * {@link ShParseErrorInfo.column}). The unconverted, raw byte-column
23
+ * position appears only inside `.message`, mvdan/sh's own formatted string,
24
+ * verbatim.
25
+ *
26
+ * @public
27
+ */
28
+ export class ShParseError extends ShBridgeError {
29
+ code = 'ESLINT_SH_PARSE_ERROR';
30
+ line;
31
+ column;
32
+ filename;
33
+ constructor(message, info) {
34
+ super(message);
35
+ this.name = 'ShParseError';
36
+ this.line = info.line;
37
+ this.column = info.column;
38
+ this.filename = info.filename;
39
+ }
40
+ }
41
+ /**
42
+ * Thrown by {@link parseSync} when {@link ParseOptions.dialect} is not one
43
+ * of the supported {@link ShellDialect} values. Carries the rejected value
44
+ * as `.dialect`; `.message` lists the supported dialects. A foreseeable,
45
+ * actionable user mistake — never an instance of {@link ShParseError}.
46
+ *
47
+ * @public
48
+ */
49
+ export class ShInvalidDialectError extends ShBridgeError {
50
+ code = 'ESLINT_SH_INVALID_DIALECT';
51
+ /** The rejected dialect value, exactly as passed to {@link parseSync}. */
52
+ dialect;
53
+ constructor(dialect, supportedDialects) {
54
+ super(`bridge: unrecognized shell dialect "${dialect}"; supported dialects are: ${supportedDialects.join(', ')}`);
55
+ this.name = 'ShInvalidDialectError';
56
+ this.dialect = dialect;
57
+ }
58
+ }
59
+ /**
60
+ * Thrown by {@link parseSync} for failures that should never happen given a
61
+ * correctly-behaving WASM shim: a malformed result envelope, an unexpected
62
+ * root node type, or any other shim contract violation. Distinct from
63
+ * {@link ShInvalidDialectError} and {@link ShParseError}, both of which
64
+ * indicate a foreseeable, actionable user mistake rather than an internal
65
+ * defect.
66
+ *
67
+ * @public
68
+ */
69
+ export class ShBridgeInternalError extends ShBridgeError {
70
+ code = 'ESLINT_SH_BRIDGE_INTERNAL';
71
+ constructor(message) {
72
+ super(message);
73
+ this.name = 'ShBridgeInternalError';
74
+ }
75
+ }
76
+ /**
77
+ * Thrown by `sh-ast/analyze`'s `enumerateCommands` when a tree's genuinely
78
+ * nested structure — subshells within subshells, chained command/process
79
+ * substitutions, deeply nested `if`/`case`/loop/function/`time`/`{ }`
80
+ * bodies, deeply chained `elif` — exceeds its defensive recursion-depth
81
+ * guard. **Not** thrown for a long *linear* chain (`|`/`|&`/`&&`/`||` of
82
+ * any realistic length): `enumerateCommands` traverses those iteratively,
83
+ * so chain length alone never grows this guard's depth counter — only
84
+ * genuine tree nesting does.
85
+ *
86
+ * `enumerateCommands` deliberately fails closed here rather than returning
87
+ * a truncated, partial result: a partial `CommandSite[]` silently omits
88
+ * real command sites, which is a false negative for a permission-hook-style
89
+ * consumer that treats "command not found in the enumeration" as "nothing
90
+ * to worry about" — an explicit, documented throw is safer than a silent
91
+ * under-report. Gate-style consumers should treat this error as `deny`,
92
+ * not fall back to "no commands found, so allow".
93
+ *
94
+ * @public
95
+ */
96
+ export class ShAnalyzeMaxDepthError extends ShBridgeError {
97
+ code = 'ESLINT_SH_ANALYZE_MAX_DEPTH';
98
+ /** The maximum nesting depth `enumerateCommands` supports; the same value every time (not caller-configurable). */
99
+ maxDepth;
100
+ constructor(maxDepth) {
101
+ super(`enumerateCommands: exceeded the maximum supported structural nesting depth (${String(maxDepth)} frames) — refusing to return a partial result. Treat this as a deny signal, not "no commands found".`);
102
+ this.name = 'ShAnalyzeMaxDepthError';
103
+ this.maxDepth = maxDepth;
104
+ }
105
+ }
106
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,MAAM,OAAgB,aAAc,SAAQ,KAAK;CAMhD;AAuBD;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,YAAa,SAAQ,aAAa;IACpC,IAAI,GAAG,uBAAuB,CAAC;IAC/B,IAAI,CAAS;IACb,MAAM,CAAS;IACf,QAAQ,CAAS;IAE1B,YAAY,OAAe,EAAE,IAAsB;QACjD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;IAChC,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,qBAAsB,SAAQ,aAAa;IAC7C,IAAI,GAAG,2BAA2B,CAAC;IAC5C,0EAA0E;IACjE,OAAO,CAAS;IAEzB,YAAY,OAAe,EAAE,iBAA0C;QACrE,KAAK,CACH,uCAAuC,OAAO,8BAA8B,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC3G,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;QACpC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF;AAED;;;;;;;;;GASG;AACH,MAAM,OAAO,qBAAsB,SAAQ,aAAa;IAC7C,IAAI,GAAG,2BAA2B,CAAC;IAE5C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,OAAO,sBAAuB,SAAQ,aAAa;IAC9C,IAAI,GAAG,6BAA6B,CAAC;IAC9C,mHAAmH;IAC1G,QAAQ,CAAS;IAE1B,YAAY,QAAgB;QAC1B,KAAK,CACH,+EAA+E,MAAM,CAAC,QAAQ,CAAC,uGAAuG,CACvM,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACrC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF"}