sh-ast 0.3.0 → 0.4.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 (84) hide show
  1. package/README.md +165 -133
  2. package/dist/analyze/ansi-c-escapes.d.ts +1 -1
  3. package/dist/analyze/ansi-c-escapes.js +1 -1
  4. package/dist/analyze/enumerate-commands.d.ts +11 -16
  5. package/dist/analyze/enumerate-commands.d.ts.map +1 -1
  6. package/dist/analyze/enumerate-commands.js +82 -92
  7. package/dist/analyze/enumerate-commands.js.map +1 -1
  8. package/dist/analyze/index.d.ts +3 -2
  9. package/dist/analyze/index.d.ts.map +1 -1
  10. package/dist/analyze/index.js +1 -1
  11. package/dist/analyze/node-helpers.d.ts +10 -21
  12. package/dist/analyze/node-helpers.d.ts.map +1 -1
  13. package/dist/analyze/node-helpers.js +16 -27
  14. package/dist/analyze/node-helpers.js.map +1 -1
  15. package/dist/analyze/resolve-argv0.d.ts +6 -6
  16. package/dist/analyze/resolve-argv0.js +6 -6
  17. package/dist/analyze/resolve-word.d.ts +7 -10
  18. package/dist/analyze/resolve-word.d.ts.map +1 -1
  19. package/dist/analyze/resolve-word.js +39 -22
  20. package/dist/analyze/resolve-word.js.map +1 -1
  21. package/dist/analyze.d.ts +3780 -397
  22. package/dist/errors.d.ts +6 -79
  23. package/dist/errors.d.ts.map +1 -1
  24. package/dist/errors.js +5 -64
  25. package/dist/errors.js.map +1 -1
  26. package/dist/index.d.ts +16 -14
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +23 -4
  29. package/dist/index.js.map +1 -1
  30. package/dist/sh-ast.d.ts +3503 -1027
  31. package/dist/syntax-errors.d.ts +56 -0
  32. package/dist/syntax-errors.d.ts.map +1 -0
  33. package/dist/syntax-errors.js +80 -0
  34. package/dist/syntax-errors.js.map +1 -0
  35. package/dist/syntax-snapshot.d.ts +47 -0
  36. package/dist/syntax-snapshot.d.ts.map +1 -0
  37. package/dist/syntax-snapshot.js +121 -0
  38. package/dist/syntax-snapshot.js.map +1 -0
  39. package/dist/syntax-wire.d.ts +31 -0
  40. package/dist/syntax-wire.d.ts.map +1 -0
  41. package/dist/syntax-wire.js +202 -0
  42. package/dist/syntax-wire.js.map +1 -0
  43. package/dist/syntax.d.ts +65 -0
  44. package/dist/syntax.d.ts.map +1 -0
  45. package/dist/syntax.js +152 -0
  46. package/dist/syntax.js.map +1 -0
  47. package/dist/wasm-instance.d.ts +5 -8
  48. package/dist/wasm-instance.d.ts.map +1 -1
  49. package/dist/wasm-instance.js +44 -47
  50. package/dist/wasm-instance.js.map +1 -1
  51. package/generated/syntax-constants.d.ts +1652 -0
  52. package/generated/syntax-constants.js +1384 -0
  53. package/generated/syntax-schema.d.ts +60 -0
  54. package/generated/syntax-schema.js +1740 -0
  55. package/generated/syntax-types.d.ts +1977 -0
  56. package/package.json +2 -2
  57. package/shim/sh-ast.wasm +0 -0
  58. package/dist/normalize.d.ts +0 -52
  59. package/dist/normalize.d.ts.map +0 -1
  60. package/dist/normalize.js +0 -260
  61. package/dist/normalize.js.map +0 -1
  62. package/dist/parse.d.ts +0 -50
  63. package/dist/parse.d.ts.map +0 -1
  64. package/dist/parse.js +0 -125
  65. package/dist/parse.js.map +0 -1
  66. package/dist/types.d.ts +0 -74
  67. package/dist/types.d.ts.map +0 -1
  68. package/dist/types.js +0 -2
  69. package/dist/types.js.map +0 -1
  70. package/dist/visitor-keys.d.ts +0 -21
  71. package/dist/visitor-keys.d.ts.map +0 -1
  72. package/dist/visitor-keys.js +0 -23
  73. package/dist/visitor-keys.js.map +0 -1
  74. package/dist/walk.d.ts +0 -18
  75. package/dist/walk.d.ts.map +0 -1
  76. package/dist/walk.js +0 -40
  77. package/dist/walk.js.map +0 -1
  78. package/generated/child-type-schema.d.ts +0 -14
  79. package/generated/child-type-schema.js +0 -188
  80. package/generated/node-types.d.ts +0 -1002
  81. package/generated/position-fields.d.ts +0 -21
  82. package/generated/position-fields.js +0 -50
  83. package/generated/visitor-keys.d.ts +0 -11
  84. package/generated/visitor-keys.js +0 -50
package/README.md CHANGED
@@ -1,147 +1,179 @@
1
1
  # sh-ast
2
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).
3
+ A synchronous TypeScript interface to the `syntax` package of
4
+ [mvdan/sh](https://github.com/mvdan/sh), pinned to **v3.13.1**. Parsing returns
5
+ mutable records preserving upstream fields, defaults, nulls, numeric operators,
6
+ byte strings, and token positions. First use initializes WebAssembly synchronously
7
+ in Node.js; no initialization promise is required.
9
8
 
10
9
  ```ts
11
- import { parseSync } from 'sh-ast';
12
-
13
- const file = parseSync('echo hi');
14
- file.stmts[0].cmd.type; // "CallExpr"
10
+ import { LangBash, nodeEnd, parseSync } from 'sh-ast';
11
+
12
+ const file = parseSync('echo hi', { Variant: LangBash, KeepComments: true });
13
+ const command = file.Stmts?.[0]?.Cmd;
14
+ if (command?.Type === 'CallExpr') {
15
+ console.log(command.Args?.[0]?.Parts); // upstream WordPart alternatives
16
+ console.log(nodeEnd(command).Offset); // exclusive byte offset
17
+ }
15
18
  ```
16
19
 
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
20
+ ## Public contract
21
+
22
+ - `parseSync(source, options?)` accepts a well-formed Unicode string or exact
23
+ `Uint8Array` bytes. Invalid UTF-8 reaches upstream and produces its parse error;
24
+ lone JavaScript surrogates are rejected before encoding.
25
+ - `ParseOptions` contains `Filename`, `KeepComments`, `Variant`, `StopAt`, and
26
+ `RecoverErrors`. Defaults match upstream, including `KeepComments: false`.
27
+ `Variant` uses exported constants such as `LangBash` and `LangPOSIX`.
28
+ - `File`, `Node`, and concrete records retain upstream spelling and a `Type`
29
+ discriminator. Every field is present; nil slices/pointers use `null`, while an
30
+ allocated empty slice is `[]`. `Slice`, `Replace`, and `Expansion` are auxiliary
31
+ records, not Nodes. Interface unions also admit `TypedNil<Kind>` with
32
+ `{ Type: 'TypedNil', Kind: 'Word' }` spelling: this preserves a Go interface
33
+ holding a nil pointer of a concrete type, distinct from an ordinary `null`
34
+ interface. A tag is allowed only where that concrete type implements the
35
+ declared interface; concrete pointer fields continue to use `null`.
36
+ - `GoString` is `string | Uint8Array`: valid UTF-8 is text; other Go string bytes
37
+ remain bytes. Consumers must explicitly handle both representations.
38
+ - `SyntaxPos` preserves `Offset`, `Line`, `Col`, `IsValid`, `IsRecovered`, and
39
+ `OffsetOverflow`. Offsets and columns count bytes; lines/columns are one-based.
40
+ Invalid, recovered, and overflow states remain distinct. No UTF-16 projection is
41
+ performed by this package.
42
+ - `nodePos(node)` and `nodeEnd(node)` independently compute upstream bounds from
43
+ the current fields, including after edits. They do not return cached parse-time
44
+ bounds. Incomplete constructed nodes can support one bound but fail the other.
45
+ - `parseWithPositionsSync(source, options?)` parses once and returns `{ File,
46
+ Positions }`. `Positions.get(node)` reads immutable original bounds by node
47
+ identity without additional WASM calls. Editing `File` leaves these observations
48
+ unchanged; copied or foreign nodes have no entry. Use this separate snapshot for
49
+ source-location adapters, and live `nodePos`/`nodeEnd` for edited syntax. A
50
+ captured upstream bounds failure is thrown when its pair is requested; it does
51
+ not discard a successful parse. Parser failures retain the same structured
52
+ error and partial File as `parseSync`.
53
+ - `newPos(offset, line, column)` preserves upstream clamping/overflow semantics.
54
+ Its uint64 inputs and the parser's Go int options accept safe integers or exact
55
+ `bigint`; stored position components and operator values are numbers.
56
+ - Operator families have named numeric constants and a distinct raw uint32 path,
57
+ `rawOperator('RedirOperator', value)`, for unnamed upstream values. Representing
58
+ a value does not promise every upstream operation accepts it.
59
+ - `SYNTAX_SCHEMA` and `SYNTAX_INTERFACES` are deeply frozen field/interface
60
+ metadata. Their order is not upstream Walk order.
61
+
62
+ `ParseError` and `LangError` preserve upstream fields, exact `Message` bytes, and
63
+ any partial `File`. Their ordinary `Error.message` is a display string.
64
+ `InputError` reports invalid binding inputs, upstream option constraints, or
65
+ unsupported constructed nodes whose requested bound panics upstream.
66
+ `ShInternalError` reports transport failures or unexpected upstream panics.
67
+ All extend `ShAstError`. The conservative structural-depth guard throws
68
+ `ShParseMaxDepthError` before potentially exhausting the WASM stack. It is a
69
+ binding resource constraint and can reject deeply nested quoted or commented text.
70
+
71
+ `sh-ast/analyze` remains a separate syntactic interpretation layer. Its word,
72
+ command, and wrapper analysis consumes the faithful records; it does not define
73
+ parser behavior or the core tree representation.
74
+
75
+ ## Migration from 0.3
76
+
77
+ Fields now use upstream spelling (`Type`, `Stmts`, `Cmd`, `Value`) and explicit
78
+ null/default values. Replace dialect strings with numeric `Variant` constants,
79
+ request comments explicitly, and handle structured errors and byte positions.
80
+ The root no longer exports normalized `ShNode`/`ShNodes`, `range`/`loc`, `walk`, or
81
+ `visitorKeys`. Consumers such as ESLint own their source-coordinate and traversal
82
+ views. Whole-file parsing is the present scope; public upstream traversal and
83
+ printing belong to the following minor release. Stateful/incremental parser
84
+ protocols and full syntax-package parity are deferred.
85
+
86
+ The [design collection](../../design/ARCHITECTURE.md) and
87
+ [implementation evidence](../../design/IMPLEMENTATION.md) describe the release
88
+ boundaries, concrete choices, and validation status. This package has no ESLint
89
+ runtime dependency. It originated in
90
+ [`eslint-sh`](https://github.com/mike-north/eslint-sh) at commit `bdd55b3`.
91
+
92
+ ## Dialect enforcement
93
+
94
+ `Variant` accepts mvdan/sh's `LangVariant` (`LangBash`, `LangPOSIX`, `LangMirBSDKorn`,
95
+ `LangBats`, `LangZsh`). mvdan/sh gates most dialect-specific syntax with a hard parse error
96
+ (`checkLang` in its `syntax/parser.go`) when the requested dialect isn't in that construct's
97
+ allowed set — but two dialect-specific keywords (`[[` test clauses and `let`) have **no such gate**:
98
+ outside the dialects that recognize them, they silently fall through to ordinary word/command
99
+ parsing instead of erroring. The table below is derived by hand from that source, not from
100
+ guessing at current behavior, and is pinned by `test/dialect-matrix.test.ts`.
101
+
102
+ | Construct | bash | posix | mksh | bats | zsh |
103
+ | -------------------------- | :--------: | :--------------------: | :--------: | :--------: | :----------------------------: |
104
+ | Test clause `[[ ]]` | parses | **parses** (see below) | parses | parses | parses |
105
+ | Regex test `[[ a =~ b ]]` | parses | parses\* | **error** | parses | parses |
106
+ | Array literal `a=(1 2 3)` | parses | **error** | parses | parses | parses |
107
+ | Process substitution `<()` | parses | **error** | **error** | parses | parses |
108
+ | Brace expansion `{a,b}` | parses\*\* | parses\*\* | parses\*\* | parses\*\* | parses\*\* |
109
+ | `function` keyword | parses | **error** | parses | parses | parses |
110
+ | C-style `for (( ; ; ))` | parses | **error** | **error** | parses | parses |
111
+ | Extended glob `@(a\|b)` | parses | **error** | parses | parses | parses\*\*\* (no ExtGlob node) |
112
+ | Herestring `<<<` | parses | **error** | parses | parses | parses |
113
+ | `let` clause | parses | **parses** (see below) | parses | parses | parses |
114
+
115
+ \* Under posix, `[[` is already an ordinary command word (see below), so `[[ a =~ b ]]` never
116
+ reaches the regex-test parser at all — `=~` is just another literal argument to a command named
117
+ `[[`.
118
+
119
+ \*\* Brace expansion is never split into a `BraceExp` node by this bridge, in any dialect — mvdan/sh
120
+ only produces `BraceExp` nodes when the caller explicitly runs its `syntax.SplitBraces()`
121
+ post-processing pass, which `shim/main.go` does not invoke. `{a,b}` always parses as an ordinary
122
+ `Lit` word part, identically in every dialect.
123
+
124
+ \*\*\* zsh's lexer never tokenizes `@(` as an extended-glob opener at all — deliberately, to avoid
125
+ ambiguity with zsh's own glob qualifiers like `*(N)`. So `ls @(a|b)` parses without error under
126
+ `Variant: LangZsh`, but produces plain `Lit` word parts rather than an `ExtGlob` node; the
127
+ `checkLang` gate that rejects extended globs under posix is never reached for zsh.
128
+
129
+ ### `[[ ... ]]` and `let` under posix: accepted by design
130
+
131
+ `Variant: LangPOSIX` accepting `[[ a == b ]]` and `let x=1` is **intentional upstream behavior, not
132
+ a bridge bug**. POSIX sh has no `[[` test clause or `let` builtin as syntax — so mvdan/sh's parser
133
+ doesn't special-case those keywords at all outside bash-like/mksh/zsh; it falls through to
134
+ ordinary word/command parsing, exactly as a real POSIX-mode shell would tokenize `[[` as a command
135
+ name. By contrast, dialect-specific constructs that cannot be read as ordinary commands—arrays,
136
+ C-style `for`, `<()`, `<<<`, extended globs, and the `function` form—are rejected in POSIX mode,
137
+ either by an explicit `checkLang` gate or, for process substitution, after lexer fallback. A regex
138
+ operator inside `[[ a =~ b ]]` remains ordinary command text because POSIX mode never enters the
139
+ test-clause parser.
140
+
141
+ Concretely, under `Variant: LangPOSIX`:
33
142
 
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`.
143
+ ```ts
144
+ import { LangPOSIX, parseSync } from 'sh-ast';
47
145
 
48
- ## Regenerating the schema tables
146
+ const file = parseSync('[[ a == b ]]', { Variant: LangPOSIX });
147
+ file.Stmts?.[0]?.Cmd?.Type; // "CallExpr" — a command literally named "[[", not "TestClause"
148
+ ```
49
149
 
50
- After bumping the pinned mvdan/sh version (`shim/go.mod` _and_ `../../tools/gen-visitor-keys/go.mod`
51
- must move together):
150
+ Evidence: mvdan/sh's own test suite (`syntax/filetests_test.go`) asserts this exact shape for
151
+ `LangPOSIX`, and the syntax bridge passes the requested numeric variant to mvdan/sh's
152
+ `Variant` control — the bridge does not special-case or mis-map `[[`.
52
153
 
53
- ```sh
54
- pnpm run generate:sh-ast-schema
55
- ```
154
+ ## Generated model
56
155
 
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.
156
+ The generator reads the pinned upstream struct definitions. Run
157
+ `pnpm run generate:sh-ast-schema` after changing the matching pins in
158
+ `shim/go.mod` and `../../tools/gen-visitor-keys/go.mod`. Faithful declarations,
159
+ constants, and complete field metadata live in `generated/syntax-*`.
160
+ Independent Go reflection tests, manually derived type assertions, and runtime
161
+ fixtures check fidelity; regeneration alone checks only drift.
63
162
 
64
163
  ## The WASM shim
65
164
 
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`.
165
+ The `syntaxProcess` linear-memory ABI transports exact byte payloads and a
166
+ reversible record encoding. `shim/internal/syntaxwire` preserves all exported
167
+ fields, nil/default values, arbitrary strings, and observable position behavior.
168
+ `shim/internal/syntaxbridge` invokes upstream parsing and position operations.
169
+ Private Go printing tests verify that reconstructed edited trees retain the
170
+ information needed for the future public printer.
171
+
172
+ The canonical binary is built by Linux/amd64 CI with the Go toolchain pinned in
173
+ `shim/go.mod` and the checksum-pinned Binaryen version in
174
+ `.github/workflows/wasm-reproducibility.yml`. Go build flags are
175
+ `-buildvcs=false -trimpath -ldflags="-s -w -buildid="`; optimization uses
176
+ `wasm-opt -Oz --enable-bulk-memory-opt --enable-nontrapping-float-to-int`.
177
+ Use local builds for experiments. Commit the CI-produced artifact, then require
178
+ CI to compare a fresh rebuild byte-for-byte with the committed binary. Historical
179
+ normalized-parser benchmarks do not describe this interface's performance.
@@ -2,7 +2,7 @@
2
2
  * Decodes `$'...'` (Bash ANSI-C Quoting) escape sequences.
3
3
  *
4
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,
5
+ * word part with `Dollar: true`, the upstream `Value` field is the raw,
6
6
  * un-decoded source text between the quotes (verified empirically against
7
7
  * mvdan.cc/sh/v3 v3.13.1 — see `test/analyze-resolve-word.test.ts`'s module
8
8
  * comment for how this was confirmed). This module fills that gap using the
@@ -2,7 +2,7 @@
2
2
  * Decodes `$'...'` (Bash ANSI-C Quoting) escape sequences.
3
3
  *
4
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,
5
+ * word part with `Dollar: true`, the upstream `Value` field is the raw,
6
6
  * un-decoded source text between the quotes (verified empirically against
7
7
  * mvdan.cc/sh/v3 v3.13.1 — see `test/analyze-resolve-word.test.ts`'s module
8
8
  * comment for how this was confirmed). This module fills that gap using the
@@ -1,4 +1,4 @@
1
- import type { ShNode } from '../types.js';
1
+ import type { CallExpr, GoString, Node } from '../../generated/syntax-types.js';
2
2
  import type { WordResolution } from './resolve-word.js';
3
3
  /**
4
4
  * A single frame of the path from the root of the tree down to a
@@ -19,10 +19,10 @@ import type { WordResolution } from './resolve-word.js';
19
19
  * `ProcSubst` (`<(...)`/`>(...)`) reached from *any* word-bearing
20
20
  * position (an argument, a redirection target, a case subject, a loop's
21
21
  * word list, an assignment value, a test/arithmetic operand, …) — not
22
- * only from `CallExpr.args`.
22
+ * only from `CallExpr.Args`.
23
23
  * - `'if'` (`branch: 'cond' | 'then' | 'else'`) — inside an `IfClause`'s
24
24
  * condition, then-branch, or else-branch (mvdan/sh nests `elif` chains as
25
- * `IfClause.else` pointing to another `IfClause`, so an `elif`'s own
25
+ * `IfClause.Else` pointing to another `IfClause`, so an `elif`'s own
26
26
  * condition/then are reached through an `{kind:'if',branch:'else'}` frame
27
27
  * first, then their own `'cond'`/`'then'` frame — reflecting the real
28
28
  * nesting rather than collapsing it).
@@ -80,7 +80,7 @@ export type CommandContext = {
80
80
  readonly role: 'body' | 'cond';
81
81
  } | {
82
82
  readonly kind: 'function';
83
- readonly name: string;
83
+ readonly name: GoString;
84
84
  } | {
85
85
  readonly kind: 'background';
86
86
  } | {
@@ -100,7 +100,7 @@ export type CommandContext = {
100
100
  */
101
101
  export interface CommandSite {
102
102
  /** The `CallExpr` node this site was found at. */
103
- readonly node: ShNode;
103
+ readonly node: CallExpr;
104
104
  /**
105
105
  * `resolveWord` applied to the first word (`argv[0]`), with
106
106
  * `{ context: 'command-argument' }` — every `CallExpr` word is an
@@ -125,14 +125,14 @@ export interface CommandSite {
125
125
  * `CmdSubst`/`ProcSubst` nested inside words, wherever those words occur
126
126
  * (arguments, redirection targets, case subjects/patterns, loop word
127
127
  * lists, assignment values, test/arithmetic operands, …), not only inside
128
- * `CallExpr.args`.
128
+ * `CallExpr.Args`.
129
129
  *
130
130
  * An assignment-only `CallExpr` (`FOO=bar`, `args` empty) has no first word
131
131
  * to resolve and is not itself a command invocation — no program runs — so
132
132
  * it produces no {@link CommandSite}; any command substitution nested in
133
133
  * its assigned value (`FOO=$(sub)`) is still found and reported.
134
134
  *
135
- * Results are sorted by source position (`node.range[0]`), so nested
135
+ * Results are sorted by source position (`nodePos(node).Offset`), so nested
136
136
  * command substitutions and out-of-structural-order redirection targets
137
137
  * still come back in source order regardless of traversal order.
138
138
  *
@@ -144,14 +144,9 @@ export interface CommandSite {
144
144
  * traversed iteratively and never risks a stack overflow or trips the
145
145
  * nesting-depth guard below, regardless of how many stages/links it has.
146
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. Note
149
- * `parseSync` itself already refuses (via its own `ShParseMaxDepthError`) any
150
- * input whose estimated nesting exceeds *its* limit before ever producing a
151
- * tree — see `parse-depth-guard.ts` — so a `root` sourced from `parseSync`
152
- * has already passed that earlier, lower-level line of defense; this
153
- * module's own guard below remains a second, independent backstop for
154
- * trees built or mutated some other way.
147
+ * @param root - Any `Node` — typically a `File` from `parseSync`, but any
148
+ * subtree (a `Stmt`, a `Command`, or even a bare `Word`) is handled.
149
+ * The depth guard also applies to caller-built or mutated trees.
155
150
  * @throws {@link ShAnalyzeMaxDepthError} if `root`'s genuinely nested
156
151
  * structure (subshells within subshells, chained command/process
157
152
  * substitutions, deeply nested `if`/`case`/loop/function/`time`/`{ }`
@@ -161,5 +156,5 @@ export interface CommandSite {
161
156
  * it as `deny`.
162
157
  * @public
163
158
  */
164
- export declare function enumerateCommands(root: ShNode): CommandSite[];
159
+ export declare function enumerateCommands(root: Node): CommandSite[];
165
160
  //# sourceMappingURL=enumerate-commands.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"enumerate-commands.d.ts","sourceRoot":"","sources":["../../src/analyze/enumerate-commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAChD;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC/C;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAC7B;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAC7B;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;CAAE,GAClE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GACzD;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEhC;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAW;IAC1B,kDAAkD;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,oIAAoI;IACpI,QAAQ,CAAC,IAAI,EAAE,SAAS,cAAc,EAAE,CAAC;IACzC,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;CAC7C;AA2aD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAU7D"}
1
+ {"version":3,"file":"enumerate-commands.d.ts","sourceRoot":"","sources":["../../src/analyze/enumerate-commands.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,QAAQ,EACR,QAAQ,EAER,IAAI,EAEL,MAAM,iCAAiC,CAAC;AAGzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAChD;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC/C;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAC7B;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAC7B;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;CAAE,GAClE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GACzD;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GACtD;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEhC;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAW;IAC1B,kDAAkD;IAClD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,oIAAoI;IACpI,QAAQ,CAAC,IAAI,EAAE,SAAS,cAAc,EAAE,CAAC;IACzC,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;CAC7C;AAiaD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,EAAE,CAa3D"}