yuku-parser 0.5.17 → 0.5.18

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 (4) hide show
  1. package/README.md +39 -43
  2. package/decode.js +41 -16
  3. package/index.d.ts +38 -11
  4. package/package.json +12 -12
package/README.md CHANGED
@@ -74,22 +74,20 @@ const { line, column } = result.locOf(result.program.body[0].start);
74
74
 
75
75
  Lines are 1-based and columns are 0-based, matching ESTree's `loc` convention. The lookup is an O(log n) binary search: tens of nanoseconds per call, safe to invoke per node during a walk.
76
76
 
77
- For bulk work — building your own line/column index, integrating with another source-map or diagnostics library, or skipping the per-call binary search — read `result.lineStarts` directly. It's a sorted array of UTF-16 offsets where each line begins: `lineStarts[i]` is the start of line `i + 1`. Decoded lazily on first access and cached.
78
-
79
77
  ## Walking the AST
80
78
 
81
- The AST is standard ESTree, so any ESTree-compatible walker works. For example, with [zimmerframe](https://github.com/sveltejs/zimmerframe):
79
+ [`yuku-ast`](https://www.npmjs.com/package/yuku-ast) is the companion toolkit built for this AST: a typed walker, node builders (`b`), type guards (`is`), and identifier validators.
82
80
 
83
81
  ```ts
84
- import { parse, type Node } from "yuku-parser";
85
- import { walk } from "zimmerframe";
82
+ import { parse } from "yuku-parser";
83
+ import { walk } from "yuku-ast";
86
84
 
87
85
  const { program } = parse(`
88
86
  const message = "hello";
89
87
  console.log(message);
90
88
  `);
91
89
 
92
- walk(program as Node, null, {
90
+ walk(program, {
93
91
  Identifier(node) {
94
92
  console.log(node.name);
95
93
  },
@@ -99,6 +97,8 @@ walk(program as Node, null, {
99
97
  });
100
98
  ```
101
99
 
100
+ The AST is also standard ESTree, so any ESTree-compatible walker (e.g. [zimmerframe](https://github.com/sveltejs/zimmerframe)) works just as well.
101
+
102
102
  ## Options
103
103
 
104
104
  All options are optional.
@@ -121,7 +121,7 @@ const result = parse(source, {
121
121
  | `preserveParens` | `true`, `false` | `true` | Keep `ParenthesizedExpression` nodes in the AST. When false, parentheses are stripped and only the inner expression is kept. |
122
122
  | `allowReturnOutsideFunction` | `true`, `false` | `false` | Allow `return` statements outside of functions, at the top level. |
123
123
  | `semanticErrors` | `true`, `false` | `false` | Run semantic analysis and report semantic errors alongside syntax errors. |
124
- | `attachComments` | `true`, `false` | `false` | Collect comments and attach them to host AST nodes. See [Comments](#comments). |
124
+ | `attachComments` | `true`, `false` | `false` | Also attach each comment to its host AST node. The flat `result.comments` list is always present. See [Comments](#comments). |
125
125
 
126
126
  ## Result
127
127
 
@@ -130,6 +130,7 @@ const result = parse(source, {
130
130
  ```ts
131
131
  interface ParseResult {
132
132
  program: Program;
133
+ comments: Comment[]; // every comment in source order
133
134
  diagnostics: Diagnostic[];
134
135
  lineStarts: number[];
135
136
  locOf(offset: number): { line: number; column: number };
@@ -163,63 +164,58 @@ This incurs a very small performance overhead. If your build pipeline already ha
163
164
 
164
165
  ## Comments
165
166
 
166
- Comments are attached to the AST node they sit next to. Enable collection with `attachComments`, then read them off any node:
167
+ Every comment is always in `result.comments`, a flat list in source order with each comment's source span:
167
168
 
168
169
  ```js
169
- const { program } = parse(
170
- `// header\nfunction foo() {} // trailing`,
171
- { attachComments: true },
172
- );
170
+ const { comments } = parse(`// a line comment\nconst x = 1; /* a block comment */`);
173
171
 
174
- const fn = program.body[0];
175
- for (const c of fn.comments ?? []) {
176
- console.log(c.position, c.type, c.value);
172
+ for (const c of comments) {
173
+ console.log(c.type, JSON.stringify(c.value), c.start, c.end);
177
174
  }
178
- // before Line " header"
179
- // after Line " trailing"
175
+ // Line " a line comment" 0 17
176
+ // Block " a block comment " 31 52
180
177
  ```
181
178
 
182
- Each comment is:
179
+ Each entry is:
183
180
 
184
181
  ```ts
185
182
  interface Comment {
186
183
  type: "Line" | "Block";
187
- position: "before" | "after" | "inside";
188
- sameLine: boolean;
189
184
  value: string; // body without delimiters
185
+ start: number; // byte offset, delimiter included
186
+ end: number; // byte offset, delimiter included
190
187
  }
191
188
  ```
192
189
 
193
- `position` tells you where the comment sits relative to its host:
194
-
195
- - `"before"`: leading the host node.
196
- - `"after"`: trailing the host node.
197
- - `"inside"`: interior to an otherwise empty host, like `function f() { /* hi */ }`.
198
-
199
- `sameLine` is `true` when the comment shares a source line with the host's adjacent edge (host's start for `before`, host's end for `after`). For `inside` it is always `false`.
200
-
201
- When `attachComments` is disabled (the default), comments are skipped like whitespace and no `comments` field appears on any node.
202
-
203
- ### Why comments are attached to nodes
204
-
205
- Comments are part of the source. Any tool that reads or rewrites code needs to know which comments belong with which node, and that link has to survive AST transforms. Yuku puts a single `comments` array on each node, with a `position` field on each comment (`"before"`, `"after"`, `"inside"`). It is the simplest shape that holds up under transforms and stays cheap to decode. Every other approach trades away one of those properties.
190
+ The span (`start`/`end`) covers the whole comment, delimiters included, so `source.slice(c.start, c.end)` returns the raw text.
206
191
 
207
- **Flat offset-indexed array (ESTree default, Oxc).** A separate `comments: [...]` array on the parse result, sibling to the program, each entry pointing at a source range. Fine for read-only analysis. It falls apart the moment you transform the tree.
192
+ ### Attaching comments to nodes
208
193
 
209
- - *Transforms desync the array.* Insert, delete, or move a node and every offset downstream of the edit is wrong. The comment that used to sit between `a` and `b` may now sit inside something unrelated, or hang in empty space between two nodes. Re-syncing means walking the entire array and rewriting offsets on every edit, and even then the meaning of "the comment between these two things" cannot really be preserved by offsets alone.
210
- - *Codegen cannot trust the offsets.* A printer takes whatever AST it is handed. With offset-based comments, a transformed AST will either print comments in the wrong places or drop them entirely. With attached comments, when you move a node its comments come with it. The printer reads them off `node.comments` and emits them next to the node, no reconciliation needed.
211
- - *Lookup is awkward.* "Give me the comments belonging to this node" with a flat array is a binary search at best and a linear scan at worst. `node.comments` is O(1) and always exact.
194
+ Set `attachComments: true` to also hang each comment on the AST node it sits next to, read off `node.comments`. This is what a codegen pass needs, since attached comments move with their node through transforms.
212
195
 
213
- **Parser returns a flat list, you attach on the JS side (Acorn).** Acorn's `onComment` option pushes every comment into an array you provide, and that is all it does. To get them onto AST nodes you have to run a separate JS pass like `escodegen.attachComments`, which walks the tree, scans the comment list alongside it, and tags each comment as leading or trailing on the nearest node. That work is unavoidable if you want to transform the tree, and doing it in JavaScript is slow. Yuku does the same attachment in Zig as part of the parse. On the 7.8 MB `typescript.js` (30k+ comments), turning `attachComments` on adds only around 4ms.
196
+ ```js
197
+ const { program } = parse(`// header\nfunction foo() {} // trailing`, { attachComments: true });
214
198
 
215
- **Three separate fields per node (Babel).** `leadingComments`, `trailingComments`, and `innerComments`. Same idea as Yuku, but split across three arrays. Two costs come with the split.
199
+ const fn = program.body[0];
200
+ for (const c of fn.comments ?? []) {
201
+ console.log(c.position, c.type, c.value);
202
+ }
203
+ // before Line " header"
204
+ // after Line " trailing"
205
+ ```
216
206
 
217
- - *Decode work.* Three fields per node means three slots to check, three potential array allocations, and roughly 3x the work on the JS side for the same information.
218
- - *Duplication.* A comment that sits between two nodes is the `trailingComments` of one and the `leadingComments` of the other. Transforms have to track both copies and keep them in sync.
207
+ Each attached comment is:
219
208
 
220
- A single `comments` array with a `position` field carries the same meaning, with one entry per comment and one place to look.
209
+ ```ts
210
+ interface AttachedComment {
211
+ type: "Line" | "Block";
212
+ position: "before" | "after" | "inside";
213
+ sameLine: boolean;
214
+ value: string; // body without delimiters
215
+ }
216
+ ```
221
217
 
222
- Even with `attachComments` enabled, Yuku still parses the same file roughly 4x faster than Babel does without comments attached at all. When the flag is off (the default), comments are skipped like whitespace and there is no attachment work at all.
218
+ `position` is where the comment sits relative to its host: `"before"` (leading), `"after"` (trailing), or `"inside"` (interior to an otherwise empty host like `function f() { /* hi */ }`). `sameLine` is `true` when the comment shares a source line with the host's adjacent edge.
223
219
 
224
220
  ## License
225
221
 
package/decode.js CHANGED
@@ -68,22 +68,24 @@ function decode(buffer, source) {
68
68
  extraCount = _u32[1],
69
69
  spLen = _u32[2];
70
70
  const commentCount = _u32[4],
71
- lineStartsCount = _u32[5],
72
- diagCount = _u32[6],
73
- progIdx = _u32[7];
74
- const _flags = _u32[8];
71
+ lineStartsCount = _u32[6],
72
+ diagCount = _u32[7],
73
+ progIdx = _u32[8];
74
+ const attachedCommentCount = _u32[5];
75
+ const _flags = _u32[9];
75
76
  const _isTs = !!(_flags & 1);
76
- const _attachComments = !!(_flags & 2);
77
- const _firstNa = _u32[9];
78
- const _nodesOff = 40;
77
+ const _attached = !!(_flags & 2);
78
+ const _firstNa = _u32[10];
79
+ const _nodesOff = 44;
79
80
  const eOff = _nodesOff + nodeCount * 48;
80
81
  const _extraBase = eOff >> 2;
81
82
  const _spOff = eOff + extraCount * 4;
82
83
  // sections after the variable-length string pool may be at any
83
84
  // byte alignment, so reads against them use `dv`, not `_u32`.
84
85
  const dv = new DataView(buffer);
85
- const _coOff = _spOff + spLen;
86
- const _cOff = _attachComments ? _coOff + (nodeCount + 1) * 4 : _coOff;
86
+ const _aoOff = _spOff + spLen;
87
+ const _acOff = _attached ? _aoOff + (nodeCount + 1) * 4 : _aoOff;
88
+ const _cOff = _acOff + attachedCommentCount * 12;
87
89
  function _poolDecode(s, e) {
88
90
  const a = _spOff + s - _srcLen, b = _spOff + e - _srcLen;
89
91
  let hasEd = false;
@@ -155,10 +157,10 @@ function decode(buffer, source) {
155
157
  if (rest !== NULL) p.push(node(rest));
156
158
  return p;
157
159
  }
158
- function _commentsOf(a, e) {
160
+ function _attachedCommentsOf(a, e) {
159
161
  const out = new Array(e - a);
160
162
  for (let j = a; j < e; j++) {
161
- const o = _cOff + j * 12;
163
+ const o = _acOff + j * 12;
162
164
  const cf = _u8[o + 0];
163
165
  const vs = dv.getUint32(o + 4, true),
164
166
  ve = dv.getUint32(o + 8, true);
@@ -176,9 +178,9 @@ function decode(buffer, source) {
176
178
  // skip unwrap cases whose inner node already carries its own
177
179
  // comments (e.g. formal_parameter returning its pattern)
178
180
  if (r && r.type !== undefined && r.comments === undefined) {
179
- const off = _coOff + i * 4;
181
+ const off = _aoOff + i * 4;
180
182
  const a = dv.getUint32(off, true), e = dv.getUint32(off + 4, true);
181
- if (a !== e) r.comments = _commentsOf(a, e);
183
+ if (a !== e) r.comments = _attachedCommentsOf(a, e);
182
184
  }
183
185
  return r;
184
186
  }
@@ -609,9 +611,27 @@ function decode(buffer, source) {
609
611
  case 170: return { type: "JSXSpreadChild", start, end, expression: f1 !== NULL ? node(f1) : null };
610
612
  }
611
613
  }
612
- const node = _attachComments ? nodeWithComments : _decode;
613
- const lsOff = _cOff + commentCount * 12;
614
+ const node = _attached ? nodeWithComments : _decode;
615
+ const lsOff = _cOff + commentCount * 20;
614
616
  const dOff = lsOff + lineStartsCount * 4;
617
+ function _decodeComments() {
618
+ const out = new Array(commentCount);
619
+ for (let j = 0; j < commentCount; j++) {
620
+ const o = _cOff + j * 20;
621
+ const cf = _u8[o + 0];
622
+ const vs = dv.getUint32(o + 4, true),
623
+ ve = dv.getUint32(o + 8, true);
624
+ const ss = dv.getUint32(o + 12, true),
625
+ se = dv.getUint32(o + 16, true);
626
+ out[j] = {
627
+ type: (cf & 1) ? "Block" : "Line",
628
+ value: str(vs, ve),
629
+ start: _p(ss),
630
+ end: _p(se),
631
+ };
632
+ }
633
+ return out;
634
+ }
615
635
  function _decodeLineStarts() {
616
636
  const out = new Array(lineStartsCount);
617
637
  if (_firstNa >= _srcLen) {
@@ -659,7 +679,7 @@ function decode(buffer, source) {
659
679
  }
660
680
  return out;
661
681
  }
662
- let _program, _lineStarts, _diagnostics;
682
+ let _program, _lineStarts, _diagnostics, _comments;
663
683
  function _getLineStarts() {
664
684
  if (_lineStarts === undefined) _lineStarts = _decodeLineStarts();
665
685
  return _lineStarts;
@@ -668,6 +688,11 @@ function decode(buffer, source) {
668
688
  get program() {
669
689
  return _program !== undefined ? _program : (_program = node(progIdx));
670
690
  },
691
+ get comments() {
692
+ return _comments !== undefined
693
+ ? _comments
694
+ : (_comments = _decodeComments());
695
+ },
671
696
  get diagnostics() {
672
697
  return _diagnostics !== undefined
673
698
  ? _diagnostics
package/index.d.ts CHANGED
@@ -41,18 +41,33 @@ interface ParseOptions {
41
41
  */
42
42
  semanticErrors?: boolean;
43
43
  /**
44
- * Collect comments and attach them to host AST nodes via
45
- * {@link BaseNode.comments}.
44
+ * Also attach each comment to the AST node it sits next to, via
45
+ * {@link BaseNode.comments}. The flat {@link ParseResult.comments} list is
46
+ * always present regardless.
46
47
  * @default false
47
48
  */
48
49
  attachComments?: boolean;
49
50
  }
50
51
 
51
- /** Whether a {@link Comment} came from a line or block source comment. */
52
+ /** Whether a comment came from a line or block source comment. */
52
53
  type CommentType = "Line" | "Block";
53
54
 
54
55
  /**
55
- * Position of a {@link Comment} relative to its host node.
56
+ * A source comment in the flat {@link ParseResult.comments} list, carrying its
57
+ * source span.
58
+ */
59
+ interface Comment {
60
+ type: CommentType;
61
+ /** Comment text without the delimiters. */
62
+ value: string;
63
+ /** Byte offset of the comment start (delimiter included). */
64
+ start: number;
65
+ /** Byte offset of the comment end (delimiter included). */
66
+ end: number;
67
+ }
68
+
69
+ /**
70
+ * Position of an {@link AttachedComment} relative to its host node.
56
71
  *
57
72
  * - `before`: leading the host.
58
73
  * - `after`: trailing the host.
@@ -61,13 +76,13 @@ type CommentType = "Line" | "Block";
61
76
  type CommentPosition = "before" | "after" | "inside";
62
77
 
63
78
  /**
64
- * A source code comment attached to a single host AST node.
79
+ * A comment attached to a single host AST node, via {@link BaseNode.comments}.
65
80
  *
66
81
  * `sameLine` is true when the comment shares a source line with the
67
82
  * host's adjacent edge (host's start for `before`, host's end for
68
83
  * `after`). For `inside` it is always `false`.
69
84
  */
70
- interface Comment {
85
+ interface AttachedComment {
71
86
  type: CommentType;
72
87
  position: CommentPosition;
73
88
  sameLine: boolean;
@@ -119,6 +134,8 @@ interface SourceLocation {
119
134
  interface ParseResult {
120
135
  /** Root ESTree/TypeScript-ESTree AST node. */
121
136
  program: Program;
137
+ /** Every comment in source order, each with its source span. */
138
+ comments: Comment[];
122
139
  /** Syntax diagnostics, and semantic diagnostics when {@link ParseOptions.semanticErrors} is enabled. */
123
140
  diagnostics: Diagnostic[];
124
141
  /**
@@ -165,9 +182,9 @@ interface BaseNode {
165
182
  end: number;
166
183
  /**
167
184
  * Comments attached to this node in source order. Present only when
168
- * {@link ParseOptions.attachComments} is enabled.
185
+ * {@link ParseOptions.attachComments} is true.
169
186
  */
170
- comments?: Comment[];
187
+ comments?: AttachedComment[];
171
188
  }
172
189
 
173
190
  type Span = BaseNode;
@@ -605,7 +622,7 @@ interface Directive extends BaseNode {
605
622
 
606
623
  interface BlockStatement extends BaseNode {
607
624
  type: "BlockStatement";
608
- body: Statement[];
625
+ body: (Statement | Directive)[];
609
626
  }
610
627
 
611
628
  interface IfStatement extends BaseNode {
@@ -1495,7 +1512,7 @@ interface TSModuleDeclaration extends BaseNode {
1495
1512
 
1496
1513
  interface TSModuleBlock extends BaseNode {
1497
1514
  type: "TSModuleBlock";
1498
- body: Statement[];
1515
+ body: ProgramStatement[];
1499
1516
  }
1500
1517
 
1501
1518
  interface TSParameterProperty extends BaseNode {
@@ -1609,9 +1626,17 @@ interface Program extends BaseNode {
1609
1626
  type: "Program";
1610
1627
  sourceType: ModuleKind;
1611
1628
  hashbang: Hashbang | null;
1612
- body: Array<Statement | ModuleDeclaration | Directive>;
1629
+ body: ProgramStatement[];
1613
1630
  }
1614
1631
 
1632
+ /**
1633
+ * An element of `Program.body`. Unlike {@link Statement}, this also includes
1634
+ * {@link ModuleDeclaration} (`import`/`export`) and {@link Directive}, which
1635
+ * are only valid at the top level of a program, never in nested statement
1636
+ * positions such as a block, loop, or `if` body.
1637
+ */
1638
+ type ProgramStatement = Statement | ModuleDeclaration | Directive;
1639
+
1615
1640
  type Declaration =
1616
1641
  | FunctionDeclaration
1617
1642
  | ClassDeclaration
@@ -1761,6 +1786,7 @@ export type {
1761
1786
  ParseOptions,
1762
1787
  ParseResult,
1763
1788
  Comment,
1789
+ AttachedComment,
1764
1790
  CommentType,
1765
1791
  CommentPosition,
1766
1792
  Diagnostic,
@@ -1773,6 +1799,7 @@ export type {
1773
1799
  BaseNode,
1774
1800
  Span,
1775
1801
  Program,
1802
+ ProgramStatement,
1776
1803
  Statement,
1777
1804
  Expression,
1778
1805
  Declaration,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yuku-parser",
3
- "version": "0.5.17",
3
+ "version": "0.5.18",
4
4
  "description": "High-performance JavaScript/TypeScript parser",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -17,17 +17,17 @@
17
17
  "decode.js"
18
18
  ],
19
19
  "optionalDependencies": {
20
- "@yuku-parser/binding-linux-x64-gnu": "0.5.17",
21
- "@yuku-parser/binding-linux-arm64-gnu": "0.5.17",
22
- "@yuku-parser/binding-linux-arm-gnu": "0.5.17",
23
- "@yuku-parser/binding-linux-x64-musl": "0.5.17",
24
- "@yuku-parser/binding-linux-arm64-musl": "0.5.17",
25
- "@yuku-parser/binding-linux-arm-musl": "0.5.17",
26
- "@yuku-parser/binding-darwin-x64": "0.5.17",
27
- "@yuku-parser/binding-darwin-arm64": "0.5.17",
28
- "@yuku-parser/binding-win32-x64": "0.5.17",
29
- "@yuku-parser/binding-win32-arm64": "0.5.17",
30
- "@yuku-parser/binding-freebsd-x64": "0.5.17"
20
+ "@yuku-parser/binding-linux-x64-gnu": "0.5.18",
21
+ "@yuku-parser/binding-linux-arm64-gnu": "0.5.18",
22
+ "@yuku-parser/binding-linux-arm-gnu": "0.5.18",
23
+ "@yuku-parser/binding-linux-x64-musl": "0.5.18",
24
+ "@yuku-parser/binding-linux-arm64-musl": "0.5.18",
25
+ "@yuku-parser/binding-linux-arm-musl": "0.5.18",
26
+ "@yuku-parser/binding-darwin-x64": "0.5.18",
27
+ "@yuku-parser/binding-darwin-arm64": "0.5.18",
28
+ "@yuku-parser/binding-win32-x64": "0.5.18",
29
+ "@yuku-parser/binding-win32-arm64": "0.5.18",
30
+ "@yuku-parser/binding-freebsd-x64": "0.5.18"
31
31
  },
32
32
  "keywords": [
33
33
  "acorn",