@tsrx/core 0.1.44 → 0.1.46

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 CHANGED
@@ -27,9 +27,10 @@ pnpm add @tsrx/core
27
27
  ## Usage
28
28
 
29
29
  ```js
30
- import { parseModule } from '@tsrx/core';
30
+ import { analyzeTsrx, parseModule } from '@tsrx/core';
31
31
 
32
32
  const ast = parseModule(source, 'App.tsrx');
33
+ const analysis = analyzeTsrx(ast, 'App.tsrx');
33
34
  ```
34
35
 
35
36
  The parser produces an ESTree-compatible AST, augmented with the TSRX node types
@@ -54,6 +55,10 @@ here and keeps package docs focused on the core parser API.
54
55
 
55
56
  - **`parseModule(source, filename, options?)`** — parse a TSRX module into an
56
57
  ESTree AST.
58
+ - **`analyzeTsrx(ast, filename, options?)`** — run target-neutral semantic
59
+ validation before framework analysis or transformation. Pass `collect: true`,
60
+ `typeOnly: true`, or `to_ts: true` to collect non-fatal diagnostics for
61
+ editor/type-only output.
57
62
  - **Scope analysis** — `createScopes`, `Scope`, `ScopeRoot`, binding tracking
58
63
  (`import`, `prop`, `let`, `const`, `function`, `for_pattern`, …).
59
64
  - **AST utilities** — pattern walkers, identifier extraction, builders, location
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.44",
6
+ "version": "0.1.46",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -0,0 +1,177 @@
1
+ /**
2
+ @import * as AST from 'estree';
3
+ @import { TSRXAnalysisOptions, TSRXAnalysisResult, TSRXAnalysisState } from '../../types/index';
4
+ */
5
+
6
+ import { walk } from 'zimmerframe';
7
+ import {
8
+ is_code_block_function_body,
9
+ is_statement_position,
10
+ is_tsrx_render_output_node,
11
+ } from '../utils/ast.js';
12
+ import { validate_forgotten_statement_container } from './validation.js';
13
+
14
+ /**
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.
18
+ *
19
+ * @param {AST.Node} parent
20
+ * @param {AST.Node} child
21
+ * @returns {boolean}
22
+ */
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
+ );
34
+ }
35
+
36
+ /**
37
+ * A template is unused only when it is itself the statement being executed.
38
+ * Templates nested in assignments, returns, arguments, operands, or other
39
+ * value-producing expressions may be consumed later and are valid.
40
+ *
41
+ * @param {AST.Node} node
42
+ * @param {AST.Node[]} path
43
+ * @returns {boolean}
44
+ */
45
+ function is_free_floating_template(node, path) {
46
+ let child = node;
47
+
48
+ for (let i = path.length - 1; i >= 0; i -= 1) {
49
+ const parent = path[i];
50
+
51
+ if (is_transparent_expression_wrapper(parent, child)) {
52
+ child = parent;
53
+ continue;
54
+ }
55
+
56
+ if (parent.type === 'ExpressionStatement' && parent.expression === child) {
57
+ return true;
58
+ }
59
+
60
+ if (is_statement_position(parent, child)) {
61
+ return true;
62
+ }
63
+
64
+ return false;
65
+ }
66
+
67
+ return false;
68
+ }
69
+
70
+ /**
71
+ * @param {AST.Function} node
72
+ * @param {{ next: (state?: TSRXAnalysisState) => unknown, state: TSRXAnalysisState }} context
73
+ */
74
+ function visit_function(node, { next, state }) {
75
+ next({
76
+ ...state,
77
+ function: node,
78
+ function_body_is_code_block: is_code_block_function_body(node.body, node),
79
+ inside_template_output: false,
80
+ });
81
+ }
82
+
83
+ /**
84
+ * @param {AST.Node} node
85
+ * @param {{ next: (state?: TSRXAnalysisState) => unknown, path: AST.Node[], state: TSRXAnalysisState }} context
86
+ */
87
+ function visit_render_output(node, { next, path, state }) {
88
+ if (!is_tsrx_render_output_node(node)) {
89
+ next();
90
+ return;
91
+ }
92
+
93
+ if (
94
+ state.function &&
95
+ !(state.function_body_is_code_block && state.function.body === node) &&
96
+ !state.inside_template_output &&
97
+ is_free_floating_template(node, path)
98
+ ) {
99
+ validate_forgotten_statement_container(
100
+ node,
101
+ state.filename,
102
+ state.collect ? state.errors : undefined,
103
+ state.comments,
104
+ );
105
+ }
106
+
107
+ // A JSXCodeBlock contains ordinary setup statements in `body` as well as
108
+ // the retained output in `render`. Reset the template context while walking
109
+ // both fields so free-floating output in setup is still diagnosed. The
110
+ // render node itself is retained by the code block, and establishes template
111
+ // context for its own descendants when this visitor reaches it.
112
+ next({ ...state, inside_template_output: node.type !== 'JSXCodeBlock' });
113
+ }
114
+
115
+ /**
116
+ * @param {AST.ClassDeclaration | AST.ClassExpression} _node
117
+ * @param {{ next: (state?: TSRXAnalysisState) => unknown, state: TSRXAnalysisState }} context
118
+ */
119
+ function visit_class(_node, { next, state }) {
120
+ next({
121
+ ...state,
122
+ function: null,
123
+ function_body_is_code_block: false,
124
+ inside_template_output: false,
125
+ });
126
+ }
127
+
128
+ const visitors = {
129
+ FunctionDeclaration: visit_function,
130
+ FunctionExpression: visit_function,
131
+ ArrowFunctionExpression: visit_function,
132
+
133
+ // A class body is not part of the surrounding function's execution context.
134
+ // Method/function nodes establish their own context when reached.
135
+ ClassDeclaration: visit_class,
136
+ ClassExpression: visit_class,
137
+
138
+ JSXElement: visit_render_output,
139
+ JSXFragment: visit_render_output,
140
+ JSXStyleElement: visit_render_output,
141
+ JSXCodeBlock: visit_render_output,
142
+ JSXIfExpression: visit_render_output,
143
+ JSXForExpression: visit_render_output,
144
+ JSXSwitchExpression: visit_render_output,
145
+ JSXTryExpression: visit_render_output,
146
+ };
147
+
148
+ /**
149
+ * Run target-neutral semantic validation over a parsed TSRX module. Parsing
150
+ * remains syntax-only; every target invokes this pass before target analysis or
151
+ * transformation. Type-only/Volar callers collect diagnostics and continue.
152
+ *
153
+ * @param {AST.Program} ast
154
+ * @param {string | null | undefined} filename
155
+ * @param {TSRXAnalysisOptions} [options]
156
+ * @returns {TSRXAnalysisResult}
157
+ */
158
+ export function analyze_tsrx(ast, filename, options = {}) {
159
+ const errors = options.errors ?? [];
160
+ const comments = options.comments ?? [];
161
+ const collect = !!(options.collect || options.loose || options.typeOnly || options.to_ts);
162
+
163
+ /** @type {TSRXAnalysisState} */
164
+ const state = {
165
+ filename: filename ?? null,
166
+ collect,
167
+ errors,
168
+ comments,
169
+ function: null,
170
+ function_body_is_code_block: false,
171
+ inside_template_output: false,
172
+ };
173
+
174
+ walk(ast, state, visitors);
175
+
176
+ return { ast, errors, comments };
177
+ }
@@ -28,6 +28,8 @@ export const TSRX_WHILE_STATEMENT_ERROR =
28
28
  'While loops are not supported in TSRX templates. Move the while loop into a function.';
29
29
  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
+ export const TSRX_FORGOTTEN_STATEMENT_CONTAINER_ERROR =
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.";
31
33
 
32
34
  const invalid_nestings = {
33
35
  // <p> cannot contain block-level elements
@@ -201,6 +203,23 @@ export function validate_tsrx_return_statement(node, filename, errors, comments)
201
203
  );
202
204
  }
203
205
 
206
+ /**
207
+ * @param {AST.Node} node
208
+ * @param {string | null | undefined} filename
209
+ * @param {CompileError[]} [errors]
210
+ * @param {AST.CommentWithLocation[]} [comments]
211
+ */
212
+ export function validate_forgotten_statement_container(node, filename, errors, comments) {
213
+ error(
214
+ TSRX_FORGOTTEN_STATEMENT_CONTAINER_ERROR,
215
+ filename ?? null,
216
+ node,
217
+ errors,
218
+ comments,
219
+ DIAGNOSTIC_CODES.FORGOTTEN_STATEMENT_CONTAINER,
220
+ );
221
+ }
222
+
204
223
  /**
205
224
  * @param {AST.ReturnStatement} node
206
225
  * @param {string | null | undefined} filename
package/src/index.js CHANGED
@@ -87,10 +87,18 @@ export {
87
87
  build_assignment_value as buildAssignmentValue,
88
88
  is_class_node as isClassNode,
89
89
  is_function_node as isFunctionNode,
90
+ is_function_or_class_node as isFunctionOrClassNode,
90
91
  is_function_or_component_node as isFunctionOrComponentNode,
91
92
  is_inside_component as isInsideComponent,
93
+ is_template_directive as isTemplateDirective,
94
+ is_tsrx_render_output_node as isTsrxRenderOutputNode,
95
+ is_code_block_function_body as isCodeBlockFunctionBody,
96
+ is_statement_position as isStatementPosition,
92
97
  } from './utils/ast.js';
93
98
 
99
+ // Shared TSRX semantic analysis
100
+ export { analyze_tsrx as analyzeTsrx } from './analyze/index.js';
101
+
94
102
  // Builders (namespace re-export — members mirror AST node kinds)
95
103
  export * as builders from './utils/builders.js';
96
104
 
@@ -244,6 +252,7 @@ export { analyze_css as analyzeCss } from './analyze/css-analyze.js';
244
252
  export { prune_css as pruneCss } from './analyze/prune.js';
245
253
  export {
246
254
  TSRX_DO_WHILE_STATEMENT_ERROR,
255
+ TSRX_FORGOTTEN_STATEMENT_CONTAINER_ERROR,
247
256
  TSRX_FOR_IN_STATEMENT_ERROR,
248
257
  TSRX_FOR_STATEMENT_ERROR,
249
258
  TSRX_IF_BREAK_ERROR,
@@ -264,6 +273,7 @@ export {
264
273
  validate_tsrx_loop_return_statement as validateTsrxLoopReturnStatement,
265
274
  validate_tsrx_return_statement as validateTsrxReturnStatement,
266
275
  validate_tsrx_unsupported_loop_statement as validateTsrxUnsupportedLoopStatement,
276
+ validate_forgotten_statement_container as validateForgottenStatementContainer,
267
277
  validate_nesting as validateNesting,
268
278
  is_template_value_position as isTemplateValuePosition,
269
279
  } from './analyze/validation.js';
package/src/plugin.js CHANGED
@@ -11,8 +11,7 @@ import { regex_newline_characters } from './utils/patterns.js';
11
11
  import { error } from './errors.js';
12
12
  import { DIAGNOSTIC_CODES } from './diagnostics.js';
13
13
  import { TSRX_RETURN_STATEMENT_ERROR } from './analyze/validation.js';
14
- const FORGOTTEN_STATEMENT_CONTAINER_ERROR =
15
- "This function body contains TSRX template output, but it is a normal JavaScript block. Add '@' before the opening brace to use a TSRX statement container.";
14
+ import { is_tsrx_render_output_node } from './utils/ast.js';
16
15
 
17
16
  const CharCode = Object.freeze({
18
17
  tab: 9,
@@ -1162,104 +1161,6 @@ export function TSRXPlugin(config) {
1162
1161
  return this.#isCodeBlockStart(index);
1163
1162
  }
1164
1163
 
1165
- /**
1166
- * @param {AST.Node | null | undefined} node
1167
- */
1168
- #isRenderOutputNode(node) {
1169
- if (!node) return false;
1170
- switch (node.type) {
1171
- case 'JSXElement':
1172
- case 'JSXFragment':
1173
- case 'JSXStyleElement':
1174
- case 'JSXCodeBlock':
1175
- case 'JSXIfExpression':
1176
- case 'JSXForExpression':
1177
- case 'JSXSwitchExpression':
1178
- case 'JSXTryExpression':
1179
- return true;
1180
- }
1181
- return false;
1182
- }
1183
-
1184
- /**
1185
- * @param {AST.Node | null | undefined} node
1186
- */
1187
- #isForgottenStatementContainerOutputNode(node) {
1188
- return this.#isRenderOutputNode(node) && node?.type !== 'JSXCodeBlock';
1189
- }
1190
-
1191
- /**
1192
- * @param {AST.Node | null | undefined} node
1193
- */
1194
- #isIgnoredForgottenStatementContainerStatement(node) {
1195
- return !node || node.type === 'EmptyStatement';
1196
- }
1197
-
1198
- /**
1199
- * A normal function body that directly contains a bare JSX/control-flow node
1200
- * almost always means the author wrote `{ ... <div /> }` but intended
1201
- * `@{ ... <div /> }`. Only report when adding `@` would produce a valid
1202
- * statement container: setup statements first, followed by one final render
1203
- * output. Report only direct body children so ordinary nested callbacks/branches
1204
- * are diagnosed by their own function body, not their parent.
1205
- * @param {AST.Node} node
1206
- */
1207
- #reportForgottenStatementContainerBody(node) {
1208
- if (!this.#collect) {
1209
- return;
1210
- }
1211
-
1212
- const body = /** @type {{ body?: AST.Node }} */ (node).body;
1213
- if (body?.type !== 'BlockStatement') {
1214
- return;
1215
- }
1216
-
1217
- const statements = /** @type {AST.BlockStatement} */ (body).body || [];
1218
- const has_return_type = Boolean(/** @type {{ returnType?: AST.Node }} */ (node).returnType);
1219
- if (!has_return_type) {
1220
- return;
1221
- }
1222
-
1223
- let target = null;
1224
- let target_index = -1;
1225
- for (let index = 0; index < statements.length; index++) {
1226
- const statement = statements[index];
1227
- const output =
1228
- this.#isForgottenStatementContainerOutputNode(statement) ||
1229
- (statement.type === 'ExpressionStatement' &&
1230
- this.#isForgottenStatementContainerOutputNode(statement.expression))
1231
- ? statement
1232
- : null;
1233
-
1234
- if (!output) {
1235
- continue;
1236
- }
1237
-
1238
- if (target_index !== -1) {
1239
- return;
1240
- }
1241
- target_index = index;
1242
- target = output;
1243
- }
1244
-
1245
- if (!target) {
1246
- return;
1247
- }
1248
-
1249
- for (const statement of statements.slice(target_index + 1)) {
1250
- if (!this.#isIgnoredForgottenStatementContainerStatement(statement)) {
1251
- return;
1252
- }
1253
- }
1254
-
1255
- this.#report_recoverable_error_range(
1256
- /** @type {number} */ (target.start),
1257
- /** @type {number} */ (target.end),
1258
- FORGOTTEN_STATEMENT_CONTAINER_ERROR,
1259
- DIAGNOSTIC_CODES.FORGOTTEN_STATEMENT_CONTAINER,
1260
- );
1261
- }
1262
-
1263
1164
  /**
1264
1165
  * Inside a code block (`@{ … }` or a directive's `{ }`), decides whether the
1265
1166
  * next thing is the single bare render node (`<tag …>`, `<>…</>`, or an
@@ -1500,7 +1401,7 @@ export function TSRXPlugin(config) {
1500
1401
  }
1501
1402
 
1502
1403
  const last = flat[flat.length - 1];
1503
- if (this.#isRenderOutputNode(last)) {
1404
+ if (is_tsrx_render_output_node(last)) {
1504
1405
  node.render = last;
1505
1406
  node.body = /** @type {AST.Statement[]} */ (flat.slice(0, -1));
1506
1407
  } else {
@@ -3455,9 +3356,7 @@ export function TSRXPlugin(config) {
3455
3356
  this.exitScope();
3456
3357
  return node;
3457
3358
  }
3458
- const parsed = super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
3459
- this.#reportForgottenStatementContainerBody(parsed);
3460
- return parsed;
3359
+ return super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
3461
3360
  } finally {
3462
3361
  this.#functionBodyDepth--;
3463
3362
  }
@@ -4153,7 +4052,12 @@ export function TSRXPlugin(config) {
4153
4052
  chunkStart = this.pos;
4154
4053
  break;
4155
4054
  }
4156
- if (this.#shouldReadTemplateRawTextToken()) {
4055
+ // Like the default case below, a slash joins the raw text run even
4056
+ // when the element is nested inside a `{ … }` expression container
4057
+ // (`{cond && (<a>x/y</a>)}`, `{a}/{b}` between child expressions).
4058
+ // Bailing to JS tokenization here would read the `/` as the start
4059
+ // of a regular expression.
4060
+ if (this.#shouldReadTemplateRawTextToken(true)) {
4157
4061
  ++this.pos;
4158
4062
  break;
4159
4063
  }
@@ -61,6 +61,10 @@ import {
61
61
  } from '../jsx-interleave.js';
62
62
  import { is_hoist_safe_jsx_node } from '../jsx-hoist.js';
63
63
  import { lower_server_module_for_types } from './server-module.js';
64
+ import {
65
+ is_function_or_class_node as is_function_or_class_boundary,
66
+ is_template_directive as is_jsx_control_flow_expression,
67
+ } from '../../utils/ast.js';
64
68
 
65
69
  const TEMPLATE_FRAGMENT_ERROR =
66
70
  'JSX fragment syntax is not needed in TSRX templates. TSRX renders in immediate mode, so everything is already a fragment. Use `<>...</>` only in expression position.';
@@ -114,20 +118,6 @@ function report_jsx_fragment_in_tsrx_error(node, transform_context) {
114
118
  * @typedef {{ source_name: string, read: () => any }} LazyBinding
115
119
  */
116
120
 
117
- /**
118
- * @param {any} node
119
- * @returns {boolean}
120
- */
121
- function is_function_or_class_boundary(node) {
122
- return (
123
- node?.type === 'FunctionDeclaration' ||
124
- node?.type === 'FunctionExpression' ||
125
- node?.type === 'ArrowFunctionExpression' ||
126
- node?.type === 'ClassDeclaration' ||
127
- node?.type === 'ClassExpression'
128
- );
129
- }
130
-
131
121
  /**
132
122
  * @param {any} node
133
123
  * @param {boolean} [inside_function]
@@ -264,22 +254,6 @@ function expand_child_code_blocks(node, seen = new Set()) {
264
254
  return out;
265
255
  }
266
256
 
267
- /**
268
- * A `@`-prefixed JSX control-flow expression (`@if`/`@for`/`@switch`/`@try`).
269
- * These are the only control-flow nodes that can appear in expression position;
270
- * the plain statement forms (`IfStatement`, `SwitchStatement`, …) never do.
271
- * @param {any} node
272
- * @returns {boolean}
273
- */
274
- function is_jsx_control_flow_expression(node) {
275
- return (
276
- node?.type === 'JSXIfExpression' ||
277
- node?.type === 'JSXForExpression' ||
278
- node?.type === 'JSXSwitchExpression' ||
279
- node?.type === 'JSXTryExpression'
280
- );
281
- }
282
-
283
257
  /**
284
258
  * Wrap a render-output node in a native TSRX fragment so it flows through the
285
259
  * same single-child render path as a `<> … </>` output. This is a compiler
@@ -1,6 +1,7 @@
1
1
  /** @import * as AST from 'estree' */
2
2
 
3
3
  import * as b from '../utils/builders.js';
4
+ import { is_function_or_class_node as is_function_or_class_boundary } from '../utils/ast.js';
4
5
  import { clone_expression_node, clone_identifier } from './jsx/ast-builders.js';
5
6
 
6
7
  const regex_backslash_and_following_character = /\\(.)/g;
@@ -236,20 +237,6 @@ function is_style_element(node) {
236
237
  return !!node && node.type === 'JSXStyleElement';
237
238
  }
238
239
 
239
- /**
240
- * @param {any} node
241
- * @returns {boolean}
242
- */
243
- function is_function_or_class_boundary(node) {
244
- return (
245
- node?.type === 'FunctionDeclaration' ||
246
- node?.type === 'FunctionExpression' ||
247
- node?.type === 'ArrowFunctionExpression' ||
248
- node?.type === 'ClassDeclaration' ||
249
- node?.type === 'ClassExpression'
250
- );
251
- }
252
-
253
240
  /**
254
241
  * @param {any} css
255
242
  * @returns {Map<string, any>}
package/src/utils/ast.js CHANGED
@@ -22,7 +22,7 @@ import * as b from './builders.js';
22
22
 
23
23
  /**
24
24
  * @param {AST.Node} node
25
- * @returns {boolean}
25
+ * @returns {node is AST.Function}
26
26
  */
27
27
  export function is_function_node(node) {
28
28
  return (
@@ -32,6 +32,14 @@ export function is_function_node(node) {
32
32
  );
33
33
  }
34
34
 
35
+ /**
36
+ * @param {AST.Node | null | undefined} node
37
+ * @returns {node is AST.Function | AST.ClassDeclaration | AST.ClassExpression}
38
+ */
39
+ export function is_function_or_class_node(node) {
40
+ return !!node && (is_function_node(node) || is_class_node(node));
41
+ }
42
+
35
43
  /**
36
44
  * @param {AST.Node} node
37
45
  * @returns {boolean}
@@ -48,6 +56,87 @@ export function is_class_node(node) {
48
56
  return node.type === 'ClassExpression' || node.type === 'ClassDeclaration';
49
57
  }
50
58
 
59
+ /**
60
+ * A parsed `@if`/`@for`/`@switch`/`@try` control-flow directive.
61
+ *
62
+ * @param {AST.Node | null | undefined} node
63
+ * @returns {node is AST.JSXTemplateDirective}
64
+ */
65
+ export function is_template_directive(node) {
66
+ return (
67
+ node?.type === 'JSXIfExpression' ||
68
+ node?.type === 'JSXForExpression' ||
69
+ node?.type === 'JSXSwitchExpression' ||
70
+ node?.type === 'JSXTryExpression'
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Any source AST node that can be the rendered output of a TSRX template or
76
+ * statement container.
77
+ *
78
+ * @param {AST.Node | null | undefined} node
79
+ * @returns {node is AST.TSRXRenderOutput}
80
+ */
81
+ export function is_tsrx_render_output_node(node) {
82
+ return !!(
83
+ node &&
84
+ (node.type === 'JSXElement' ||
85
+ node.type === 'JSXFragment' ||
86
+ node.type === 'JSXStyleElement' ||
87
+ node.type === 'JSXCodeBlock' ||
88
+ is_template_directive(node))
89
+ );
90
+ }
91
+
92
+ /**
93
+ * @param {AST.Node | null | undefined} node
94
+ * @param {AST.Node | null | undefined} parent
95
+ * @returns {node is AST.JSXCodeBlock}
96
+ */
97
+ export function is_code_block_function_body(node, parent) {
98
+ return !!(
99
+ node &&
100
+ node.type === 'JSXCodeBlock' &&
101
+ parent &&
102
+ is_function_node(parent) &&
103
+ parent.body === node
104
+ );
105
+ }
106
+
107
+ /**
108
+ * Returns whether `child` directly occupies a statement slot of `parent`.
109
+ * Parser-native TSRX output nodes can appear in these slots without an
110
+ * `ExpressionStatement` wrapper, notably as braceless control-flow bodies.
111
+ *
112
+ * @param {AST.Node} parent
113
+ * @param {AST.Node} child
114
+ * @returns {boolean}
115
+ */
116
+ export function is_statement_position(parent, child) {
117
+ switch (parent.type) {
118
+ case 'Program':
119
+ case 'BlockStatement':
120
+ return parent.body.includes(/** @type {AST.Statement} */ (child));
121
+ case 'SwitchCase':
122
+ return parent.consequent.includes(/** @type {AST.Statement} */ (child));
123
+ case 'JSXCodeBlock':
124
+ return parent.body.includes(/** @type {AST.Statement} */ (child));
125
+ case 'IfStatement':
126
+ return parent.consequent === child || parent.alternate === child;
127
+ case 'ForStatement':
128
+ case 'ForInStatement':
129
+ case 'ForOfStatement':
130
+ case 'WhileStatement':
131
+ case 'DoWhileStatement':
132
+ case 'LabeledStatement':
133
+ case 'WithStatement':
134
+ return parent.body === child;
135
+ default:
136
+ return false;
137
+ }
138
+ }
139
+
51
140
  /**
52
141
  * Returns the closest native TSRX function in an ancestry path. By default,
53
142
  * function and class boundaries stop the search so callers only match direct
package/types/index.d.ts CHANGED
@@ -445,6 +445,14 @@ declare module 'estree' {
445
445
  | JSXSwitchExpression
446
446
  | JSXTryExpression;
447
447
 
448
+ /** Any node that can be the rendered output of a TSRX template or statement container. */
449
+ type TSRXRenderOutput =
450
+ | ESTreeJSX.JSXElement
451
+ | ESTreeJSX.JSXFragment
452
+ | JSXStyleElement
453
+ | JSXCodeBlock
454
+ | JSXTemplateDirective;
455
+
448
456
  interface ParenthesizedExpression extends AST.BaseNode {
449
457
  type: 'ParenthesizedExpression';
450
458
  expression: AST.Expression;
@@ -1269,6 +1277,30 @@ export interface AnalyzeOptions extends ParseOptions, Pick<CompileOptions, 'mode
1269
1277
  to_ts?: boolean;
1270
1278
  }
1271
1279
 
1280
+ /** Options for the target-neutral TSRX semantic analysis pass. */
1281
+ export interface TSRXAnalysisOptions extends ParseOptions {
1282
+ typeOnly?: boolean;
1283
+ to_ts?: boolean;
1284
+ }
1285
+
1286
+ /** Traversal state used by the target-neutral TSRX semantic analysis pass. */
1287
+ export interface TSRXAnalysisState {
1288
+ filename: string | null;
1289
+ collect: boolean;
1290
+ errors: CompileError[];
1291
+ comments: AST.CommentWithLocation[];
1292
+ function: AST.Function | null;
1293
+ function_body_is_code_block: boolean;
1294
+ inside_template_output: boolean;
1295
+ }
1296
+
1297
+ /** Result of target-neutral TSRX semantic analysis. */
1298
+ export interface TSRXAnalysisResult {
1299
+ ast: AST.Program;
1300
+ errors: CompileError[];
1301
+ comments: AST.CommentWithLocation[];
1302
+ }
1303
+
1272
1304
  /**
1273
1305
  * Result of parsing operation
1274
1306
  */