@tsrx/core 0.1.62 → 0.1.64

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/src/plugin.js CHANGED
@@ -49,6 +49,60 @@ const CharCode = Object.freeze({
49
49
  closeBrace: 125,
50
50
  });
51
51
 
52
+ /** @type {WeakMap<Parse.Parser, number[]>} */
53
+ const parser_line_starts = new WeakMap();
54
+
55
+ /**
56
+ * Resolve an offset without rescanning the source from the beginning on every
57
+ * TSRX tokenizer rewind. Acorn's `getLineInfo` is linear in the offset; the
58
+ * parser calls this path often enough that large modules otherwise pay a
59
+ * quadratic location-tracking cost.
60
+ *
61
+ * @param {Parse.Parser} parser
62
+ * @param {number} offset
63
+ * @returns {acorn.Position}
64
+ */
65
+ function get_line_info(parser, offset) {
66
+ let starts = parser_line_starts.get(parser);
67
+ if (starts === undefined) {
68
+ starts = [0];
69
+ for (let i = 0; i < parser.input.length; i++) {
70
+ const ch = parser.input.charCodeAt(i);
71
+ if (ch === CharCode.carriageReturn && parser.input.charCodeAt(i + 1) === CharCode.lineFeed) {
72
+ i++;
73
+ starts.push(i + 1);
74
+ } else if (
75
+ ch === CharCode.lineFeed ||
76
+ ch === CharCode.carriageReturn ||
77
+ ch === 0x2028 ||
78
+ ch === 0x2029
79
+ ) {
80
+ starts.push(i + 1);
81
+ }
82
+ }
83
+ parser_line_starts.set(parser, starts);
84
+ }
85
+
86
+ let low = 0;
87
+ let high = starts.length;
88
+ while (low + 1 < high) {
89
+ const middle = (low + high) >>> 1;
90
+ if (starts[middle] <= offset) low = middle;
91
+ else high = middle;
92
+ }
93
+ // `getLineInfo(input, offset)` treats a CR as a complete line break when
94
+ // `offset` points at the LF of a CRLF pair, even though later offsets treat
95
+ // the pair as one terminator. Preserve that boundary behavior exactly.
96
+ if (
97
+ offset > 0 &&
98
+ parser.input.charCodeAt(offset) === CharCode.lineFeed &&
99
+ parser.input.charCodeAt(offset - 1) === CharCode.carriageReturn
100
+ ) {
101
+ return new acorn.Position(low + 2, 0);
102
+ }
103
+ return new acorn.Position(low + 1, offset - starts[low]);
104
+ }
105
+
52
106
  // Transparent wrappers to look through when validating a dynamic tag
53
107
  // expression (`<{expr}>`), and syntax that disqualifies one outright.
54
108
  const DYNAMIC_TAG_WRAPPER_TYPES = new Set([
@@ -108,6 +162,88 @@ function get_argument_clash_reported_names(check_clashes) {
108
162
  return reported_names;
109
163
  }
110
164
 
165
+ /**
166
+ * The position of a pending `&{…}`/`&[…]` lazy binding pattern recorded on a
167
+ * DestructuringErrors context, or -1. Acorn's DestructuringErrors class knows
168
+ * nothing about the field, so absence means none was recorded.
169
+ *
170
+ * @param {Parse.DestructuringErrors | undefined | null} refDestructuringErrors
171
+ * @returns {number}
172
+ */
173
+ function get_lazy_binding_pos(refDestructuringErrors) {
174
+ return refDestructuringErrors?.lazyBindingPos ?? -1;
175
+ }
176
+
177
+ /** @type {ReadonlySet<string>} */
178
+ const lazy_target_wrapper_types = new Set([
179
+ 'ParenthesizedExpression',
180
+ 'TSAsExpression',
181
+ 'TSSatisfiesExpression',
182
+ 'TSNonNullExpression',
183
+ 'TSTypeAssertion',
184
+ 'TSTypeCastExpression',
185
+ ]);
186
+
187
+ /**
188
+ * Whether the lazy binding pattern recorded at `pos` (the position of its `&`,
189
+ * one character before the pattern node) sits in a pattern-forming position of
190
+ * `node` — a slot that toAssignable converts into a binding or assignment
191
+ * target. A lazy pattern reached only through an expression position (for
192
+ * example as a member expression's object in `&{ a }.b = x`) is an expression
193
+ * use and must keep its pending error. `node` is the pre-conversion tree, so
194
+ * both the expression and pattern spellings of each slot appear here.
195
+ *
196
+ * @param {AST.Node | null | undefined} node
197
+ * @param {number} pos
198
+ * @returns {boolean}
199
+ */
200
+ function pattern_position_contains_lazy(node, pos) {
201
+ if (!node || typeof node !== 'object') return false;
202
+ if (
203
+ /** @type {AST.ObjectPattern | AST.ArrayPattern} */ (node).lazy &&
204
+ /** @type {number} */ (node.start) === pos + 1
205
+ ) {
206
+ return true;
207
+ }
208
+ // Parentheses and the TypeScript expression wrappers acorn-typescript's
209
+ // toAssignable unwraps when converting a target (`[&{ a }!] = arr`) — the
210
+ // wrapped node stays in a pattern-forming position. TSTypeCastExpression is
211
+ // internal to acorn-typescript, hence the string set rather than switch
212
+ // cases over the ESTree union.
213
+ if (lazy_target_wrapper_types.has(node.type)) {
214
+ return pattern_position_contains_lazy(
215
+ /** @type {{ expression: AST.Node }} */ (/** @type {unknown} */ (node)).expression,
216
+ pos,
217
+ );
218
+ }
219
+ switch (node.type) {
220
+ case 'ObjectExpression':
221
+ case 'ObjectPattern':
222
+ return node.properties.some((property) =>
223
+ pattern_position_contains_lazy(
224
+ property.type === 'Property'
225
+ ? /** @type {AST.Node} */ (property.value)
226
+ : property.argument,
227
+ pos,
228
+ ),
229
+ );
230
+ case 'ArrayExpression':
231
+ case 'ArrayPattern':
232
+ return node.elements.some(
233
+ (element) => element && pattern_position_contains_lazy(element, pos),
234
+ );
235
+ case 'AssignmentExpression':
236
+ return node.operator === '=' && pattern_position_contains_lazy(node.left, pos);
237
+ case 'AssignmentPattern':
238
+ return pattern_position_contains_lazy(node.left, pos);
239
+ case 'SpreadElement':
240
+ case 'RestElement':
241
+ return pattern_position_contains_lazy(node.argument, pos);
242
+ default:
243
+ return false;
244
+ }
245
+ }
246
+
111
247
  /**
112
248
  * A `<` opens a tag only when the character after it can begin one: `/` for a
113
249
  * closing tag, `>` for a fragment, `{` for a dynamic tag, or an element or
@@ -276,6 +412,8 @@ export function TSRXPlugin(config) {
276
412
  #errors = undefined;
277
413
  /** @type {string | null} */
278
414
  #filename = null;
415
+ /** @type {WeakMap<object, { names: Set<string>, lexicalLength: number, varLength: number }>} */
416
+ #localExportNamesByScope = new WeakMap();
279
417
  #functionBodyDepth = 0;
280
418
  #allowExpressionContainerTrailingSemicolon = false;
281
419
  #jsxAttributeValueExpressionDepth = 0;
@@ -625,12 +763,12 @@ export function TSRXPlugin(config) {
625
763
  }
626
764
  if (this.#isTemplateBlockCommentStart(index)) {
627
765
  const comment_start = index;
628
- const comment_start_loc = acorn.getLineInfo(this.input, comment_start);
766
+ const comment_start_loc = get_line_info(this, comment_start);
629
767
  const close = this.input.indexOf('*/', index + 2);
630
768
  const value_end = close === -1 ? this.input.length : close;
631
769
  index = close === -1 ? this.input.length : close + 2;
632
770
  if (this.options.onComment && comment_start >= token_end) {
633
- const comment_end_loc = acorn.getLineInfo(this.input, index);
771
+ const comment_end_loc = get_line_info(this, index);
634
772
  this.options.onComment(
635
773
  true,
636
774
  this.input.slice(comment_start + 2, value_end),
@@ -657,7 +795,7 @@ export function TSRXPlugin(config) {
657
795
  index++;
658
796
  }
659
797
 
660
- const endLoc = acorn.getLineInfo(this.input, index);
798
+ const endLoc = get_line_info(this, index);
661
799
  const node = /** @type {ESTreeJSX.JSXText} */ (this.startNodeAt(start, this.startLoc));
662
800
  node.value = value;
663
801
  node.raw = this.input.slice(start, index);
@@ -709,7 +847,7 @@ export function TSRXPlugin(config) {
709
847
  }
710
848
  }
711
849
  if (!has_newline) return;
712
- const loc = acorn.getLineInfo(this.input, index);
850
+ const loc = get_line_info(this, index);
713
851
  this.start = index;
714
852
  this.startLoc = new acorn.Position(loc.line, loc.column);
715
853
  if (this.pos <= index) {
@@ -738,8 +876,8 @@ export function TSRXPlugin(config) {
738
876
  this.input.slice(start + 2, end),
739
877
  start,
740
878
  end,
741
- acorn.getLineInfo(this.input, start),
742
- acorn.getLineInfo(this.input, end),
879
+ get_line_info(this, start),
880
+ get_line_info(this, end),
743
881
  metadata,
744
882
  );
745
883
  }
@@ -852,7 +990,7 @@ export function TSRXPlugin(config) {
852
990
  }
853
991
  this.pos = start;
854
992
  this.start = start;
855
- this.startLoc = acorn.getLineInfo(this.input, start);
993
+ this.startLoc = get_line_info(this, start);
856
994
  this.exprAllowed = true;
857
995
  this.#suppressTemplateRawTextToken = true;
858
996
  this.next();
@@ -947,7 +1085,7 @@ export function TSRXPlugin(config) {
947
1085
  index++;
948
1086
  }
949
1087
 
950
- const endLoc = acorn.getLineInfo(this.input, index);
1088
+ const endLoc = get_line_info(this, index);
951
1089
  const node = /** @type {ESTreeJSX.JSXText} */ (this.startNodeAt(start, this.startLoc));
952
1090
  node.value = this.input.slice(start, index);
953
1091
  node.raw = node.value;
@@ -1111,7 +1249,7 @@ export function TSRXPlugin(config) {
1111
1249
  const start = this.pos;
1112
1250
  const index = this.#templateRawTextEnd(start);
1113
1251
 
1114
- const endLoc = acorn.getLineInfo(this.input, index);
1252
+ const endLoc = get_line_info(this, index);
1115
1253
  const value = this.input.slice(start, index);
1116
1254
  if (value.match(regex_newline_characters)) {
1117
1255
  this.curLine = endLoc.line;
@@ -1310,7 +1448,7 @@ export function TSRXPlugin(config) {
1310
1448
  let node;
1311
1449
  try {
1312
1450
  if (this.type === tstt.jsxText || this.type === tstt.jsxName) {
1313
- const loc = acorn.getLineInfo(this.input, this.start);
1451
+ const loc = get_line_info(this, this.start);
1314
1452
  this.pos = this.start;
1315
1453
  this.curLine = loc.line;
1316
1454
  this.lineStart = this.start - loc.column;
@@ -1345,7 +1483,7 @@ export function TSRXPlugin(config) {
1345
1483
  // (a preceding setup statement's context restore can strip the JSX tag
1346
1484
  // contexts the trailing `<`/`@` token first pushed).
1347
1485
  if (this.start !== at_index) {
1348
- const loc = acorn.getLineInfo(this.input, at_index);
1486
+ const loc = get_line_info(this, at_index);
1349
1487
  this.pos = at_index;
1350
1488
  this.start = at_index;
1351
1489
  this.startLoc = new acorn.Position(loc.line, loc.column);
@@ -1457,7 +1595,7 @@ export function TSRXPlugin(config) {
1457
1595
  if (this.curContext() !== b_stat) {
1458
1596
  this.context.push(b_stat);
1459
1597
  }
1460
- const braceLoc = acorn.getLineInfo(this.input, braceStart);
1598
+ const braceLoc = get_line_info(this, braceStart);
1461
1599
  this.pos = braceStart;
1462
1600
  this.start = braceStart;
1463
1601
  this.startLoc = new acorn.Position(braceLoc.line, braceLoc.column);
@@ -1514,6 +1652,25 @@ export function TSRXPlugin(config) {
1514
1652
  * @type {Parse.Parser['parseExprAtom']}
1515
1653
  */
1516
1654
  parseExprAtom(refDestructuringErrors, forInit, forNew) {
1655
+ // A `&{…}`/`&[…]` lazy binding pattern is only meaningful where the
1656
+ // expression may still turn out to be a binding or assignment target —
1657
+ // an arrow parameter list, a destructuring assignment target, or a
1658
+ // for–of/for–in loop target. Those are exactly the positions Acorn
1659
+ // parses with a DestructuringErrors context, so parse the pattern there
1660
+ // and record it as a pending error the same way Acorn treats `{a = b}`
1661
+ // shorthand: pattern conversion accepts the node as-is, while contexts
1662
+ // that remain expressions raise in checkExpressionErrors.
1663
+ if (
1664
+ refDestructuringErrors &&
1665
+ this.type === tt.bitwiseAND &&
1666
+ (this.input.charCodeAt(this.end) === CharCode.openBrace ||
1667
+ this.input.charCodeAt(this.end) === CharCode.openBracket)
1668
+ ) {
1669
+ if (get_lazy_binding_pos(refDestructuringErrors) < 0) {
1670
+ refDestructuringErrors.lazyBindingPos = this.start;
1671
+ }
1672
+ return /** @type {AST.Expression} */ (/** @type {unknown} */ (this.parseBindingAtom()));
1673
+ }
1517
1674
  // A token already consumed as JSX text (a script-mode element child) must
1518
1675
  // stay text even when it happens to begin at an `@` — otherwise whether
1519
1676
  // `@if` parses as a directive would depend on leading whitespace.
@@ -1578,7 +1735,7 @@ export function TSRXPlugin(config) {
1578
1735
  const keywordStart = start + 1;
1579
1736
  this.pos = keywordStart;
1580
1737
  this.start = keywordStart;
1581
- this.startLoc = acorn.getLineInfo(this.input, keywordStart);
1738
+ this.startLoc = get_line_info(this, keywordStart);
1582
1739
  this.curLine = this.startLoc.line;
1583
1740
  this.lineStart = keywordStart - this.startLoc.column;
1584
1741
  this.#filterTemplateScriptContexts();
@@ -1711,13 +1868,13 @@ export function TSRXPlugin(config) {
1711
1868
  start: keywordStart,
1712
1869
  end: keywordEnd,
1713
1870
  loc: {
1714
- start: acorn.getLineInfo(this.input, keywordStart),
1715
- end: acorn.getLineInfo(this.input, keywordEnd),
1871
+ start: get_line_info(this, keywordStart),
1872
+ end: get_line_info(this, keywordEnd),
1716
1873
  },
1717
1874
  };
1718
1875
  this.pos = wordStart;
1719
1876
  this.start = wordStart;
1720
- this.startLoc = acorn.getLineInfo(this.input, wordStart);
1877
+ this.startLoc = get_line_info(this, wordStart);
1721
1878
  this.curLine = this.startLoc.line;
1722
1879
  this.lineStart = wordStart - this.startLoc.column;
1723
1880
  this.#filterTemplateScriptContexts();
@@ -1753,7 +1910,7 @@ export function TSRXPlugin(config) {
1753
1910
 
1754
1911
  this.pos = wordStart;
1755
1912
  this.start = wordStart;
1756
- this.startLoc = acorn.getLineInfo(this.input, wordStart);
1913
+ this.startLoc = get_line_info(this, wordStart);
1757
1914
  this.curLine = this.startLoc.line;
1758
1915
  this.lineStart = wordStart - this.startLoc.column;
1759
1916
  this.#filterTemplateScriptContexts();
@@ -2108,12 +2265,12 @@ export function TSRXPlugin(config) {
2108
2265
 
2109
2266
  if (relativeCloseStart !== -1) {
2110
2267
  const closingStart = contentStart + content.length;
2111
- const closingLineInfo = acorn.getLineInfo(this.input, closingStart);
2268
+ const closingLineInfo = get_line_info(this, closingStart);
2112
2269
  const closingStartLoc = new acorn.Position(closingLineInfo.line, closingLineInfo.column);
2113
2270
  const nameStart = closingStart + 2;
2114
2271
  const nameEnd = nameStart + tagName.length;
2115
- const nameStartInfo = acorn.getLineInfo(this.input, nameStart);
2116
- const nameEndInfo = acorn.getLineInfo(this.input, nameEnd);
2272
+ const nameStartInfo = get_line_info(this, nameStart);
2273
+ const nameEndInfo = get_line_info(this, nameEnd);
2117
2274
  const name = /** @type {ESTreeJSX.JSXIdentifier} */ (
2118
2275
  this.startNodeAt(
2119
2276
  nameStart,
@@ -2128,7 +2285,7 @@ export function TSRXPlugin(config) {
2128
2285
  new acorn.Position(nameEndInfo.line, nameEndInfo.column),
2129
2286
  );
2130
2287
  const closingEnd = closingStart + closeTag.length;
2131
- const closingEndInfo = acorn.getLineInfo(this.input, closingEnd);
2288
+ const closingEndInfo = get_line_info(this, closingEnd);
2132
2289
  const closingElement =
2133
2290
  /** @type {ESTreeJSX.TSRXJSXClosingElement & AST.NodeWithLocation} */ (
2134
2291
  this.startNodeAt(closingStart, closingStartLoc)
@@ -2234,14 +2391,14 @@ export function TSRXPlugin(config) {
2234
2391
  node.children = [];
2235
2392
 
2236
2393
  if (content.length > 0) {
2237
- const bodyStartInfo = acorn.getLineInfo(this.input, open.end);
2394
+ const bodyStartInfo = get_line_info(this, open.end);
2238
2395
  const text = /** @type {ESTreeJSX.JSXText} */ (
2239
2396
  this.startNodeAt(open.end, new acorn.Position(bodyStartInfo.line, bodyStartInfo.column))
2240
2397
  );
2241
2398
  text.value = content;
2242
2399
  text.raw = content;
2243
2400
  const bodyEnd = open.end + content.length;
2244
- const bodyEndInfo = acorn.getLineInfo(this.input, bodyEnd);
2401
+ const bodyEndInfo = get_line_info(this, bodyEnd);
2245
2402
  this.finishNodeAt(
2246
2403
  text,
2247
2404
  'JSXText',
@@ -2522,8 +2679,8 @@ export function TSRXPlugin(config) {
2522
2679
  #report_recoverable_error_range(position, end, message, code) {
2523
2680
  const start = Math.max(0, Math.min(position, this.input.length));
2524
2681
  const range_end = Math.max(start, Math.min(end, this.input.length));
2525
- const start_loc = acorn.getLineInfo(this.input, start);
2526
- const end_loc = acorn.getLineInfo(this.input, range_end);
2682
+ const start_loc = get_line_info(this, start);
2683
+ const end_loc = get_line_info(this, range_end);
2527
2684
 
2528
2685
  error(
2529
2686
  message,
@@ -2923,7 +3080,7 @@ export function TSRXPlugin(config) {
2923
3080
  this.input.charCodeAt(this.pos - 1) === CharCode.equals
2924
3081
  ) {
2925
3082
  const start = this.pos - 1;
2926
- const loc = acorn.getLineInfo(this.input, start);
3083
+ const loc = get_line_info(this, start);
2927
3084
  this.start = start;
2928
3085
  this.startLoc = loc;
2929
3086
  this.pos++;
@@ -2975,7 +3132,7 @@ export function TSRXPlugin(config) {
2975
3132
  this.input.charCodeAt(this.pos - 1) === CharCode.equals
2976
3133
  ) {
2977
3134
  const start = this.pos - 1;
2978
- const loc = acorn.getLineInfo(this.input, start);
3135
+ const loc = get_line_info(this, start);
2979
3136
  this.start = start;
2980
3137
  this.startLoc = loc;
2981
3138
  this.pos++;
@@ -3236,6 +3393,89 @@ export function TSRXPlugin(config) {
3236
3393
  return expr;
3237
3394
  }
3238
3395
 
3396
+ /**
3397
+ * A recorded lazy binding pattern that never became a binding or
3398
+ * assignment target is a pending error, exactly like `{a = b}` shorthand
3399
+ * outside a destructuring pattern. Acorn calls the throwing form at every
3400
+ * boundary where a context definitively stayed an expression
3401
+ * (parenthesized expressions, call arguments, plain assignments'
3402
+ * right-hand sides, …) — that is where the record is enforced.
3403
+ *
3404
+ * The non-throwing form is deliberately left untouched: Acorn uses it in
3405
+ * parseExprOps/parseMaybeConditional to stop parsing operators early, and
3406
+ * returning true there would cut the pattern off from the `as` /
3407
+ * `satisfies` operator lane before toAssignable can accept the wrapped
3408
+ * target (`[&{ a } as T] = arr`). Every path that keeps a stale record
3409
+ * still ends in a throwing check.
3410
+ *
3411
+ * @type {Parse.Parser['checkExpressionErrors']}
3412
+ */
3413
+ checkExpressionErrors(refDestructuringErrors, andThrow) {
3414
+ if (andThrow) {
3415
+ const lazy_binding_pos = get_lazy_binding_pos(refDestructuringErrors);
3416
+ if (lazy_binding_pos >= 0) {
3417
+ this.raise(
3418
+ lazy_binding_pos,
3419
+ 'Lazy binding patterns are only valid as binding or assignment targets',
3420
+ );
3421
+ }
3422
+ }
3423
+ return super.checkExpressionErrors(refDestructuringErrors, andThrow);
3424
+ }
3425
+
3426
+ /**
3427
+ * acorn-typescript unwraps TS expression wrappers in checkLValSimple,
3428
+ * which is only right for simple targets (`[b as any] = arr`). A wrapped
3429
+ * destructuring pattern (`[&{ a } as T] = arr`) reaches checkLValPattern
3430
+ * still wrapped — toAssignableList ignores return values, so the wrapper
3431
+ * survives conversion — and would fall through to checkLValSimple's
3432
+ * "Assigning to rvalue". Unwrap here so wrapped patterns take the
3433
+ * pattern lane.
3434
+ *
3435
+ * @type {Parse.Parser['checkLValPattern']}
3436
+ */
3437
+ checkLValPattern(expr, bindingType, checkClashes) {
3438
+ let node = expr;
3439
+ while (
3440
+ node.type === 'TSNonNullExpression' ||
3441
+ node.type === 'TSAsExpression' ||
3442
+ node.type === 'TSSatisfiesExpression' ||
3443
+ node.type === 'TSTypeAssertion'
3444
+ ) {
3445
+ node = /** @type {AST.Node} */ (
3446
+ /** @type {{ expression: AST.Node }} */ (/** @type {unknown} */ (node)).expression
3447
+ );
3448
+ }
3449
+ return super.checkLValPattern(node, bindingType, checkClashes);
3450
+ }
3451
+
3452
+ /**
3453
+ * Converting a node into an assignment target resolves any lazy binding
3454
+ * pattern recorded inside it — the pattern landed in a valid position,
3455
+ * so its pending error must not outlive the conversion (e.g.
3456
+ * `({ pair: &{ a } } = obj)` would otherwise still raise when the
3457
+ * enclosing parenthesized expression runs checkExpressionErrors). This
3458
+ * mirrors how Acorn resets `shorthandAssign` once the shorthand ends up
3459
+ * inside a converted left-hand side.
3460
+ *
3461
+ * @type {Parse.Parser['toAssignable']}
3462
+ */
3463
+ toAssignable(node, isBinding, refDestructuringErrors, preserveTypeScriptWrapper) {
3464
+ const lazy_binding_pos = get_lazy_binding_pos(refDestructuringErrors);
3465
+ // Only a pattern-forming position resolves the record: a lazy pattern
3466
+ // that is merely inside the target's span but reached through an
3467
+ // expression position (`&{ a }.b = x`) is still an expression use.
3468
+ if (lazy_binding_pos >= 0 && pattern_position_contains_lazy(node, lazy_binding_pos)) {
3469
+ /** @type {Parse.DestructuringErrors} */ (refDestructuringErrors).lazyBindingPos = -1;
3470
+ }
3471
+ return super.toAssignable(
3472
+ node,
3473
+ isBinding,
3474
+ refDestructuringErrors,
3475
+ preserveTypeScriptWrapper,
3476
+ );
3477
+ }
3478
+
3239
3479
  /**
3240
3480
  * Override checkLocalExport to check all scopes in the scope stack.
3241
3481
  * This is needed because submodules create nested scopes, but exports
@@ -3248,8 +3488,7 @@ export function TSRXPlugin(config) {
3248
3488
  if (this.hasImport(name)) return;
3249
3489
  // Check all scopes in the scope stack, not just the top-level scope
3250
3490
  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) {
3491
+ if (this.#scopeDeclaredNames(this.scopeStack[i]).has(name)) {
3253
3492
  // Found in a scope, remove from undefinedExports if it was added
3254
3493
  delete this.undefinedExports[name];
3255
3494
  return;
@@ -3259,6 +3498,32 @@ export function TSRXPlugin(config) {
3259
3498
  this.undefinedExports[name] = id;
3260
3499
  }
3261
3500
 
3501
+ /**
3502
+ * The names declared in `scope`, as a cached Set. Acorn only ever
3503
+ * appends to a scope's `lexical` and `var` arrays during the scope's
3504
+ * lifetime, so syncing from the last-seen lengths is enough to keep the
3505
+ * Set current.
3506
+ *
3507
+ * @param {{ lexical: string[], var: string[] }} scope
3508
+ * @returns {Set<string>}
3509
+ */
3510
+ #scopeDeclaredNames(scope) {
3511
+ let cached = this.#localExportNamesByScope.get(scope);
3512
+ if (!cached) {
3513
+ cached = { names: new Set(), lexicalLength: 0, varLength: 0 };
3514
+ this.#localExportNamesByScope.set(scope, cached);
3515
+ }
3516
+ for (let i = cached.lexicalLength; i < scope.lexical.length; i++) {
3517
+ cached.names.add(scope.lexical[i]);
3518
+ }
3519
+ for (let i = cached.varLength; i < scope.var.length; i++) {
3520
+ cached.names.add(scope.var[i]);
3521
+ }
3522
+ cached.lexicalLength = scope.lexical.length;
3523
+ cached.varLength = scope.var.length;
3524
+ return cached.names;
3525
+ }
3526
+
3262
3527
  /** @type {Parse.Parser['parseForStatement']} */
3263
3528
  parseForStatement(node) {
3264
3529
  this.next();
@@ -3612,8 +3877,8 @@ export function TSRXPlugin(config) {
3612
3877
  }
3613
3878
  const brace_start = skip_whitespace_from(this.input, name_end);
3614
3879
  if (this.input.charCodeAt(brace_start) === CharCode.closeBrace) {
3615
- const name_start_loc = acorn.getLineInfo(this.input, name_start);
3616
- const name_end_loc = acorn.getLineInfo(this.input, name_end);
3880
+ const name_start_loc = get_line_info(this, name_start);
3881
+ const name_end_loc = get_line_info(this, name_end);
3617
3882
  const name_value = this.input.slice(name_start, name_end);
3618
3883
  const id = /** @type {ESTreeJSX.JSXIdentifier} */ (
3619
3884
  this.startNodeAt(name_start, name_start_loc)
@@ -3633,14 +3898,14 @@ export function TSRXPlugin(config) {
3633
3898
  expression,
3634
3899
  'JSXExpressionContainer',
3635
3900
  brace_start + 1,
3636
- acorn.getLineInfo(this.input, brace_start + 1),
3901
+ get_line_info(this, brace_start + 1),
3637
3902
  );
3638
3903
  /** @type {ESTreeJSX.JSXAttribute} */ (node).name = id;
3639
3904
  /** @type {ESTreeJSX.JSXAttribute} */ (node).value = expression;
3640
3905
  /** @type {ESTreeJSX.JSXAttribute} */ (node).shorthand = true;
3641
3906
 
3642
3907
  const end = brace_start + 1;
3643
- const endLoc = acorn.getLineInfo(this.input, end);
3908
+ const endLoc = get_line_info(this, end);
3644
3909
  this.pos = end;
3645
3910
  this.curLine = endLoc.line;
3646
3911
  this.lineStart = end - endLoc.column;
@@ -3893,7 +4158,7 @@ export function TSRXPlugin(config) {
3893
4158
  if (this.input.charCodeAt(paramStart) === CharCode.openParen) {
3894
4159
  this.pos = paramStart;
3895
4160
  this.start = paramStart;
3896
- this.startLoc = acorn.getLineInfo(this.input, paramStart);
4161
+ this.startLoc = get_line_info(this, paramStart);
3897
4162
  this.curLine = this.startLoc.line;
3898
4163
  this.lineStart = paramStart - this.startLoc.column;
3899
4164
  this.#filterTemplateScriptContexts();
@@ -4063,7 +4328,7 @@ export function TSRXPlugin(config) {
4063
4328
  this.input.charCodeAt(index + 1) === CharCode.greaterThan &&
4064
4329
  this.context.includes(tstc.tc_expr)
4065
4330
  ) {
4066
- const loc = acorn.getLineInfo(this.input, index);
4331
+ const loc = get_line_info(this, index);
4067
4332
  this.pos = index;
4068
4333
  this.start = index;
4069
4334
  this.startLoc = loc;
@@ -4256,7 +4521,7 @@ export function TSRXPlugin(config) {
4256
4521
  !this.#shouldReadTemplateRawTextToken()
4257
4522
  ) {
4258
4523
  const start = this.pos - 1;
4259
- const loc = acorn.getLineInfo(this.input, start);
4524
+ const loc = get_line_info(this, start);
4260
4525
  this.start = start;
4261
4526
  this.startLoc = loc;
4262
4527
  this.pos++;
@@ -4607,7 +4872,7 @@ export function TSRXPlugin(config) {
4607
4872
  );
4608
4873
  text_node.value = ws_value;
4609
4874
  text_node.raw = ws_value;
4610
- const loc = acorn.getLineInfo(this.input, at_index);
4875
+ const loc = get_line_info(this, at_index);
4611
4876
  const at_position = new acorn.Position(loc.line, loc.column);
4612
4877
  this.finishNodeAt(text_node, 'JSXText', at_index, at_position);
4613
4878
  if (this.#shouldKeepTemplateTextNode(text_node)) {
@@ -4717,7 +4982,7 @@ export function TSRXPlugin(config) {
4717
4982
  this.start > blockEnd &&
4718
4983
  /^\s*$/.test(this.input.slice(blockEnd, this.start))
4719
4984
  ) {
4720
- const loc = acorn.getLineInfo(this.input, blockEnd);
4985
+ const loc = get_line_info(this, blockEnd);
4721
4986
  this.pos = blockEnd;
4722
4987
  this.start = blockEnd;
4723
4988
  this.startLoc = new acorn.Position(loc.line, loc.column);
@@ -5162,7 +5427,16 @@ export function TSRXPlugin(config) {
5162
5427
  parseBlock(createNewLexicalScope, node, exitStrict) {
5163
5428
  const parent = this.#path.at(-1);
5164
5429
 
5165
- if (this.#isNativeTemplateNode(parent) && this.#templateControlFlowBlockDepth > 0) {
5430
+ // `#templateControlFlowBlockDepth` alone decides here don't also
5431
+ // require `#isNativeTemplateNode(parent)`: a directive body's own
5432
+ // parsing (`#parseTemplateControlFlowBlock`) empties `#path`, so for
5433
+ // a `@for` nested inside another directive's branch, `parent` is
5434
+ // `undefined` and that check would skip this redirect, dropping the
5435
+ // `@for`'s body into plain statement parsing (nested directives then
5436
+ // land in expression position instead of template position). The
5437
+ // depth counter is set only around a `@for`'s own header+body and
5438
+ // `@empty` clause, so plain JS `for` loops can't false-positive.
5439
+ if (this.#templateControlFlowBlockDepth > 0) {
5166
5440
  this.#templateControlFlowBlockDepth--;
5167
5441
  try {
5168
5442
  return this.#parseTemplateControlFlowBlock(createNewLexicalScope, node, exitStrict);