@tsrx/core 0.1.63 → 0.1.65

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.63",
6
+ "version": "0.1.65",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -79,7 +79,7 @@
79
79
  "is-reference": "^3.0.3",
80
80
  "magic-string": "^0.30.18",
81
81
  "zimmerframe": "^1.1.2",
82
- "@tsrx/runtime": "0.1.2"
82
+ "@tsrx/runtime": "0.1.4"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@types/node": "^24.3.0",
@@ -7,30 +7,75 @@ import { walk } from 'zimmerframe';
7
7
  import {
8
8
  is_code_block_function_body,
9
9
  is_statement_position,
10
+ is_supported_lazy_assignment_position,
11
+ is_transparent_expression_wrapper,
10
12
  is_tsrx_render_output_node,
11
13
  } from '../utils/ast.js';
12
- import { validate_forgotten_statement_container } from './validation.js';
14
+ import {
15
+ validate_forgotten_statement_container,
16
+ validate_unsupported_lazy_assignment_position,
17
+ } from './validation.js';
13
18
 
14
19
  /**
15
- * Wrappers that preserve an expression's value and therefore do not turn a
16
- * template into a used value on their own. Looking through them keeps strict
17
- * builds and `preserveParens` type-only builds on the same diagnostic path.
20
+ * Find the first authored lazy pattern along an assignment target's binding
21
+ * edges. Default-value expressions and computed keys are evaluated
22
+ * expressions, so nested assignments there own their own diagnostics.
18
23
  *
19
- * @param {AST.Node} parent
20
- * @param {AST.Node} child
21
- * @returns {boolean}
24
+ * @param {AST.Node} node
25
+ * @returns {import('../../types/index').LazyPattern | null}
22
26
  */
23
- function is_transparent_expression_wrapper(parent, child) {
24
- return (
25
- (parent.type === 'ParenthesizedExpression' ||
26
- parent.type === 'TSAsExpression' ||
27
- parent.type === 'TSSatisfiesExpression' ||
28
- parent.type === 'TSNonNullExpression' ||
29
- parent.type === 'TSInstantiationExpression' ||
30
- parent.type === 'TSTypeAssertion' ||
31
- parent.type === 'ChainExpression') &&
32
- /** @type {{ expression?: AST.Node }} */ (parent).expression === child
33
- );
27
+ function find_first_lazy_pattern(node) {
28
+ if ((node.type === 'ObjectPattern' || node.type === 'ArrayPattern') && node.lazy) {
29
+ return node;
30
+ }
31
+
32
+ switch (node.type) {
33
+ case 'AssignmentPattern':
34
+ return find_first_lazy_pattern(node.left);
35
+ case 'RestElement':
36
+ return find_first_lazy_pattern(node.argument);
37
+ case 'ObjectPattern':
38
+ for (const property of node.properties) {
39
+ const lazy =
40
+ property.type === 'Property'
41
+ ? find_first_lazy_pattern(property.value)
42
+ : find_first_lazy_pattern(property.argument);
43
+ if (lazy) return lazy;
44
+ }
45
+ return null;
46
+ case 'ArrayPattern':
47
+ for (const element of node.elements) {
48
+ if (!element) continue;
49
+ const lazy = find_first_lazy_pattern(element);
50
+ if (lazy) return lazy;
51
+ }
52
+ return null;
53
+ default: {
54
+ const expression = /** @type {{ expression?: AST.Node }} */ (node).expression;
55
+ return expression && is_transparent_expression_wrapper(node, expression)
56
+ ? find_first_lazy_pattern(expression)
57
+ : null;
58
+ }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * @param {AST.AssignmentExpression} node
64
+ * @param {{ next: () => unknown, path: AST.Node[], state: TSRXAnalysisState }} context
65
+ */
66
+ function visit_assignment_expression(node, { next, path, state }) {
67
+ const lazy = find_first_lazy_pattern(node.left);
68
+
69
+ if (lazy && !is_supported_lazy_assignment_position(node, path)) {
70
+ validate_unsupported_lazy_assignment_position(
71
+ lazy,
72
+ state.filename,
73
+ state.collect ? state.errors : undefined,
74
+ state.comments,
75
+ );
76
+ }
77
+
78
+ next();
34
79
  }
35
80
 
36
81
  /**
@@ -126,6 +171,8 @@ function visit_class(_node, { next, state }) {
126
171
  }
127
172
 
128
173
  const visitors = {
174
+ AssignmentExpression: visit_assignment_expression,
175
+
129
176
  FunctionDeclaration: visit_function,
130
177
  FunctionExpression: visit_function,
131
178
  ArrowFunctionExpression: visit_function,
@@ -30,6 +30,8 @@ export const TSRX_DO_WHILE_STATEMENT_ERROR =
30
30
  'Do...while loops are not supported in TSRX templates. Move the do...while loop into a function.';
31
31
  export const TSRX_FORGOTTEN_STATEMENT_CONTAINER_ERROR =
32
32
  "This TSRX template output is unused. Return it, assign it to a value that is rendered, or make it part of the rendered output of a function '@{...}' body.";
33
+ export const TSRX_UNSUPPORTED_LAZY_ASSIGNMENT_POSITION_ERROR =
34
+ 'Lazy destructuring assignments require a directly lazy target as a standalone statement inside a program, block, TSRX code block, or switch case.';
33
35
 
34
36
  const invalid_nestings = {
35
37
  // <p> cannot contain block-level elements
@@ -220,6 +222,23 @@ export function validate_forgotten_statement_container(node, filename, errors, c
220
222
  );
221
223
  }
222
224
 
225
+ /**
226
+ * @param {import('../../types/index').LazyPattern} node
227
+ * @param {string | null | undefined} filename
228
+ * @param {CompileError[]} [errors]
229
+ * @param {AST.CommentWithLocation[]} [comments]
230
+ */
231
+ export function validate_unsupported_lazy_assignment_position(node, filename, errors, comments) {
232
+ error(
233
+ TSRX_UNSUPPORTED_LAZY_ASSIGNMENT_POSITION_ERROR,
234
+ filename ?? null,
235
+ node,
236
+ errors,
237
+ comments,
238
+ DIAGNOSTIC_CODES.UNSUPPORTED_LAZY_ASSIGNMENT_POSITION,
239
+ );
240
+ }
241
+
223
242
  /**
224
243
  * @param {AST.ReturnStatement} node
225
244
  * @param {string | null | undefined} filename
@@ -5,4 +5,5 @@ export const DIAGNOSTIC_CODES = {
5
5
  TEMPLATE_EXPRESSION_TRAILING_SEMICOLON: 'tsrx-template-expression-trailing-semicolon',
6
6
  TEMPLATE_RETURN_STATEMENT: 'tsrx-template-return-statement',
7
7
  FORGOTTEN_STATEMENT_CONTAINER: 'tsrx-forgotten-statement-container',
8
+ UNSUPPORTED_LAZY_ASSIGNMENT_POSITION: 'tsrx-unsupported-lazy-assignment-position',
8
9
  };
package/src/index.js CHANGED
@@ -94,6 +94,7 @@ export {
94
94
  is_template_directive as isTemplateDirective,
95
95
  is_tsrx_render_output_node as isTsrxRenderOutputNode,
96
96
  is_code_block_function_body as isCodeBlockFunctionBody,
97
+ is_statement_list_item as isStatementListItem,
97
98
  is_statement_position as isStatementPosition,
98
99
  } from './utils/ast.js';
99
100
 
@@ -262,6 +263,7 @@ export {
262
263
  TSRX_LOOP_CONTINUE_ERROR,
263
264
  TSRX_LOOP_RETURN_ERROR,
264
265
  TSRX_RETURN_STATEMENT_ERROR,
266
+ TSRX_UNSUPPORTED_LAZY_ASSIGNMENT_POSITION_ERROR,
265
267
  TSRX_WHILE_STATEMENT_ERROR,
266
268
  get_return_keyword_node as getReturnKeywordNode,
267
269
  get_statement_keyword_node as getStatementKeywordNode,
@@ -272,6 +274,7 @@ export {
272
274
  validate_tsrx_loop_continue_statement as validateTsrxLoopContinueStatement,
273
275
  validate_tsrx_loop_return_statement as validateTsrxLoopReturnStatement,
274
276
  validate_tsrx_return_statement as validateTsrxReturnStatement,
277
+ validate_unsupported_lazy_assignment_position as validateUnsupportedLazyAssignmentPosition,
275
278
  validate_tsrx_unsupported_loop_statement as validateTsrxUnsupportedLoopStatement,
276
279
  validate_forgotten_statement_container as validateForgottenStatementContainer,
277
280
  validate_nesting as validateNesting,
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([
@@ -709,12 +763,12 @@ export function TSRXPlugin(config) {
709
763
  }
710
764
  if (this.#isTemplateBlockCommentStart(index)) {
711
765
  const comment_start = index;
712
- const comment_start_loc = acorn.getLineInfo(this.input, comment_start);
766
+ const comment_start_loc = get_line_info(this, comment_start);
713
767
  const close = this.input.indexOf('*/', index + 2);
714
768
  const value_end = close === -1 ? this.input.length : close;
715
769
  index = close === -1 ? this.input.length : close + 2;
716
770
  if (this.options.onComment && comment_start >= token_end) {
717
- const comment_end_loc = acorn.getLineInfo(this.input, index);
771
+ const comment_end_loc = get_line_info(this, index);
718
772
  this.options.onComment(
719
773
  true,
720
774
  this.input.slice(comment_start + 2, value_end),
@@ -741,7 +795,7 @@ export function TSRXPlugin(config) {
741
795
  index++;
742
796
  }
743
797
 
744
- const endLoc = acorn.getLineInfo(this.input, index);
798
+ const endLoc = get_line_info(this, index);
745
799
  const node = /** @type {ESTreeJSX.JSXText} */ (this.startNodeAt(start, this.startLoc));
746
800
  node.value = value;
747
801
  node.raw = this.input.slice(start, index);
@@ -793,7 +847,7 @@ export function TSRXPlugin(config) {
793
847
  }
794
848
  }
795
849
  if (!has_newline) return;
796
- const loc = acorn.getLineInfo(this.input, index);
850
+ const loc = get_line_info(this, index);
797
851
  this.start = index;
798
852
  this.startLoc = new acorn.Position(loc.line, loc.column);
799
853
  if (this.pos <= index) {
@@ -822,8 +876,8 @@ export function TSRXPlugin(config) {
822
876
  this.input.slice(start + 2, end),
823
877
  start,
824
878
  end,
825
- acorn.getLineInfo(this.input, start),
826
- acorn.getLineInfo(this.input, end),
879
+ get_line_info(this, start),
880
+ get_line_info(this, end),
827
881
  metadata,
828
882
  );
829
883
  }
@@ -936,7 +990,7 @@ export function TSRXPlugin(config) {
936
990
  }
937
991
  this.pos = start;
938
992
  this.start = start;
939
- this.startLoc = acorn.getLineInfo(this.input, start);
993
+ this.startLoc = get_line_info(this, start);
940
994
  this.exprAllowed = true;
941
995
  this.#suppressTemplateRawTextToken = true;
942
996
  this.next();
@@ -1031,7 +1085,7 @@ export function TSRXPlugin(config) {
1031
1085
  index++;
1032
1086
  }
1033
1087
 
1034
- const endLoc = acorn.getLineInfo(this.input, index);
1088
+ const endLoc = get_line_info(this, index);
1035
1089
  const node = /** @type {ESTreeJSX.JSXText} */ (this.startNodeAt(start, this.startLoc));
1036
1090
  node.value = this.input.slice(start, index);
1037
1091
  node.raw = node.value;
@@ -1195,7 +1249,7 @@ export function TSRXPlugin(config) {
1195
1249
  const start = this.pos;
1196
1250
  const index = this.#templateRawTextEnd(start);
1197
1251
 
1198
- const endLoc = acorn.getLineInfo(this.input, index);
1252
+ const endLoc = get_line_info(this, index);
1199
1253
  const value = this.input.slice(start, index);
1200
1254
  if (value.match(regex_newline_characters)) {
1201
1255
  this.curLine = endLoc.line;
@@ -1394,7 +1448,7 @@ export function TSRXPlugin(config) {
1394
1448
  let node;
1395
1449
  try {
1396
1450
  if (this.type === tstt.jsxText || this.type === tstt.jsxName) {
1397
- const loc = acorn.getLineInfo(this.input, this.start);
1451
+ const loc = get_line_info(this, this.start);
1398
1452
  this.pos = this.start;
1399
1453
  this.curLine = loc.line;
1400
1454
  this.lineStart = this.start - loc.column;
@@ -1429,7 +1483,7 @@ export function TSRXPlugin(config) {
1429
1483
  // (a preceding setup statement's context restore can strip the JSX tag
1430
1484
  // contexts the trailing `<`/`@` token first pushed).
1431
1485
  if (this.start !== at_index) {
1432
- const loc = acorn.getLineInfo(this.input, at_index);
1486
+ const loc = get_line_info(this, at_index);
1433
1487
  this.pos = at_index;
1434
1488
  this.start = at_index;
1435
1489
  this.startLoc = new acorn.Position(loc.line, loc.column);
@@ -1541,7 +1595,7 @@ export function TSRXPlugin(config) {
1541
1595
  if (this.curContext() !== b_stat) {
1542
1596
  this.context.push(b_stat);
1543
1597
  }
1544
- const braceLoc = acorn.getLineInfo(this.input, braceStart);
1598
+ const braceLoc = get_line_info(this, braceStart);
1545
1599
  this.pos = braceStart;
1546
1600
  this.start = braceStart;
1547
1601
  this.startLoc = new acorn.Position(braceLoc.line, braceLoc.column);
@@ -1681,7 +1735,7 @@ export function TSRXPlugin(config) {
1681
1735
  const keywordStart = start + 1;
1682
1736
  this.pos = keywordStart;
1683
1737
  this.start = keywordStart;
1684
- this.startLoc = acorn.getLineInfo(this.input, keywordStart);
1738
+ this.startLoc = get_line_info(this, keywordStart);
1685
1739
  this.curLine = this.startLoc.line;
1686
1740
  this.lineStart = keywordStart - this.startLoc.column;
1687
1741
  this.#filterTemplateScriptContexts();
@@ -1814,13 +1868,13 @@ export function TSRXPlugin(config) {
1814
1868
  start: keywordStart,
1815
1869
  end: keywordEnd,
1816
1870
  loc: {
1817
- start: acorn.getLineInfo(this.input, keywordStart),
1818
- end: acorn.getLineInfo(this.input, keywordEnd),
1871
+ start: get_line_info(this, keywordStart),
1872
+ end: get_line_info(this, keywordEnd),
1819
1873
  },
1820
1874
  };
1821
1875
  this.pos = wordStart;
1822
1876
  this.start = wordStart;
1823
- this.startLoc = acorn.getLineInfo(this.input, wordStart);
1877
+ this.startLoc = get_line_info(this, wordStart);
1824
1878
  this.curLine = this.startLoc.line;
1825
1879
  this.lineStart = wordStart - this.startLoc.column;
1826
1880
  this.#filterTemplateScriptContexts();
@@ -1856,7 +1910,7 @@ export function TSRXPlugin(config) {
1856
1910
 
1857
1911
  this.pos = wordStart;
1858
1912
  this.start = wordStart;
1859
- this.startLoc = acorn.getLineInfo(this.input, wordStart);
1913
+ this.startLoc = get_line_info(this, wordStart);
1860
1914
  this.curLine = this.startLoc.line;
1861
1915
  this.lineStart = wordStart - this.startLoc.column;
1862
1916
  this.#filterTemplateScriptContexts();
@@ -2211,12 +2265,12 @@ export function TSRXPlugin(config) {
2211
2265
 
2212
2266
  if (relativeCloseStart !== -1) {
2213
2267
  const closingStart = contentStart + content.length;
2214
- const closingLineInfo = acorn.getLineInfo(this.input, closingStart);
2268
+ const closingLineInfo = get_line_info(this, closingStart);
2215
2269
  const closingStartLoc = new acorn.Position(closingLineInfo.line, closingLineInfo.column);
2216
2270
  const nameStart = closingStart + 2;
2217
2271
  const nameEnd = nameStart + tagName.length;
2218
- const nameStartInfo = acorn.getLineInfo(this.input, nameStart);
2219
- const nameEndInfo = acorn.getLineInfo(this.input, nameEnd);
2272
+ const nameStartInfo = get_line_info(this, nameStart);
2273
+ const nameEndInfo = get_line_info(this, nameEnd);
2220
2274
  const name = /** @type {ESTreeJSX.JSXIdentifier} */ (
2221
2275
  this.startNodeAt(
2222
2276
  nameStart,
@@ -2231,7 +2285,7 @@ export function TSRXPlugin(config) {
2231
2285
  new acorn.Position(nameEndInfo.line, nameEndInfo.column),
2232
2286
  );
2233
2287
  const closingEnd = closingStart + closeTag.length;
2234
- const closingEndInfo = acorn.getLineInfo(this.input, closingEnd);
2288
+ const closingEndInfo = get_line_info(this, closingEnd);
2235
2289
  const closingElement =
2236
2290
  /** @type {ESTreeJSX.TSRXJSXClosingElement & AST.NodeWithLocation} */ (
2237
2291
  this.startNodeAt(closingStart, closingStartLoc)
@@ -2337,14 +2391,14 @@ export function TSRXPlugin(config) {
2337
2391
  node.children = [];
2338
2392
 
2339
2393
  if (content.length > 0) {
2340
- const bodyStartInfo = acorn.getLineInfo(this.input, open.end);
2394
+ const bodyStartInfo = get_line_info(this, open.end);
2341
2395
  const text = /** @type {ESTreeJSX.JSXText} */ (
2342
2396
  this.startNodeAt(open.end, new acorn.Position(bodyStartInfo.line, bodyStartInfo.column))
2343
2397
  );
2344
2398
  text.value = content;
2345
2399
  text.raw = content;
2346
2400
  const bodyEnd = open.end + content.length;
2347
- const bodyEndInfo = acorn.getLineInfo(this.input, bodyEnd);
2401
+ const bodyEndInfo = get_line_info(this, bodyEnd);
2348
2402
  this.finishNodeAt(
2349
2403
  text,
2350
2404
  'JSXText',
@@ -2625,8 +2679,8 @@ export function TSRXPlugin(config) {
2625
2679
  #report_recoverable_error_range(position, end, message, code) {
2626
2680
  const start = Math.max(0, Math.min(position, this.input.length));
2627
2681
  const range_end = Math.max(start, Math.min(end, this.input.length));
2628
- const start_loc = acorn.getLineInfo(this.input, start);
2629
- 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);
2630
2684
 
2631
2685
  error(
2632
2686
  message,
@@ -3026,7 +3080,7 @@ export function TSRXPlugin(config) {
3026
3080
  this.input.charCodeAt(this.pos - 1) === CharCode.equals
3027
3081
  ) {
3028
3082
  const start = this.pos - 1;
3029
- const loc = acorn.getLineInfo(this.input, start);
3083
+ const loc = get_line_info(this, start);
3030
3084
  this.start = start;
3031
3085
  this.startLoc = loc;
3032
3086
  this.pos++;
@@ -3078,7 +3132,7 @@ export function TSRXPlugin(config) {
3078
3132
  this.input.charCodeAt(this.pos - 1) === CharCode.equals
3079
3133
  ) {
3080
3134
  const start = this.pos - 1;
3081
- const loc = acorn.getLineInfo(this.input, start);
3135
+ const loc = get_line_info(this, start);
3082
3136
  this.start = start;
3083
3137
  this.startLoc = loc;
3084
3138
  this.pos++;
@@ -3823,8 +3877,8 @@ export function TSRXPlugin(config) {
3823
3877
  }
3824
3878
  const brace_start = skip_whitespace_from(this.input, name_end);
3825
3879
  if (this.input.charCodeAt(brace_start) === CharCode.closeBrace) {
3826
- const name_start_loc = acorn.getLineInfo(this.input, name_start);
3827
- 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);
3828
3882
  const name_value = this.input.slice(name_start, name_end);
3829
3883
  const id = /** @type {ESTreeJSX.JSXIdentifier} */ (
3830
3884
  this.startNodeAt(name_start, name_start_loc)
@@ -3844,14 +3898,14 @@ export function TSRXPlugin(config) {
3844
3898
  expression,
3845
3899
  'JSXExpressionContainer',
3846
3900
  brace_start + 1,
3847
- acorn.getLineInfo(this.input, brace_start + 1),
3901
+ get_line_info(this, brace_start + 1),
3848
3902
  );
3849
3903
  /** @type {ESTreeJSX.JSXAttribute} */ (node).name = id;
3850
3904
  /** @type {ESTreeJSX.JSXAttribute} */ (node).value = expression;
3851
3905
  /** @type {ESTreeJSX.JSXAttribute} */ (node).shorthand = true;
3852
3906
 
3853
3907
  const end = brace_start + 1;
3854
- const endLoc = acorn.getLineInfo(this.input, end);
3908
+ const endLoc = get_line_info(this, end);
3855
3909
  this.pos = end;
3856
3910
  this.curLine = endLoc.line;
3857
3911
  this.lineStart = end - endLoc.column;
@@ -4104,7 +4158,7 @@ export function TSRXPlugin(config) {
4104
4158
  if (this.input.charCodeAt(paramStart) === CharCode.openParen) {
4105
4159
  this.pos = paramStart;
4106
4160
  this.start = paramStart;
4107
- this.startLoc = acorn.getLineInfo(this.input, paramStart);
4161
+ this.startLoc = get_line_info(this, paramStart);
4108
4162
  this.curLine = this.startLoc.line;
4109
4163
  this.lineStart = paramStart - this.startLoc.column;
4110
4164
  this.#filterTemplateScriptContexts();
@@ -4274,7 +4328,7 @@ export function TSRXPlugin(config) {
4274
4328
  this.input.charCodeAt(index + 1) === CharCode.greaterThan &&
4275
4329
  this.context.includes(tstc.tc_expr)
4276
4330
  ) {
4277
- const loc = acorn.getLineInfo(this.input, index);
4331
+ const loc = get_line_info(this, index);
4278
4332
  this.pos = index;
4279
4333
  this.start = index;
4280
4334
  this.startLoc = loc;
@@ -4467,7 +4521,7 @@ export function TSRXPlugin(config) {
4467
4521
  !this.#shouldReadTemplateRawTextToken()
4468
4522
  ) {
4469
4523
  const start = this.pos - 1;
4470
- const loc = acorn.getLineInfo(this.input, start);
4524
+ const loc = get_line_info(this, start);
4471
4525
  this.start = start;
4472
4526
  this.startLoc = loc;
4473
4527
  this.pos++;
@@ -4818,7 +4872,7 @@ export function TSRXPlugin(config) {
4818
4872
  );
4819
4873
  text_node.value = ws_value;
4820
4874
  text_node.raw = ws_value;
4821
- const loc = acorn.getLineInfo(this.input, at_index);
4875
+ const loc = get_line_info(this, at_index);
4822
4876
  const at_position = new acorn.Position(loc.line, loc.column);
4823
4877
  this.finishNodeAt(text_node, 'JSXText', at_index, at_position);
4824
4878
  if (this.#shouldKeepTemplateTextNode(text_node)) {
@@ -4928,7 +4982,7 @@ export function TSRXPlugin(config) {
4928
4982
  this.start > blockEnd &&
4929
4983
  /^\s*$/.test(this.input.slice(blockEnd, this.start))
4930
4984
  ) {
4931
- const loc = acorn.getLineInfo(this.input, blockEnd);
4985
+ const loc = get_line_info(this, blockEnd);
4932
4986
  this.pos = blockEnd;
4933
4987
  this.start = blockEnd;
4934
4988
  this.startLoc = new acorn.Position(loc.line, loc.column);
@@ -5373,7 +5427,16 @@ export function TSRXPlugin(config) {
5373
5427
  parseBlock(createNewLexicalScope, node, exitStrict) {
5374
5428
  const parent = this.#path.at(-1);
5375
5429
 
5376
- 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) {
5377
5440
  this.#templateControlFlowBlockDepth--;
5378
5441
  try {
5379
5442
  return this.#parseTemplateControlFlowBlock(createNewLexicalScope, node, exitStrict);