@tsrx/core 0.1.45 → 0.1.47

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.45",
6
+ "version": "0.1.47",
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,19 @@ 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,
92
+ has_location,
91
93
  is_inside_component as isInsideComponent,
94
+ is_template_directive as isTemplateDirective,
95
+ is_tsrx_render_output_node as isTsrxRenderOutputNode,
96
+ is_code_block_function_body as isCodeBlockFunctionBody,
97
+ is_statement_position as isStatementPosition,
92
98
  } from './utils/ast.js';
93
99
 
100
+ // Shared TSRX semantic analysis
101
+ export { analyze_tsrx as analyzeTsrx } from './analyze/index.js';
102
+
94
103
  // Builders (namespace re-export — members mirror AST node kinds)
95
104
  export * as builders from './utils/builders.js';
96
105
 
@@ -159,7 +168,6 @@ export {
159
168
  plan_switch_lift as planSwitchLift,
160
169
  return_value_body_to_expression as returnValueBodyToExpression,
161
170
  rewrite_loop_continues_to_bare_returns as rewriteLoopContinuesToBareReturns,
162
- to_jsx_attribute as toJsxAttribute,
163
171
  validate_at_most_one_ref_attribute as validateAtMostOneRefAttribute,
164
172
  wrap_edge_whitespace as wrapEdgeWhitespace,
165
173
  } from './transform/jsx/index.js';
@@ -181,7 +189,7 @@ export {
181
189
  } from './transform/style-ref.js';
182
190
  export {
183
191
  add_extra_source_mappings_from_matching_expression,
184
- clone_expression_node,
192
+ clone_ast_node,
185
193
  clone_identifier,
186
194
  clone_jsx_name,
187
195
  contains_component_jsx,
@@ -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';
@@ -8,6 +8,7 @@
8
8
  import * as acorn from 'acorn';
9
9
  import { tsPlugin } from '@sveltejs/acorn-typescript';
10
10
  import { walk } from 'zimmerframe';
11
+ import { has_location } from '../utils/ast.js';
11
12
 
12
13
  /**
13
14
  * @typedef {(BaseParser: typeof acorn.Parser) => typeof acorn.Parser} AcornPlugin
@@ -607,8 +608,8 @@ export function get_comment_handlers(source, comments, index = 0) {
607
608
  }
608
609
 
609
610
  const ancestorElements = path
610
- .filter((ancestor) => isNativeTemplateNode(ancestor) && ancestor.loc)
611
- .map((ancestor) => /** @type {AST.NodeWithLocation} */ (ancestor))
611
+ .filter(has_location)
612
+ .filter(isNativeTemplateNode)
612
613
  .sort((a, b) => a.loc.start.line - b.loc.start.line);
613
614
 
614
615
  const targetAncestor = ancestorElements.find(
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
  }