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
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # sh-ast
2
+
3
+ A synchronous, fully-typed shell AST for JavaScript and TypeScript. `sh-ast` parses shell scripts
4
+ (bash, POSIX sh, mksh, zsh, and bats) using [mvdan/sh](https://github.com/mvdan/sh) — the Go
5
+ shell parser used by `shfmt` and `shellcheck`-adjacent tooling — via a small Go/WASM shim, and
6
+ normalizes the result into a plain-object AST with UTF-16-safe positions that's natural to
7
+ consume from JS/TS (linters, formatters, codemods, static analysis, or anything else that needs
8
+ to understand shell source structurally).
9
+
10
+ ```ts
11
+ import { parseSync } from 'sh-ast';
12
+
13
+ const file = parseSync('echo hi');
14
+ file.stmts[0].cmd.type; // "CallExpr"
15
+ ```
16
+
17
+ ## Provenance
18
+
19
+ `sh-ast` was extracted from [`eslint-sh`](https://github.com/mike-north/eslint-sh) (as
20
+ `@eslint-sh/bridge`) at commit
21
+ [`bdd55b3`](https://github.com/mike-north/eslint-sh/commit/bdd55b3), where it was built as the
22
+ parser core underneath an ESLint plugin for shell scripts. The full commit history predating the
23
+ extraction lives in that repository. This package carries no ESLint dependency and exposes the
24
+ same public API `@eslint-sh/bridge` did.
25
+
26
+ ## Status
27
+
28
+ The visitor-keys and child-type-schema tables, plus the `ShNodes.*` TypeScript typings, are
29
+ generated from mvdan/sh's `syntax` package struct definitions by `tools/gen-visitor-keys` — see
30
+ that tool's README for "The schema table is generated, not hand-written".
31
+
32
+ ## Public API
33
+
34
+ - `parseSync(text, options?)` — synchronous parse; throws `ShParseError` (with `.line`,
35
+ `.column`, `.filename`) on a shell syntax error.
36
+ - `ParseOptions` — `{ dialect?, filename? }`; `dialect` is one of `bash | posix | mksh | bats |
37
+ zsh`, mapped to mvdan/sh's `LangVariant` by string name.
38
+ - `ShNode` / `ShFile` — normalized node shape (`type`, `range`, `loc`, plus lowercased fields
39
+ from mvdan/sh's tree).
40
+ - `ShNodes` — generated, strongly-typed `ShNode` subtypes for every mvdan/sh node type (e.g.
41
+ `ShNodes.ShCallExprNode`, `ShNodes.ShDeclClauseNode`), plus union aliases (`ShAnyNode`,
42
+ `ShCommandNode`, `ShWordPartNode`, …). Additive and optional — `ShNode`'s index signature
43
+ remains the base contract.
44
+ - `visitorKeys` — generated node-type → child-field-name table.
45
+ - `walk(node, visit)` — visits every node in a normalized tree; discovers children structurally
46
+ rather than depending on `visitorKeys`.
47
+
48
+ ## Regenerating the schema tables
49
+
50
+ After bumping the pinned mvdan/sh version (`shim/go.mod` _and_ `../../tools/gen-visitor-keys/go.mod`
51
+ must move together):
52
+
53
+ ```sh
54
+ pnpm run generate:sh-ast-schema
55
+ ```
56
+
57
+ This regenerates `generated/visitor-keys.js(.d.ts)`, `generated/child-type-schema.js(.d.ts)`, and
58
+ `generated/node-types.d.ts`, then formats them with Prettier. CI
59
+ (`.github/workflows/gen-visitor-keys.yml`) reruns the same script and fails the build if the
60
+ committed artifacts don't come back byte-identical — the drift gate for future mvdan/sh bumps.
61
+ Re-run the kitchen-sink golden test (`test/kitchen-sink.test.ts`) afterward; it fails if the new
62
+ mvdan/sh version adds a node type the fixtures don't yet exercise.
63
+
64
+ ## The WASM shim
65
+
66
+ `shim/main.go` parses with `mvdan.cc/sh/v3/syntax` and encodes the result with
67
+ `shim/internal/nodeencode` — a performance fork of `mvdan.cc/sh/v3/syntax/typedjson`'s encode
68
+ path (see that package's doc comment for why: typedjson's reflection-based encoder rebuilds an
69
+ identical synthetic struct type per node _instance_ rather than once per node _type_, which
70
+ dominated warm-parse latency — see "Performance" below) — so interface-typed nodes (`CallExpr`,
71
+ `BinaryCmd`, …) carry a `Type` discriminator, exactly as upstream typedjson does (verified
72
+ byte-identical in `shim/internal/nodeencode/encode_test.go`). The whole result envelope —
73
+ including any error message — is marshaled with Go's `encoding/json`, so JSON-significant bytes
74
+ in an error message (e.g. an unclosed-quote message containing `"`) survive the Go→JS boundary
75
+ intact.
76
+
77
+ The shim exposes a linear-memory ABI — `alloc`/`process`/`free` WASM exports plus the `mem`
78
+ memory export — instead of a `syscall/js` global function. `wasm-instance.ts` writes UTF-8 bytes
79
+ directly into WASM memory and reads the JSON result back with `TextDecoder`, avoiding
80
+ `syscall/js`'s much more expensive per-call JS↔Go value marshaling (see "Performance" below).
81
+
82
+ ### Performance
83
+
84
+ Local measurement (700-line/~9k-node fixture, warm instance, Apple M-series):
85
+
86
+ | Stage | Before (syscall/js + typedjson) | After (linear memory + nodeencode) |
87
+ | ------------------------------------- | ------------------------------: | ---------------------------------: |
88
+ | End-to-end warm `parseSync` (p50) | ~97 ms | ~29 ms |
89
+ | wasm call (parse + encode + transfer) | ~91 ms | ~23.5 ms |
90
+ | `JSON.parse` | ~3 ms | ~3 ms |
91
+ | `normalize` | ~2.5 ms | ~2.7 ms |
92
+ | `sh-ast.wasm` size | ~4.10 MB | ~3.82 MB |
93
+
94
+ The dominant fix was **not** the transport switch alone — profiling found `typedjson.Encode`'s
95
+ `reflect.StructOf` call (rebuilding an identical synthetic struct type once per node _instance_)
96
+ responsible for ~80ms of the ~90ms wasm call; `nodeencode`'s per-_type_ cache cut that to ~14ms.
97
+ The linear-memory ABI switch and a hand-rolled envelope wrap (skipping a redundant
98
+ `encoding/json.Marshal` re-compaction of the already-encoded AST bytes) account for the rest.
99
+
100
+ **TinyGo**, evaluated for its ~4x smaller runtime, cannot run this shim: it compiles
101
+ `reflect.StructOf` calls without error but panics at runtime with `unimplemented:
102
+ reflect.StructOf()` the moment `nodeencode`/`typedjson`'s encoder runs (confirmed against
103
+ TinyGo 0.41.1). Standard Go + `wasm-opt -Oz` (binaryen, pinned in CI) is the fallback:
104
+ `sh-ast.wasm` is optimized after every build, for a ~7% size reduction (~4.09 MB → ~3.82 MB) with
105
+ no behavior change. Neither path reaches a 1.5 MB stretch target.
106
+
107
+ After editing `shim/main.go` or `shim/internal/nodeencode`, rebuild the committed
108
+ `shim/sh-ast.wasm`:
109
+
110
+ ```sh
111
+ cd shim
112
+ GOOS=js GOARCH=wasm go build -buildvcs=false -trimpath -ldflags="-s -w -buildid=" -o sh-ast.wasm .
113
+ wasm-opt -Oz --enable-bulk-memory-opt --enable-nontrapping-float-to-int sh-ast.wasm -o sh-ast.wasm
114
+ ```
115
+
116
+ `wasm-opt` ships with [binaryen](https://github.com/WebAssembly/binaryen/releases) — CI installs
117
+ a pinned release (see `.github/workflows/wasm-reproducibility.yml`'s `BINARYEN_VERSION`); install
118
+ the same version locally (`brew install binaryen`, or download the pinned release directly) so
119
+ your rebuild matches CI byte-for-byte. `--enable-bulk-memory-opt` and
120
+ `--enable-nontrapping-float-to-int` tell `wasm-opt` to accept the two WASM proposal features
121
+ Go's `js/wasm` target already emits (both broadly supported in Node and browsers since ~2020) —
122
+ without them, `wasm-opt` rejects the input as invalid.
123
+
124
+ The `-buildid=` flag blanks out Go's build-ID metadata (used only by the Go toolchain's own
125
+ build cache, never read at runtime); without it, `-trimpath` alone is not enough for a
126
+ byte-identical rebuild — the embedded build ID otherwise varies between build-cache states even
127
+ with identical source and dependencies.
128
+
129
+ `-buildvcs=false` is equally required: Go 1.18+ stamps binaries with `vcs.revision`/`vcs.time`/
130
+ `vcs.modified` by default (`-buildvcs=auto`), reading whatever commit and dirty-state the
131
+ checkout happens to be in at build time. That stamp isn't touched by `-s -w` (it lives outside
132
+ the symbol table/DWARF that those flags strip), so two builds of _identical source_ differ
133
+ whenever the checkout's commit or dirty-state differs — which is exactly what happens for a
134
+ `pull_request`-triggered CI run (GitHub synthesizes a fresh merge commit, with its own SHA and
135
+ timestamp, on every run) versus a `push`-triggered run (checks out the named branch commit
136
+ as-is), and equally for a local checkout with any uncommitted changes elsewhere in the working
137
+ tree. `-buildvcs=false` removes this source of non-determinism entirely; verified byte-for-byte
138
+ identical across a native macOS/arm64 build, a `linux/amd64` build under Docker/QEMU emulation,
139
+ and real `ubuntu-latest` CI runs triggered via `push`, `pull_request`, and `workflow_dispatch`.
140
+
141
+ `wasm-opt -Oz` is a deterministic, version-pinned transform: rebuilding twice (Go build +
142
+ wasm-opt) produces byte-identical output, verified locally and by CI's `rebuild-match` job.
143
+
144
+ CI rebuilds the shim with a pinned Go toolchain (`shim/go.mod`) and a pinned `wasm-opt`
145
+ (`.github/workflows/wasm-reproducibility.yml`), and fails if the output differs from the
146
+ committed artifact. It also runs `shim/internal/nodeencode`'s Go test suite, which asserts the
147
+ fork's output is byte-identical to upstream `typedjson.Encode`.
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Decodes `$'...'` (Bash ANSI-C Quoting) escape sequences.
3
+ *
4
+ * mvdan/sh's parser does **not** perform this decoding: for a `SglQuoted`
5
+ * word part with `dollar: true`, the normalized `value` field is the raw,
6
+ * un-decoded source text between the quotes (verified empirically against
7
+ * mvdan.cc/sh/v3 v3.13.1 — see `test/analyze-resolve-word.test.ts`'s module
8
+ * comment for how this was confirmed). This module fills that gap using the
9
+ * escape table the Bash Reference Manual documents, independently of
10
+ * whatever mvdan/sh itself does or doesn't decode.
11
+ *
12
+ * @see https://www.gnu.org/software/bash/manual/bash.html#ANSI_002dC-Quoting
13
+ * @internal
14
+ */
15
+ /**
16
+ * The result of {@link decodeAnsiCString}: either the fully decoded text,
17
+ * or `ok: false` when the raw text contains a `\u`/`\U` escape whose value
18
+ * has no corresponding Unicode character (see {@link MAX_UNICODE_CODE_POINT}'s
19
+ * doc comment for why that case can't be decoded to an exact JavaScript
20
+ * string, and can't be left as literal un-decoded text either without
21
+ * claiming a false `static: true` result). Never throws — callers (see
22
+ * `resolveWord`'s `SglQuoted` handling) turn `ok: false` into
23
+ * `{ static: false, reason: 'unsupported' }` for the whole word.
24
+ *
25
+ * @internal
26
+ */
27
+ export type AnsiCDecodeResult = {
28
+ readonly ok: true;
29
+ readonly text: string;
30
+ } | {
31
+ readonly ok: false;
32
+ };
33
+ /**
34
+ * Decodes the raw inner text of a `$'...'` (ANSI-C quoted) string per Bash
35
+ * Reference Manual §3.1.2.4, "ANSI-C Quoting". `raw` is the text between
36
+ * the quotes (mvdan/sh's `SglQuoted.Value` for a `Dollar: true` node),
37
+ * exactly as it appears in source — not yet interpreted in any way.
38
+ *
39
+ * Recognized escapes: `\a \b \e \E \f \n \r \t \v \\ \' \" \?`, octal
40
+ * `\nnn` (one to three digits), hex `\xHH` (one or two digits), Unicode
41
+ * `\uHHHH` (one to four digits) and `\UHHHHHHHH` (one to eight digits), and
42
+ * control-character `\cX`. An unrecognized backslash sequence (not in this
43
+ * table, and not a recognized numeric escape) is left as-is — Bash's own
44
+ * decoding is best-effort for malformed input, and this function mirrors
45
+ * that rather than throwing, matching {@link resolveWord}'s "never throw
46
+ * for well-formed input" contract at the string level too. A `\u`/`\U`
47
+ * whose value *is* well-formed but has no corresponding Unicode character
48
+ * is a different case — see {@link AnsiCDecodeResult}.
49
+ *
50
+ * @internal
51
+ */
52
+ export declare function decodeAnsiCString(raw: string): AnsiCDecodeResult;
53
+ //# sourceMappingURL=ansi-c-escapes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ansi-c-escapes.d.ts","sourceRoot":"","sources":["../../src/analyze/ansi-c-escapes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAwFH;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,iBAAiB,GAC3B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAA;CAAE,CAAC;AAExE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CA8IhE"}
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Decodes `$'...'` (Bash ANSI-C Quoting) escape sequences.
3
+ *
4
+ * mvdan/sh's parser does **not** perform this decoding: for a `SglQuoted`
5
+ * word part with `dollar: true`, the normalized `value` field is the raw,
6
+ * un-decoded source text between the quotes (verified empirically against
7
+ * mvdan.cc/sh/v3 v3.13.1 — see `test/analyze-resolve-word.test.ts`'s module
8
+ * comment for how this was confirmed). This module fills that gap using the
9
+ * escape table the Bash Reference Manual documents, independently of
10
+ * whatever mvdan/sh itself does or doesn't decode.
11
+ *
12
+ * @see https://www.gnu.org/software/bash/manual/bash.html#ANSI_002dC-Quoting
13
+ * @internal
14
+ */
15
+ /**
16
+ * Hex digit predicate for `\xHH`/`\uHHHH`/`\UHHHHHHHH`. `ch` is expected to
17
+ * come from `String.prototype.charAt`, which returns `''` (never
18
+ * `undefined`) past the end of the string — `''` correctly fails this test,
19
+ * so callers don't need a separate bounds check.
20
+ */
21
+ function isHexDigit(ch) {
22
+ return /^[0-9a-fA-F]$/.test(ch);
23
+ }
24
+ /** Octal digit predicate for `\nnn`; see {@link isHexDigit}'s `charAt` note. */
25
+ function isOctalDigit(ch) {
26
+ return ch.length === 1 && ch >= '0' && ch <= '7';
27
+ }
28
+ /**
29
+ * The highest Unicode scalar value `\uHHHH`/`\UHHHHHHHH` can validly decode
30
+ * to. `\uHHHH` (max 4 hex digits, i.e. `0xFFFF`) can never exceed this, but
31
+ * `\UHHHHHHHH` (up to 8 hex digits, i.e. up to `0xFFFFFFFF`) routinely does.
32
+ *
33
+ * The Bash Reference Manual's ANSI-C Quoting table documents `\UHHHHHHHH`
34
+ * only as "the Unicode ... character whose value is the hexadecimal value
35
+ * HHHHHHHH" — it does not say what happens when that value has no
36
+ * corresponding Unicode character (i.e. is greater than `U+10FFFF`, the
37
+ * highest code point Unicode defines). Real bash's behavior for this case
38
+ * is undocumented and not a reasonable decode target: it falls back to a
39
+ * pre-RFC-3629, up-to-6-byte "UTF-8-like" bit-packing with no range
40
+ * validation (confirmed empirically — e.g. `$'\U00110000'` produces the
41
+ * raw byte sequence `f4 90 80 80`, which is not valid UTF-8 text, and
42
+ * `$'\UFFFFFFFF'` produces no output at all, exit 0). Neither of those is
43
+ * representable as an exact JavaScript string, so this module does **not**
44
+ * fall back to treating the escape as literal, un-decoded text the way it
45
+ * does for other unrecognized escapes — that would silently claim a
46
+ * `static: true` exact text that real bash never produces (a false
47
+ * static). Instead, {@link decodeAnsiCString} reports an out-of-range
48
+ * `\u`/`\U` as *unrepresentable* via {@link AnsiCDecodeResult}, and
49
+ * `resolveWord` propagates that as `{ static: false, reason: 'unsupported' }`
50
+ * for the whole word — never `String.fromCodePoint`, which throws
51
+ * `RangeError` for any value over this limit and would violate
52
+ * `resolveWord`'s "never throw for well-formed input" contract.
53
+ *
54
+ * @see https://www.gnu.org/software/bash/manual/bash.html#ANSI_002dC-Quoting
55
+ */
56
+ const MAX_UNICODE_CODE_POINT = 0x10ffff;
57
+ /**
58
+ * Greedily consumes up to `maxDigits` hex digits starting at `start`,
59
+ * returning the parsed numeric value and how many digits were consumed (`0`
60
+ * if none were valid).
61
+ */
62
+ function readHexDigits(source, start, maxDigits) {
63
+ let digits = '';
64
+ let index = start;
65
+ while (digits.length < maxDigits) {
66
+ const ch = source.charAt(index);
67
+ if (!isHexDigit(ch))
68
+ break;
69
+ digits += ch;
70
+ index += 1;
71
+ }
72
+ return { value: digits.length > 0 ? parseInt(digits, 16) : 0, length: digits.length };
73
+ }
74
+ /**
75
+ * Greedily consumes up to `maxDigits` octal digits starting at `start`,
76
+ * returning the parsed numeric value and how many digits were consumed.
77
+ */
78
+ function readOctalDigits(source, start, maxDigits) {
79
+ let digits = '';
80
+ let index = start;
81
+ while (digits.length < maxDigits) {
82
+ const ch = source.charAt(index);
83
+ if (!isOctalDigit(ch))
84
+ break;
85
+ digits += ch;
86
+ index += 1;
87
+ }
88
+ return { value: digits.length > 0 ? parseInt(digits, 8) : 0, length: digits.length };
89
+ }
90
+ /**
91
+ * Decodes the raw inner text of a `$'...'` (ANSI-C quoted) string per Bash
92
+ * Reference Manual §3.1.2.4, "ANSI-C Quoting". `raw` is the text between
93
+ * the quotes (mvdan/sh's `SglQuoted.Value` for a `Dollar: true` node),
94
+ * exactly as it appears in source — not yet interpreted in any way.
95
+ *
96
+ * Recognized escapes: `\a \b \e \E \f \n \r \t \v \\ \' \" \?`, octal
97
+ * `\nnn` (one to three digits), hex `\xHH` (one or two digits), Unicode
98
+ * `\uHHHH` (one to four digits) and `\UHHHHHHHH` (one to eight digits), and
99
+ * control-character `\cX`. An unrecognized backslash sequence (not in this
100
+ * table, and not a recognized numeric escape) is left as-is — Bash's own
101
+ * decoding is best-effort for malformed input, and this function mirrors
102
+ * that rather than throwing, matching {@link resolveWord}'s "never throw
103
+ * for well-formed input" contract at the string level too. A `\u`/`\U`
104
+ * whose value *is* well-formed but has no corresponding Unicode character
105
+ * is a different case — see {@link AnsiCDecodeResult}.
106
+ *
107
+ * @internal
108
+ */
109
+ export function decodeAnsiCString(raw) {
110
+ let text = '';
111
+ let i = 0;
112
+ while (i < raw.length) {
113
+ const ch = raw[i];
114
+ if (ch !== '\\' || i + 1 >= raw.length) {
115
+ // `i < raw.length` (the while guard) means indexing here is always
116
+ // in-bounds and `ch` is a real character, not `undefined`.
117
+ text += ch;
118
+ i += 1;
119
+ continue;
120
+ }
121
+ // `i + 1 < raw.length` was just confirmed above, so this index is safe.
122
+ const next = raw[i + 1];
123
+ switch (next) {
124
+ case 'a':
125
+ text += '\x07';
126
+ i += 2;
127
+ break;
128
+ case 'b':
129
+ text += '\x08';
130
+ i += 2;
131
+ break;
132
+ case 'e':
133
+ case 'E':
134
+ text += '\x1b';
135
+ i += 2;
136
+ break;
137
+ case 'f':
138
+ text += '\x0c';
139
+ i += 2;
140
+ break;
141
+ case 'n':
142
+ text += '\n';
143
+ i += 2;
144
+ break;
145
+ case 'r':
146
+ text += '\r';
147
+ i += 2;
148
+ break;
149
+ case 't':
150
+ text += '\t';
151
+ i += 2;
152
+ break;
153
+ case 'v':
154
+ text += '\x0b';
155
+ i += 2;
156
+ break;
157
+ case '\\':
158
+ text += '\\';
159
+ i += 2;
160
+ break;
161
+ case "'":
162
+ text += "'";
163
+ i += 2;
164
+ break;
165
+ case '"':
166
+ text += '"';
167
+ i += 2;
168
+ break;
169
+ case '?':
170
+ text += '?';
171
+ i += 2;
172
+ break;
173
+ case 'x': {
174
+ const { value, length } = readHexDigits(raw, i + 2, 2);
175
+ if (length > 0) {
176
+ text += String.fromCharCode(value);
177
+ i += 2 + length;
178
+ }
179
+ else {
180
+ text += ch;
181
+ i += 1;
182
+ }
183
+ break;
184
+ }
185
+ case 'u': {
186
+ const { value, length } = readHexDigits(raw, i + 2, 4);
187
+ if (length === 0) {
188
+ text += ch;
189
+ i += 1;
190
+ break;
191
+ }
192
+ // `\uHHHH` allows at most 4 hex digits (max 0xFFFF), which can
193
+ // never exceed MAX_UNICODE_CODE_POINT (0x10FFFF) — this check is
194
+ // purely defensive, mirroring the `\U` case below, and is
195
+ // unreachable today (see MAX_UNICODE_CODE_POINT's doc comment).
196
+ if (value > MAX_UNICODE_CODE_POINT) {
197
+ return { ok: false };
198
+ }
199
+ text += String.fromCodePoint(value);
200
+ i += 2 + length;
201
+ break;
202
+ }
203
+ case 'U': {
204
+ const { value, length } = readHexDigits(raw, i + 2, 8);
205
+ if (length === 0) {
206
+ // Not a recognized numeric escape (no valid hex digit at all):
207
+ // leave un-decoded, same as any other unrecognized escape.
208
+ text += ch;
209
+ i += 1;
210
+ break;
211
+ }
212
+ if (value > MAX_UNICODE_CODE_POINT) {
213
+ // Well-formed 8-digit hex value, but out of Unicode's range (see
214
+ // MAX_UNICODE_CODE_POINT's doc comment): bash's real output for
215
+ // this case can't be represented as an exact JS string, so this
216
+ // is not the same as an "unrecognized escape" — signal failure
217
+ // for the whole string rather than claiming a false literal.
218
+ return { ok: false };
219
+ }
220
+ text += String.fromCodePoint(value);
221
+ i += 2 + length;
222
+ break;
223
+ }
224
+ case 'c': {
225
+ const controlChar = raw.charAt(i + 2);
226
+ if (controlChar !== '') {
227
+ // Bash's control-character formula: the char XOR 0x40 (e.g. 'A'
228
+ // 0x41 ^ 0x40 = 0x01). See ANSI-C Quoting's "\cx" entry.
229
+ const code = (controlChar.toUpperCase().charCodeAt(0) ^ 0x40) & 0xff;
230
+ text += String.fromCharCode(code);
231
+ i += 3;
232
+ }
233
+ else {
234
+ text += ch;
235
+ i += 1;
236
+ }
237
+ break;
238
+ }
239
+ default:
240
+ if (isOctalDigit(next)) {
241
+ const { value, length } = readOctalDigits(raw, i + 1, 3);
242
+ text += String.fromCharCode(value);
243
+ i += 1 + length;
244
+ }
245
+ else {
246
+ // Not a recognized escape: leave the backslash (and whatever
247
+ // follows) untouched, one character at a time.
248
+ text += ch;
249
+ i += 1;
250
+ }
251
+ }
252
+ }
253
+ return { ok: true, text };
254
+ }
255
+ //# sourceMappingURL=ansi-c-escapes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ansi-c-escapes.js","sourceRoot":"","sources":["../../src/analyze/ansi-c-escapes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Decodes `Lit` word-part text and detects unquoted glob metacharacters.
3
+ * mvdan/sh's `Lit.Value` is raw source text for ordinary backslash escapes
4
+ * — a non-quoted backslash and the character it escapes both remain in
5
+ * `Lit.Value` exactly as written, undecoded (verified empirically; see
6
+ * `test/analyze-resolve-word.test.ts`) — so this module performs the two
7
+ * context-dependent decodings Bash itself defines for a `Lit` depending on
8
+ * whether it is a direct child of a `Word` ("unquoted") or nested inside a
9
+ * `DblQuoted` ("double-quoted"). The one exception is a backslash-newline
10
+ * *line continuation*: mvdan/sh's lexer consumes and discards it before
11
+ * `Lit.Value` is ever populated (verified empirically — parsing
12
+ * `cmd a\` + newline + `s` yields a `Lit` whose `value` is `"as"`, with no
13
+ * trace of the backslash or newline), so unlike ordinary escapes it never
14
+ * reaches this module and there is nothing here left to decode for it.
15
+ *
16
+ * @see https://www.gnu.org/software/bash/manual/bash.html#Escape-Character
17
+ * @see https://www.gnu.org/software/bash/manual/bash.html#Double-Quotes
18
+ * @see https://www.gnu.org/software/bash/manual/bash.html#Pattern-Matching
19
+ * @internal
20
+ */
21
+ /**
22
+ * The result of decoding a `Lit` part that is a direct (unquoted) child of
23
+ * a `Word`.
24
+ *
25
+ * @internal
26
+ */
27
+ export interface UnquotedLitResult {
28
+ /** The decoded literal text (escapes resolved, glob chars included verbatim). */
29
+ readonly text: string;
30
+ /** `true` iff an unescaped `*` or `?` appears in `raw`. */
31
+ readonly hasGlob: boolean;
32
+ /**
33
+ * Every unescaped `[` or `]` in `raw`, concatenated in source order (e.g.
34
+ * `raw`'s unescaped occurrences in `"a[b]c]"` yield `"[]]"`). A caller
35
+ * assembling a `Word`'s full bracket-expression classification
36
+ * concatenates this across all of a word's top-level `Lit` parts, in
37
+ * part order, then checks for a `[` with a later `]` — see
38
+ * `resolveWord`'s bracket scan and POSIX 2.13.1 / Bash §3.5.8.1.
39
+ */
40
+ readonly brackets: string;
41
+ }
42
+ /**
43
+ * Decodes a `Lit` part's raw value when it is an unquoted, direct child of
44
+ * a `Word` (not nested inside `DblQuoted`). Per Bash Reference Manual
45
+ * §3.1.2.1, "Escape Character": a non-quoted backslash preserves the
46
+ * literal value of the very next character. (A backslash-newline line
47
+ * continuation never reaches this function at all — see this module's
48
+ * doc comment — so there is no such case to handle here.) Escaping a glob
49
+ * metacharacter or bracket character suppresses its special meaning, so
50
+ * `hasGlob` and `brackets` only ever reflect *unescaped* occurrences.
51
+ *
52
+ * @internal
53
+ */
54
+ export declare function decodeUnquotedLit(raw: string): UnquotedLitResult;
55
+ /**
56
+ * Decodes a `Lit` part's raw value when nested inside a `DblQuoted`. Per
57
+ * Bash Reference Manual §3.1.2.3, "Double Quotes": inside double quotes, a
58
+ * backslash retains its special meaning only when followed by `$`, `` ` ``,
59
+ * `"`, `\`, or newline; any other backslash is preserved literally (both
60
+ * the backslash and the following character remain in the result). (As
61
+ * with {@link decodeUnquotedLit}, a backslash-newline line continuation
62
+ * never reaches this function — mvdan/sh's lexer strips it before
63
+ * `Lit.Value` is populated even inside double quotes, verified empirically:
64
+ * parsing `"a\` + newline + `b"` yields two sibling `Lit` nodes, `"a"` and
65
+ * `"b"`, with no `Lit` ever containing the backslash or newline — so there
66
+ * is no such case for this function to decode.) Quoting always suppresses
67
+ * pattern matching (Filename Expansion never applies to quoted text), so
68
+ * double-quoted `Lit` text never contributes to a word's glob
69
+ * classification — this function has no `hasGlob` output.
70
+ *
71
+ * @internal
72
+ */
73
+ export declare function decodeDblQuotedLit(raw: string): string;
74
+ //# sourceMappingURL=decode-lit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode-lit.d.ts","sourceRoot":"","sources":["../../src/analyze/decode-lit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAmBH;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,iFAAiF;IACjF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAuBhE;AAKD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAqBtD"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Decodes `Lit` word-part text and detects unquoted glob metacharacters.
3
+ * mvdan/sh's `Lit.Value` is raw source text for ordinary backslash escapes
4
+ * — a non-quoted backslash and the character it escapes both remain in
5
+ * `Lit.Value` exactly as written, undecoded (verified empirically; see
6
+ * `test/analyze-resolve-word.test.ts`) — so this module performs the two
7
+ * context-dependent decodings Bash itself defines for a `Lit` depending on
8
+ * whether it is a direct child of a `Word` ("unquoted") or nested inside a
9
+ * `DblQuoted` ("double-quoted"). The one exception is a backslash-newline
10
+ * *line continuation*: mvdan/sh's lexer consumes and discards it before
11
+ * `Lit.Value` is ever populated (verified empirically — parsing
12
+ * `cmd a\` + newline + `s` yields a `Lit` whose `value` is `"as"`, with no
13
+ * trace of the backslash or newline), so unlike ordinary escapes it never
14
+ * reaches this module and there is nothing here left to decode for it.
15
+ *
16
+ * @see https://www.gnu.org/software/bash/manual/bash.html#Escape-Character
17
+ * @see https://www.gnu.org/software/bash/manual/bash.html#Double-Quotes
18
+ * @see https://www.gnu.org/software/bash/manual/bash.html#Pattern-Matching
19
+ * @internal
20
+ */
21
+ /**
22
+ * Unquoted glob metacharacters this module detects directly, by a single
23
+ * unescaped occurrence: `*` (any string) and `?` (any single character) —
24
+ * Bash Reference Manual §3.5.8.1, "Pattern Matching". Bracket expressions
25
+ * (`[...]`) are handled separately by {@link decodeUnquotedLit}'s
26
+ * `brackets` output, since — per POSIX 2.13.1 and Bash §3.5.8.1 — a lone
27
+ * unquoted `[` is only glob-significant when a later unquoted `]` closes
28
+ * it (`a[bc` with no closing bracket is a *literal* filename in Bash,
29
+ * while `a[bc]` is a pattern), and that closing `]` may live in a
30
+ * different, concatenated `Lit` part of the same `Word` (see
31
+ * `resolveWord`'s cross-part bracket scan).
32
+ */
33
+ const GLOB_METACHARACTERS = new Set(['*', '?']);
34
+ /** The two characters {@link decodeUnquotedLit} tracks for cross-part bracket-expression detection. */
35
+ const BRACKET_CHARACTERS = new Set(['[', ']']);
36
+ /**
37
+ * Decodes a `Lit` part's raw value when it is an unquoted, direct child of
38
+ * a `Word` (not nested inside `DblQuoted`). Per Bash Reference Manual
39
+ * §3.1.2.1, "Escape Character": a non-quoted backslash preserves the
40
+ * literal value of the very next character. (A backslash-newline line
41
+ * continuation never reaches this function at all — see this module's
42
+ * doc comment — so there is no such case to handle here.) Escaping a glob
43
+ * metacharacter or bracket character suppresses its special meaning, so
44
+ * `hasGlob` and `brackets` only ever reflect *unescaped* occurrences.
45
+ *
46
+ * @internal
47
+ */
48
+ export function decodeUnquotedLit(raw) {
49
+ let text = '';
50
+ let hasGlob = false;
51
+ let brackets = '';
52
+ let i = 0;
53
+ while (i < raw.length) {
54
+ const ch = raw[i];
55
+ if (ch === '\\' && i + 1 < raw.length) {
56
+ const next = raw[i + 1];
57
+ text += next;
58
+ i += 2;
59
+ continue;
60
+ }
61
+ if (GLOB_METACHARACTERS.has(ch)) {
62
+ hasGlob = true;
63
+ }
64
+ if (BRACKET_CHARACTERS.has(ch)) {
65
+ brackets += ch;
66
+ }
67
+ text += ch;
68
+ i += 1;
69
+ }
70
+ return { text, hasGlob, brackets };
71
+ }
72
+ /** Characters after which a backslash retains special meaning inside double quotes. */
73
+ const DBLQUOTED_ESCAPABLE = new Set(['$', '`', '"', '\\']);
74
+ /**
75
+ * Decodes a `Lit` part's raw value when nested inside a `DblQuoted`. Per
76
+ * Bash Reference Manual §3.1.2.3, "Double Quotes": inside double quotes, a
77
+ * backslash retains its special meaning only when followed by `$`, `` ` ``,
78
+ * `"`, `\`, or newline; any other backslash is preserved literally (both
79
+ * the backslash and the following character remain in the result). (As
80
+ * with {@link decodeUnquotedLit}, a backslash-newline line continuation
81
+ * never reaches this function — mvdan/sh's lexer strips it before
82
+ * `Lit.Value` is populated even inside double quotes, verified empirically:
83
+ * parsing `"a\` + newline + `b"` yields two sibling `Lit` nodes, `"a"` and
84
+ * `"b"`, with no `Lit` ever containing the backslash or newline — so there
85
+ * is no such case for this function to decode.) Quoting always suppresses
86
+ * pattern matching (Filename Expansion never applies to quoted text), so
87
+ * double-quoted `Lit` text never contributes to a word's glob
88
+ * classification — this function has no `hasGlob` output.
89
+ *
90
+ * @internal
91
+ */
92
+ export function decodeDblQuotedLit(raw) {
93
+ let text = '';
94
+ let i = 0;
95
+ while (i < raw.length) {
96
+ const ch = raw[i];
97
+ if (ch === '\\' && i + 1 < raw.length) {
98
+ const next = raw[i + 1];
99
+ if (DBLQUOTED_ESCAPABLE.has(next)) {
100
+ text += next;
101
+ i += 2;
102
+ continue;
103
+ }
104
+ // Not one of the escapable characters: the backslash stays literal.
105
+ text += ch;
106
+ i += 1;
107
+ continue;
108
+ }
109
+ text += ch;
110
+ i += 1;
111
+ }
112
+ return text;
113
+ }
114
+ //# sourceMappingURL=decode-lit.js.map