@tsrx/core 0.1.62 → 0.1.63

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Core compiler infrastructure for TSRX syntax",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.1.62",
6
+ "version": "0.1.63",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
package/src/plugin.js CHANGED
@@ -108,6 +108,88 @@ function get_argument_clash_reported_names(check_clashes) {
108
108
  return reported_names;
109
109
  }
110
110
 
111
+ /**
112
+ * The position of a pending `&{…}`/`&[…]` lazy binding pattern recorded on a
113
+ * DestructuringErrors context, or -1. Acorn's DestructuringErrors class knows
114
+ * nothing about the field, so absence means none was recorded.
115
+ *
116
+ * @param {Parse.DestructuringErrors | undefined | null} refDestructuringErrors
117
+ * @returns {number}
118
+ */
119
+ function get_lazy_binding_pos(refDestructuringErrors) {
120
+ return refDestructuringErrors?.lazyBindingPos ?? -1;
121
+ }
122
+
123
+ /** @type {ReadonlySet<string>} */
124
+ const lazy_target_wrapper_types = new Set([
125
+ 'ParenthesizedExpression',
126
+ 'TSAsExpression',
127
+ 'TSSatisfiesExpression',
128
+ 'TSNonNullExpression',
129
+ 'TSTypeAssertion',
130
+ 'TSTypeCastExpression',
131
+ ]);
132
+
133
+ /**
134
+ * Whether the lazy binding pattern recorded at `pos` (the position of its `&`,
135
+ * one character before the pattern node) sits in a pattern-forming position of
136
+ * `node` — a slot that toAssignable converts into a binding or assignment
137
+ * target. A lazy pattern reached only through an expression position (for
138
+ * example as a member expression's object in `&{ a }.b = x`) is an expression
139
+ * use and must keep its pending error. `node` is the pre-conversion tree, so
140
+ * both the expression and pattern spellings of each slot appear here.
141
+ *
142
+ * @param {AST.Node | null | undefined} node
143
+ * @param {number} pos
144
+ * @returns {boolean}
145
+ */
146
+ function pattern_position_contains_lazy(node, pos) {
147
+ if (!node || typeof node !== 'object') return false;
148
+ if (
149
+ /** @type {AST.ObjectPattern | AST.ArrayPattern} */ (node).lazy &&
150
+ /** @type {number} */ (node.start) === pos + 1
151
+ ) {
152
+ return true;
153
+ }
154
+ // Parentheses and the TypeScript expression wrappers acorn-typescript's
155
+ // toAssignable unwraps when converting a target (`[&{ a }!] = arr`) — the
156
+ // wrapped node stays in a pattern-forming position. TSTypeCastExpression is
157
+ // internal to acorn-typescript, hence the string set rather than switch
158
+ // cases over the ESTree union.
159
+ if (lazy_target_wrapper_types.has(node.type)) {
160
+ return pattern_position_contains_lazy(
161
+ /** @type {{ expression: AST.Node }} */ (/** @type {unknown} */ (node)).expression,
162
+ pos,
163
+ );
164
+ }
165
+ switch (node.type) {
166
+ case 'ObjectExpression':
167
+ case 'ObjectPattern':
168
+ return node.properties.some((property) =>
169
+ pattern_position_contains_lazy(
170
+ property.type === 'Property'
171
+ ? /** @type {AST.Node} */ (property.value)
172
+ : property.argument,
173
+ pos,
174
+ ),
175
+ );
176
+ case 'ArrayExpression':
177
+ case 'ArrayPattern':
178
+ return node.elements.some(
179
+ (element) => element && pattern_position_contains_lazy(element, pos),
180
+ );
181
+ case 'AssignmentExpression':
182
+ return node.operator === '=' && pattern_position_contains_lazy(node.left, pos);
183
+ case 'AssignmentPattern':
184
+ return pattern_position_contains_lazy(node.left, pos);
185
+ case 'SpreadElement':
186
+ case 'RestElement':
187
+ return pattern_position_contains_lazy(node.argument, pos);
188
+ default:
189
+ return false;
190
+ }
191
+ }
192
+
111
193
  /**
112
194
  * A `<` opens a tag only when the character after it can begin one: `/` for a
113
195
  * closing tag, `>` for a fragment, `{` for a dynamic tag, or an element or
@@ -276,6 +358,8 @@ export function TSRXPlugin(config) {
276
358
  #errors = undefined;
277
359
  /** @type {string | null} */
278
360
  #filename = null;
361
+ /** @type {WeakMap<object, { names: Set<string>, lexicalLength: number, varLength: number }>} */
362
+ #localExportNamesByScope = new WeakMap();
279
363
  #functionBodyDepth = 0;
280
364
  #allowExpressionContainerTrailingSemicolon = false;
281
365
  #jsxAttributeValueExpressionDepth = 0;
@@ -1514,6 +1598,25 @@ export function TSRXPlugin(config) {
1514
1598
  * @type {Parse.Parser['parseExprAtom']}
1515
1599
  */
1516
1600
  parseExprAtom(refDestructuringErrors, forInit, forNew) {
1601
+ // A `&{…}`/`&[…]` lazy binding pattern is only meaningful where the
1602
+ // expression may still turn out to be a binding or assignment target —
1603
+ // an arrow parameter list, a destructuring assignment target, or a
1604
+ // for–of/for–in loop target. Those are exactly the positions Acorn
1605
+ // parses with a DestructuringErrors context, so parse the pattern there
1606
+ // and record it as a pending error the same way Acorn treats `{a = b}`
1607
+ // shorthand: pattern conversion accepts the node as-is, while contexts
1608
+ // that remain expressions raise in checkExpressionErrors.
1609
+ if (
1610
+ refDestructuringErrors &&
1611
+ this.type === tt.bitwiseAND &&
1612
+ (this.input.charCodeAt(this.end) === CharCode.openBrace ||
1613
+ this.input.charCodeAt(this.end) === CharCode.openBracket)
1614
+ ) {
1615
+ if (get_lazy_binding_pos(refDestructuringErrors) < 0) {
1616
+ refDestructuringErrors.lazyBindingPos = this.start;
1617
+ }
1618
+ return /** @type {AST.Expression} */ (/** @type {unknown} */ (this.parseBindingAtom()));
1619
+ }
1517
1620
  // A token already consumed as JSX text (a script-mode element child) must
1518
1621
  // stay text even when it happens to begin at an `@` — otherwise whether
1519
1622
  // `@if` parses as a directive would depend on leading whitespace.
@@ -3236,6 +3339,89 @@ export function TSRXPlugin(config) {
3236
3339
  return expr;
3237
3340
  }
3238
3341
 
3342
+ /**
3343
+ * A recorded lazy binding pattern that never became a binding or
3344
+ * assignment target is a pending error, exactly like `{a = b}` shorthand
3345
+ * outside a destructuring pattern. Acorn calls the throwing form at every
3346
+ * boundary where a context definitively stayed an expression
3347
+ * (parenthesized expressions, call arguments, plain assignments'
3348
+ * right-hand sides, …) — that is where the record is enforced.
3349
+ *
3350
+ * The non-throwing form is deliberately left untouched: Acorn uses it in
3351
+ * parseExprOps/parseMaybeConditional to stop parsing operators early, and
3352
+ * returning true there would cut the pattern off from the `as` /
3353
+ * `satisfies` operator lane before toAssignable can accept the wrapped
3354
+ * target (`[&{ a } as T] = arr`). Every path that keeps a stale record
3355
+ * still ends in a throwing check.
3356
+ *
3357
+ * @type {Parse.Parser['checkExpressionErrors']}
3358
+ */
3359
+ checkExpressionErrors(refDestructuringErrors, andThrow) {
3360
+ if (andThrow) {
3361
+ const lazy_binding_pos = get_lazy_binding_pos(refDestructuringErrors);
3362
+ if (lazy_binding_pos >= 0) {
3363
+ this.raise(
3364
+ lazy_binding_pos,
3365
+ 'Lazy binding patterns are only valid as binding or assignment targets',
3366
+ );
3367
+ }
3368
+ }
3369
+ return super.checkExpressionErrors(refDestructuringErrors, andThrow);
3370
+ }
3371
+
3372
+ /**
3373
+ * acorn-typescript unwraps TS expression wrappers in checkLValSimple,
3374
+ * which is only right for simple targets (`[b as any] = arr`). A wrapped
3375
+ * destructuring pattern (`[&{ a } as T] = arr`) reaches checkLValPattern
3376
+ * still wrapped — toAssignableList ignores return values, so the wrapper
3377
+ * survives conversion — and would fall through to checkLValSimple's
3378
+ * "Assigning to rvalue". Unwrap here so wrapped patterns take the
3379
+ * pattern lane.
3380
+ *
3381
+ * @type {Parse.Parser['checkLValPattern']}
3382
+ */
3383
+ checkLValPattern(expr, bindingType, checkClashes) {
3384
+ let node = expr;
3385
+ while (
3386
+ node.type === 'TSNonNullExpression' ||
3387
+ node.type === 'TSAsExpression' ||
3388
+ node.type === 'TSSatisfiesExpression' ||
3389
+ node.type === 'TSTypeAssertion'
3390
+ ) {
3391
+ node = /** @type {AST.Node} */ (
3392
+ /** @type {{ expression: AST.Node }} */ (/** @type {unknown} */ (node)).expression
3393
+ );
3394
+ }
3395
+ return super.checkLValPattern(node, bindingType, checkClashes);
3396
+ }
3397
+
3398
+ /**
3399
+ * Converting a node into an assignment target resolves any lazy binding
3400
+ * pattern recorded inside it — the pattern landed in a valid position,
3401
+ * so its pending error must not outlive the conversion (e.g.
3402
+ * `({ pair: &{ a } } = obj)` would otherwise still raise when the
3403
+ * enclosing parenthesized expression runs checkExpressionErrors). This
3404
+ * mirrors how Acorn resets `shorthandAssign` once the shorthand ends up
3405
+ * inside a converted left-hand side.
3406
+ *
3407
+ * @type {Parse.Parser['toAssignable']}
3408
+ */
3409
+ toAssignable(node, isBinding, refDestructuringErrors, preserveTypeScriptWrapper) {
3410
+ const lazy_binding_pos = get_lazy_binding_pos(refDestructuringErrors);
3411
+ // Only a pattern-forming position resolves the record: a lazy pattern
3412
+ // that is merely inside the target's span but reached through an
3413
+ // expression position (`&{ a }.b = x`) is still an expression use.
3414
+ if (lazy_binding_pos >= 0 && pattern_position_contains_lazy(node, lazy_binding_pos)) {
3415
+ /** @type {Parse.DestructuringErrors} */ (refDestructuringErrors).lazyBindingPos = -1;
3416
+ }
3417
+ return super.toAssignable(
3418
+ node,
3419
+ isBinding,
3420
+ refDestructuringErrors,
3421
+ preserveTypeScriptWrapper,
3422
+ );
3423
+ }
3424
+
3239
3425
  /**
3240
3426
  * Override checkLocalExport to check all scopes in the scope stack.
3241
3427
  * This is needed because submodules create nested scopes, but exports
@@ -3248,8 +3434,7 @@ export function TSRXPlugin(config) {
3248
3434
  if (this.hasImport(name)) return;
3249
3435
  // Check all scopes in the scope stack, not just the top-level scope
3250
3436
  for (let i = this.scopeStack.length - 1; i >= 0; i--) {
3251
- const scope = this.scopeStack[i];
3252
- if (scope.lexical.indexOf(name) !== -1 || scope.var.indexOf(name) !== -1) {
3437
+ if (this.#scopeDeclaredNames(this.scopeStack[i]).has(name)) {
3253
3438
  // Found in a scope, remove from undefinedExports if it was added
3254
3439
  delete this.undefinedExports[name];
3255
3440
  return;
@@ -3259,6 +3444,32 @@ export function TSRXPlugin(config) {
3259
3444
  this.undefinedExports[name] = id;
3260
3445
  }
3261
3446
 
3447
+ /**
3448
+ * The names declared in `scope`, as a cached Set. Acorn only ever
3449
+ * appends to a scope's `lexical` and `var` arrays during the scope's
3450
+ * lifetime, so syncing from the last-seen lengths is enough to keep the
3451
+ * Set current.
3452
+ *
3453
+ * @param {{ lexical: string[], var: string[] }} scope
3454
+ * @returns {Set<string>}
3455
+ */
3456
+ #scopeDeclaredNames(scope) {
3457
+ let cached = this.#localExportNamesByScope.get(scope);
3458
+ if (!cached) {
3459
+ cached = { names: new Set(), lexicalLength: 0, varLength: 0 };
3460
+ this.#localExportNamesByScope.set(scope, cached);
3461
+ }
3462
+ for (let i = cached.lexicalLength; i < scope.lexical.length; i++) {
3463
+ cached.names.add(scope.lexical[i]);
3464
+ }
3465
+ for (let i = cached.varLength; i < scope.var.length; i++) {
3466
+ cached.names.add(scope.var[i]);
3467
+ }
3468
+ cached.lexicalLength = scope.lexical.length;
3469
+ cached.varLength = scope.var.length;
3470
+ return cached.names;
3471
+ }
3472
+
3262
3473
  /** @type {Parse.Parser['parseForStatement']} */
3263
3474
  parseForStatement(node) {
3264
3475
  this.next();
package/types/parse.d.ts CHANGED
@@ -86,6 +86,11 @@ export namespace Parse {
86
86
  parenthesizedAssign: number;
87
87
  parenthesizedBind: number;
88
88
  doubleProto: number;
89
+ /**
90
+ * TSRX extension: position of a pending `&{…}`/`&[…]` lazy binding
91
+ * pattern parsed in expression position, set only by TSRXPlugin.
92
+ */
93
+ lazyBindingPos?: number;
89
94
  }
90
95
 
91
96
  /**
@@ -1605,11 +1610,13 @@ export namespace Parse {
1605
1610
  * @param node Expression to convert
1606
1611
  * @param isBinding Whether binding pattern
1607
1612
  * @param refDestructuringErrors Error collector
1613
+ * @param preserveTypeScriptWrapper Keep TS wrapper nodes (acorn-typescript extension)
1608
1614
  */
1609
1615
  toAssignable(
1610
1616
  node: AST.Node,
1611
1617
  isBinding?: boolean,
1612
1618
  refDestructuringErrors?: DestructuringErrors,
1619
+ preserveTypeScriptWrapper?: boolean,
1613
1620
  ): AST.Pattern;
1614
1621
 
1615
1622
  /**