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.
- package/README.md +165 -133
- package/dist/analyze/ansi-c-escapes.d.ts +1 -1
- package/dist/analyze/ansi-c-escapes.js +1 -1
- package/dist/analyze/enumerate-commands.d.ts +11 -16
- package/dist/analyze/enumerate-commands.d.ts.map +1 -1
- package/dist/analyze/enumerate-commands.js +82 -92
- package/dist/analyze/enumerate-commands.js.map +1 -1
- package/dist/analyze/index.d.ts +3 -2
- package/dist/analyze/index.d.ts.map +1 -1
- package/dist/analyze/index.js +1 -1
- package/dist/analyze/node-helpers.d.ts +10 -21
- package/dist/analyze/node-helpers.d.ts.map +1 -1
- package/dist/analyze/node-helpers.js +16 -27
- package/dist/analyze/node-helpers.js.map +1 -1
- package/dist/analyze/resolve-argv0.d.ts +6 -6
- package/dist/analyze/resolve-argv0.js +6 -6
- package/dist/analyze/resolve-word.d.ts +7 -10
- package/dist/analyze/resolve-word.d.ts.map +1 -1
- package/dist/analyze/resolve-word.js +39 -22
- package/dist/analyze/resolve-word.js.map +1 -1
- package/dist/analyze.d.ts +3780 -397
- package/dist/errors.d.ts +6 -79
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +5 -64
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +16 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -4
- package/dist/index.js.map +1 -1
- package/dist/sh-ast.d.ts +3503 -1027
- package/dist/syntax-errors.d.ts +56 -0
- package/dist/syntax-errors.d.ts.map +1 -0
- package/dist/syntax-errors.js +80 -0
- package/dist/syntax-errors.js.map +1 -0
- package/dist/syntax-snapshot.d.ts +47 -0
- package/dist/syntax-snapshot.d.ts.map +1 -0
- package/dist/syntax-snapshot.js +121 -0
- package/dist/syntax-snapshot.js.map +1 -0
- package/dist/syntax-wire.d.ts +31 -0
- package/dist/syntax-wire.d.ts.map +1 -0
- package/dist/syntax-wire.js +202 -0
- package/dist/syntax-wire.js.map +1 -0
- package/dist/syntax.d.ts +65 -0
- package/dist/syntax.d.ts.map +1 -0
- package/dist/syntax.js +152 -0
- package/dist/syntax.js.map +1 -0
- package/dist/wasm-instance.d.ts +5 -8
- package/dist/wasm-instance.d.ts.map +1 -1
- package/dist/wasm-instance.js +44 -47
- package/dist/wasm-instance.js.map +1 -1
- package/generated/syntax-constants.d.ts +1652 -0
- package/generated/syntax-constants.js +1384 -0
- package/generated/syntax-schema.d.ts +60 -0
- package/generated/syntax-schema.js +1740 -0
- package/generated/syntax-types.d.ts +1977 -0
- package/package.json +2 -2
- package/shim/sh-ast.wasm +0 -0
- package/dist/normalize.d.ts +0 -52
- package/dist/normalize.d.ts.map +0 -1
- package/dist/normalize.js +0 -260
- package/dist/normalize.js.map +0 -1
- package/dist/parse.d.ts +0 -50
- package/dist/parse.d.ts.map +0 -1
- package/dist/parse.js +0 -125
- package/dist/parse.js.map +0 -1
- package/dist/types.d.ts +0 -74
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/visitor-keys.d.ts +0 -21
- package/dist/visitor-keys.d.ts.map +0 -1
- package/dist/visitor-keys.js +0 -23
- package/dist/visitor-keys.js.map +0 -1
- package/dist/walk.d.ts +0 -18
- package/dist/walk.d.ts.map +0 -1
- package/dist/walk.js +0 -40
- package/dist/walk.js.map +0 -1
- package/generated/child-type-schema.d.ts +0 -14
- package/generated/child-type-schema.js +0 -188
- package/generated/node-types.d.ts +0 -1002
- package/generated/position-fields.d.ts +0 -21
- package/generated/position-fields.js +0 -50
- package/generated/visitor-keys.d.ts +0 -11
- package/generated/visitor-keys.js +0 -50
package/README.md
CHANGED
|
@@ -1,147 +1,179 @@
|
|
|
1
1
|
# sh-ast
|
|
2
2
|
|
|
3
|
-
A synchronous
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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.
|
|
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
|
-
##
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
pnpm run generate:sh-ast-schema
|
|
55
|
-
```
|
|
154
|
+
## Generated model
|
|
56
155
|
|
|
57
|
-
|
|
58
|
-
`
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
`
|
|
67
|
-
`shim/internal/
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 `
|
|
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 `
|
|
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 {
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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 `
|
|
148
|
-
* subtree (a `Stmt`, a `Command`, or even a bare `Word`) is handled.
|
|
149
|
-
*
|
|
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:
|
|
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":"
|
|
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"}
|