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,28 @@
1
+ import type { ShNode } from '../types.js';
2
+ /**
3
+ * True iff `value` has the minimal shape every normalized {@link ShNode}
4
+ * carries (an object, not an array, with a string `type` field). Used to
5
+ * narrow `unknown` field values discovered while walking a node's own
6
+ * properties — every node field on {@link ShNode} is typed `unknown` (see
7
+ * its index signature), so any code that inspects a specific field's value
8
+ * needs this runtime check before treating it as a child node.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare function isShNodeShape(value: unknown): value is ShNode;
13
+ /**
14
+ * Filters `value` down to the {@link ShNode} elements of an array-shaped
15
+ * field, discarding anything else — and returning `[]` for a non-array or
16
+ * `undefined` field, so callers never need a separate `Array.isArray` guard.
17
+ *
18
+ * @internal
19
+ */
20
+ export declare function nodeArray(value: unknown): readonly ShNode[];
21
+ /**
22
+ * Reads `node[field]` as a string, defaulting to `''` for any other shape
23
+ * (missing field, non-string value).
24
+ *
25
+ * @internal
26
+ */
27
+ export declare function stringField(node: ShNode, field: string): string;
28
+ //# sourceMappingURL=node-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-helpers.d.ts","sourceRoot":"","sources":["../../src/analyze/node-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAO7D;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,MAAM,EAAE,CAE3D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAG/D"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * True iff `value` has the minimal shape every normalized {@link ShNode}
3
+ * carries (an object, not an array, with a string `type` field). Used to
4
+ * narrow `unknown` field values discovered while walking a node's own
5
+ * properties — every node field on {@link ShNode} is typed `unknown` (see
6
+ * its index signature), so any code that inspects a specific field's value
7
+ * needs this runtime check before treating it as a child node.
8
+ *
9
+ * @internal
10
+ */
11
+ export function isShNodeShape(value) {
12
+ return (typeof value === 'object' &&
13
+ value !== null &&
14
+ !Array.isArray(value) &&
15
+ typeof value.type === 'string');
16
+ }
17
+ /**
18
+ * Filters `value` down to the {@link ShNode} elements of an array-shaped
19
+ * field, discarding anything else — and returning `[]` for a non-array or
20
+ * `undefined` field, so callers never need a separate `Array.isArray` guard.
21
+ *
22
+ * @internal
23
+ */
24
+ export function nodeArray(value) {
25
+ return Array.isArray(value) ? value.filter(isShNodeShape) : [];
26
+ }
27
+ /**
28
+ * Reads `node[field]` as a string, defaulting to `''` for any other shape
29
+ * (missing field, non-string value).
30
+ *
31
+ * @internal
32
+ */
33
+ export function stringField(node, field) {
34
+ const value = node[field];
35
+ return typeof value === 'string' ? value : '';
36
+ }
37
+ //# sourceMappingURL=node-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-helpers.js","sourceRoot":"","sources":["../../src/analyze/node-helpers.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACrB,OAAQ,KAA4B,CAAC,IAAI,KAAK,QAAQ,CACvD,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,KAAc;IACtC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACjE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,KAAa;IACrD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1B,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAChD,CAAC"}
@@ -0,0 +1,146 @@
1
+ import type { ShNode } from '../types.js';
2
+ /**
3
+ * Why a {@link resolveWord} result is `static: false`: the word's exact
4
+ * text cannot be determined from source alone.
5
+ *
6
+ * - `'expansion'` — the word contains a `ParamExp` (`$x`, `${x}`), a
7
+ * `CmdSubst` (command substitution, either `$(...)` or backtick form), an
8
+ * `ArithmExp` (`$((expr))`), or a `ProcSubst` (`<(cmd)`/`>(cmd)`) part:
9
+ * its value depends on program state (variables, command output, process
10
+ * substitution) that only exists at run time.
11
+ * - `'tilde'` — an unquoted, unescaped `~` triggers Bash tilde expansion
12
+ * (Bash Reference Manual §3.5.2, "Tilde Expansion") at the start of the
13
+ * word, or — when `resolveWord`'s `context` option is `'assignment-value'`
14
+ * or omitted — immediately after an unquoted `:` anywhere in the word too
15
+ * (§3.5.2 also documents this for assignment-statement values, e.g.
16
+ * `PATH=/foo:~/bar`). Either way: statically shaped, but its value
17
+ * depends on the filesystem/environment (`$HOME`, NSS user database
18
+ * lookups) at run time. See `resolveWord`'s `context` parameter doc for
19
+ * the full command-argument-vs-assignment-value distinction.
20
+ * - `'glob'` — the word contains an unquoted, unescaped glob metacharacter
21
+ * (`*`/`?`), an unquoted bracket expression (`[...]` — an unquoted `[`
22
+ * with a later unquoted `]` in the same word, POSIX 2.13.1 / Bash
23
+ * Reference Manual §3.5.8.1, "Pattern Matching"), or an `ExtGlob` part
24
+ * (Bash Reference Manual §3.5.8, "Filename Expansion"): its value depends
25
+ * on which filesystem entries exist at run time.
26
+ * - `'brace'` — the word contains a `BraceExp` part (`{a,b}`/`{1..10}`,
27
+ * Bash Reference Manual §3.5.1, "Brace Expansion"). mvdan/sh v3.13.1 only
28
+ * ever produces this node type when its caller applies `syntax.SplitBraces`
29
+ * to the parsed tree; this bridge's shim does not call it, so today a
30
+ * brace expression like `{a,b}` reaches {@link resolveWord} as an
31
+ * ordinary `Lit` (indistinguishable, in the AST this bridge produces,
32
+ * from literal `{`/`,`/`}` characters) rather than as a `BraceExp` part.
33
+ * This reason is handled here for forward-compatibility with that node
34
+ * type and is not reachable through {@link resolveWord} today.
35
+ * - `'locale'` — the word contains a `$"..."` locale-translated string
36
+ * (Bash Reference Manual §3.1.2.5, "Locale-Specific Translation"): its
37
+ * value is looked up in the current locale's translation catalog at run
38
+ * time via `gettext`, regardless of whether its contents also contain an
39
+ * expansion. `'locale'` takes precedence over any `'expansion'` a
40
+ * `$"..."` word's *contents* would otherwise report — the `DblQuoted`
41
+ * node's own `dollar` flag is checked before its children are visited at
42
+ * all, so e.g. `$"hi $x"` reports `'locale'`, never `'expansion'`.
43
+ * - `'unsupported'` — the word contains a `$'...'` (ANSI-C quoted) `\u`/`\U`
44
+ * escape whose value has no corresponding Unicode character (greater than
45
+ * `U+10FFFF`, e.g. `$'\UFFFFFFFF'`): real bash's output for this case is
46
+ * an undocumented, non-UTF-8 byte sequence with no exact representation
47
+ * as a JavaScript string (see `ansi-c-escapes.ts`'s
48
+ * `MAX_UNICODE_CODE_POINT` doc comment for the empirical detail) — this
49
+ * package declines to claim a `static: true` text bash never actually
50
+ * produces.
51
+ *
52
+ * This union may grow in a **minor** release — new mvdan/sh word-part node
53
+ * types, or new statically-shaped-but-dynamic categories, can add new reason
54
+ * strings without that being a breaking change. It is deliberately not
55
+ * sealed against extension elsewhere in the codebase. A reason string this
56
+ * version of the package doesn't yet know about only ever accompanies
57
+ * `static: false`, never changes the meaning of `static: true`, so runtime
58
+ * consumers that only branch on `.static` are safe by construction across
59
+ * such additions; an exhaustive compile-time `switch` over `.reason` should
60
+ * still include a `default` case to stay forward-compatible.
61
+ *
62
+ * @public
63
+ */
64
+ export type WordResolutionReason = 'expansion' | 'tilde' | 'glob' | 'brace' | 'locale' | 'unsupported';
65
+ /**
66
+ * The result of statically resolving a `Word` node's text. Reports facts
67
+ * only — never a safety verdict, never a hardcoded command/wrapper list.
68
+ * `static: false` ("unknowable") is a first-class, valid result, not an
69
+ * error condition.
70
+ *
71
+ * @public
72
+ */
73
+ export type WordResolution = {
74
+ /** The word is a compile-time-known string. */
75
+ readonly static: true;
76
+ /** The word's exact resolved text (quotes and escapes removed). */
77
+ readonly text: string;
78
+ } | {
79
+ /** The word's value depends on program state, the environment, or the filesystem. */
80
+ readonly static: false;
81
+ /** Why this word cannot be statically resolved. */
82
+ readonly reason: WordResolutionReason;
83
+ };
84
+ /**
85
+ * Options controlling {@link resolveWord}'s tilde-expansion detection.
86
+ *
87
+ * @public
88
+ */
89
+ export interface ResolveWordOptions {
90
+ /**
91
+ * Which Bash grammar position `word` occupies — the two positions Bash
92
+ * §3.5.2 ("Tilde Expansion") documents distinct tilde-expansion triggers
93
+ * for:
94
+ *
95
+ * - `'command-argument'` — an ordinary simple-command argument word (e.g.
96
+ * `echo <word>`). Only a *word-initial* unquoted `~` triggers tilde
97
+ * expansion; an unquoted `~` elsewhere in the word (including right
98
+ * after a `:`) is an ordinary character (`echo a:~b` prints `a:~b`
99
+ * literally in real bash).
100
+ * - `'assignment-value'` — the value side of a shell assignment (e.g.
101
+ * `PATH=<word>`, an `Assign` node's `value`). Bash additionally
102
+ * tilde-expands an unquoted `~` immediately following an unquoted `:`
103
+ * anywhere in the value (`PATH=/foo:~/bar` expands the `~/bar`
104
+ * segment) — this is documented for `PATH`-like colon-separated
105
+ * assignment values specifically.
106
+ *
107
+ * **Default: `'assignment-value'` semantics apply whenever this option is
108
+ * omitted.** `resolveWord` takes a bare `Word` node with no grammar
109
+ * context attached, so it cannot see whether its caller extracted that
110
+ * word from a command argument or an assignment value — omitting this
111
+ * option is deliberately treated as the *more conservative* of the two
112
+ * (the one that reports `'tilde'` in more cases), so a caller that
113
+ * doesn't know or care about the distinction never under-reports a real
114
+ * tilde-expansion site. Pass `'command-argument'` explicitly to opt into
115
+ * the narrower, word-initial-only check that matches real bash's
116
+ * behavior for ordinary arguments.
117
+ *
118
+ * @see https://www.gnu.org/software/bash/manual/bash.html#Tilde-Expansion
119
+ */
120
+ readonly context?: 'command-argument' | 'assignment-value';
121
+ }
122
+ /**
123
+ * Determines whether a `Word` node is statically a known string, and if
124
+ * so, what that string is — the atom of shell-AST static analysis (e.g.
125
+ * "is this word literally `rm`"). A word is static iff every part is a
126
+ * literal after shell unquoting: a bare `Lit` (backslash escapes
127
+ * processed), a `SglQuoted` (including `$'...'` ANSI-C escapes decoded),
128
+ * or a `DblQuoted` containing only literal parts (and not itself a
129
+ * `$"..."` locale translation) — including concatenations of these (`'r'm`
130
+ * resolves to `"rm"`). A word containing any expansion (`ParamExp`,
131
+ * `CmdSubst`, `ArithmExp`, `ProcSubst`), a triggering unquoted `~` (see
132
+ * `options.context`), an unquoted glob metacharacter/bracket
133
+ * expression/`ExtGlob`, a `$"..."` locale translation, or an unrepresentable
134
+ * `$'\U...'` escape is `static: false` with a reason — never an error, and
135
+ * never a safety judgment (see {@link WordResolution}).
136
+ *
137
+ * @param word - A `Word` node, e.g. `parseSync(...).stmts[0].cmd.args[0]`.
138
+ * @param options - See {@link ResolveWordOptions}.
139
+ * @throws TypeError if `word.type` is not `"Word"` — a programmer-error
140
+ * misuse of the API, not a "malformed shell source" case (well-formed
141
+ * shell source, however dynamic, never throws; see {@link WordResolution}).
142
+ *
143
+ * @public
144
+ */
145
+ export declare function resolveWord(word: ShNode, options?: ResolveWordOptions): WordResolution;
146
+ //# sourceMappingURL=resolve-word.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-word.d.ts","sourceRoot":"","sources":["../../src/analyze/resolve-word.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAK1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,MAAM,MAAM,oBAAoB,GAC9B,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,aAAa,CAAC;AAEtE;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,GACtB;IACE,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IACtB,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,GACD;IACE,qFAAqF;IACrF,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;IACvB,mDAAmD;IACnD,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;CACvC,CAAC;AAEN;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,kBAAkB,CAAC;CAC5D;AA6JD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,cAAc,CA4BtF"}
@@ -0,0 +1,202 @@
1
+ import { decodeAnsiCString } from './ansi-c-escapes.js';
2
+ import { decodeDblQuotedLit, decodeUnquotedLit } from './decode-lit.js';
3
+ import { nodeArray, stringField } from './node-helpers.js';
4
+ /**
5
+ * True iff `raw` (a `Lit` part's *undecoded* value) begins with a literal
6
+ * `~` character. Only meaningful when checked against a `Word`'s very
7
+ * first part: Bash tilde expansion (§3.5.2) triggers only at the start of
8
+ * a word, so `a~b` (mid-word) and `\~foo` (escaped — `raw` starts with a
9
+ * backslash, not `~`) are correctly excluded, while `~"foo"` (a leading
10
+ * unquoted `~` followed by a separately-quoted part) is correctly
11
+ * included, since it is still a distinct first `Lit` part starting with an
12
+ * unquoted `~`.
13
+ */
14
+ function startsWithUnescapedTilde(raw) {
15
+ return raw.startsWith('~');
16
+ }
17
+ /**
18
+ * True iff an unquoted, unescaped `~` immediately follows an unquoted,
19
+ * unescaped `:` anywhere across `parts` — Bash §3.5.2's assignment-value
20
+ * tilde-expansion trigger (e.g. the `~/bar` in `PATH=/foo:~/bar`).
21
+ *
22
+ * Only a `Word`'s top-level `Lit` parts carry unquoted text; any other
23
+ * part type (`SglQuoted`, `DblQuoted`, `ParamExp`, …) breaks adjacency —
24
+ * whatever text or expansion it represents sits between the `:` and any
25
+ * following `~`, so they are no longer *immediately* adjacent. `:`/`~`
26
+ * adjacency is tracked across concatenated `Lit` parts (state carried
27
+ * between loop iterations), since concatenation joins parts with no
28
+ * grammar-level separator between them.
29
+ */
30
+ function hasUnquotedColonTilde(parts) {
31
+ let prevWasUnescapedColon = false;
32
+ for (const part of parts) {
33
+ if (part.type !== 'Lit') {
34
+ prevWasUnescapedColon = false;
35
+ continue;
36
+ }
37
+ const raw = stringField(part, 'value');
38
+ let i = 0;
39
+ while (i < raw.length) {
40
+ const ch = raw[i];
41
+ if (ch === '\\' && i + 1 < raw.length) {
42
+ // An escaped character (whatever it is) is never an unescaped `:`
43
+ // or `~`, and breaks adjacency for the purposes of this scan.
44
+ prevWasUnescapedColon = false;
45
+ i += 2;
46
+ continue;
47
+ }
48
+ if (prevWasUnescapedColon && ch === '~') {
49
+ return true;
50
+ }
51
+ prevWasUnescapedColon = ch === ':';
52
+ i += 1;
53
+ }
54
+ }
55
+ return false;
56
+ }
57
+ /**
58
+ * True iff `parts` contains an unquoted bracket expression: an unescaped
59
+ * `[` with a later unescaped `]` anywhere in the same word (POSIX 2.13.1 /
60
+ * Bash Reference Manual §3.5.8.1, "Pattern Matching"). `a[bc` (no closing
61
+ * bracket) is a *literal* filename in Bash; `a[bc]d` is a pattern. The
62
+ * closing `]` may live in a different, concatenated `Lit` part than the
63
+ * opening `[` (`decodeUnquotedLit`'s `brackets` output is concatenated,
64
+ * in part order, across every top-level `Lit` part first) — and content
65
+ * *between* the two, even if it came from an unquoted or quoted part, does
66
+ * not prevent them from forming a bracket expression (`a["b"]` matches
67
+ * real bash's file `ab`, verified empirically: quoting `"b"` doesn't
68
+ * suppress the surrounding unquoted `[`/`]`). Only `Lit` parts contribute
69
+ * to the scan — `SglQuoted`/`DblQuoted` text is fully quoted and never
70
+ * glob-significant (`a'['bc']'d`'s brackets are both quoted, so it stays
71
+ * literal).
72
+ */
73
+ function hasUnquotedBracketExpression(parts) {
74
+ let brackets = '';
75
+ for (const part of parts) {
76
+ if (part.type !== 'Lit')
77
+ continue;
78
+ brackets += decodeUnquotedLit(stringField(part, 'value')).brackets;
79
+ }
80
+ const openIndex = brackets.indexOf('[');
81
+ if (openIndex === -1)
82
+ return false;
83
+ return brackets.includes(']', openIndex + 1);
84
+ }
85
+ function resolvePart(part, quoting) {
86
+ switch (part.type) {
87
+ case 'Lit': {
88
+ const raw = stringField(part, 'value');
89
+ if (quoting === 'dblquoted') {
90
+ return { ok: true, text: decodeDblQuotedLit(raw) };
91
+ }
92
+ const { text, hasGlob } = decodeUnquotedLit(raw);
93
+ if (hasGlob)
94
+ return { ok: false, reason: 'glob' };
95
+ return { ok: true, text };
96
+ }
97
+ case 'SglQuoted': {
98
+ const raw = stringField(part, 'value');
99
+ // ANSI-C quoting ($'...') is flagged by SglQuoted.Dollar (mvdan/sh
100
+ // v3.13.1's `syntax.SglQuoted.Dollar bool`); plain single-quoted
101
+ // text ('...') never has any escapes interpreted at all.
102
+ if (part.dollar === true) {
103
+ const decoded = decodeAnsiCString(raw);
104
+ if (!decoded.ok)
105
+ return { ok: false, reason: 'unsupported' };
106
+ return { ok: true, text: decoded.text };
107
+ }
108
+ return { ok: true, text: raw };
109
+ }
110
+ case 'DblQuoted': {
111
+ // Locale translation ($"...") is flagged by DblQuoted.Dollar
112
+ // (mvdan/sh v3.13.1's `syntax.DblQuoted.Dollar bool`) and is checked
113
+ // before descending into this node's children: the whole construct
114
+ // is locale-dependent regardless of its contents (Bash Reference
115
+ // Manual §3.1.2.5), so 'locale' deterministically takes precedence
116
+ // over any 'expansion' (or other reason) its contents would
117
+ // otherwise report — see WordResolutionReason's 'locale' doc.
118
+ if (part.dollar === true) {
119
+ return { ok: false, reason: 'locale' };
120
+ }
121
+ let text = '';
122
+ for (const inner of nodeArray(part.parts)) {
123
+ const result = resolvePart(inner, 'dblquoted');
124
+ if (!result.ok)
125
+ return result;
126
+ text += result.text;
127
+ }
128
+ return { ok: true, text };
129
+ }
130
+ case 'ParamExp':
131
+ case 'CmdSubst':
132
+ case 'ArithmExp':
133
+ case 'ProcSubst':
134
+ return { ok: false, reason: 'expansion' };
135
+ case 'ExtGlob':
136
+ return { ok: false, reason: 'glob' };
137
+ case 'BraceExp':
138
+ // See WordResolutionReason's 'brace' doc: unreachable via this
139
+ // bridge's parser today, handled for forward-compatibility.
140
+ return { ok: false, reason: 'brace' };
141
+ default:
142
+ // Any word-part node type this module doesn't specifically know
143
+ // about. "Unknowable" must stay a first-class result for well-formed
144
+ // input rather than a thrown error — 'expansion' is the closest
145
+ // neutral bucket (some run-time-dependent mechanism) until the type
146
+ // is specifically handled.
147
+ return { ok: false, reason: 'expansion' };
148
+ }
149
+ }
150
+ /**
151
+ * Determines whether a `Word` node is statically a known string, and if
152
+ * so, what that string is — the atom of shell-AST static analysis (e.g.
153
+ * "is this word literally `rm`"). A word is static iff every part is a
154
+ * literal after shell unquoting: a bare `Lit` (backslash escapes
155
+ * processed), a `SglQuoted` (including `$'...'` ANSI-C escapes decoded),
156
+ * or a `DblQuoted` containing only literal parts (and not itself a
157
+ * `$"..."` locale translation) — including concatenations of these (`'r'm`
158
+ * resolves to `"rm"`). A word containing any expansion (`ParamExp`,
159
+ * `CmdSubst`, `ArithmExp`, `ProcSubst`), a triggering unquoted `~` (see
160
+ * `options.context`), an unquoted glob metacharacter/bracket
161
+ * expression/`ExtGlob`, a `$"..."` locale translation, or an unrepresentable
162
+ * `$'\U...'` escape is `static: false` with a reason — never an error, and
163
+ * never a safety judgment (see {@link WordResolution}).
164
+ *
165
+ * @param word - A `Word` node, e.g. `parseSync(...).stmts[0].cmd.args[0]`.
166
+ * @param options - See {@link ResolveWordOptions}.
167
+ * @throws TypeError if `word.type` is not `"Word"` — a programmer-error
168
+ * misuse of the API, not a "malformed shell source" case (well-formed
169
+ * shell source, however dynamic, never throws; see {@link WordResolution}).
170
+ *
171
+ * @public
172
+ */
173
+ export function resolveWord(word, options) {
174
+ if (word.type !== 'Word') {
175
+ throw new TypeError(`resolveWord expects a "Word" node, got "${word.type}"`);
176
+ }
177
+ const parts = nodeArray(word.parts);
178
+ if (parts.length === 0) {
179
+ return { static: true, text: '' };
180
+ }
181
+ // `parts.length === 0` returned above, so `parts[0]` is a real element.
182
+ const first = parts[0];
183
+ if (first.type === 'Lit' && startsWithUnescapedTilde(stringField(first, 'value'))) {
184
+ return { static: false, reason: 'tilde' };
185
+ }
186
+ if (options?.context !== 'command-argument' && hasUnquotedColonTilde(parts)) {
187
+ return { static: false, reason: 'tilde' };
188
+ }
189
+ if (hasUnquotedBracketExpression(parts)) {
190
+ return { static: false, reason: 'glob' };
191
+ }
192
+ let text = '';
193
+ for (const part of parts) {
194
+ const result = resolvePart(part, 'unquoted');
195
+ if (!result.ok) {
196
+ return { static: false, reason: result.reason };
197
+ }
198
+ text += result.text;
199
+ }
200
+ return { static: true, text };
201
+ }
202
+ //# sourceMappingURL=resolve-word.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-word.js","sourceRoot":"","sources":["../../src/analyze/resolve-word.ts"],"names":[],"mappings":"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"}