@tsrx/core 0.1.4 → 0.1.6
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 +23 -5
- package/src/diagnostics.js +1 -0
- package/src/index.js +7 -0
- package/src/plugin.js +31 -1
- package/src/runtime/index.js +110 -0
- package/src/source-map-utils.js +19 -2
- package/src/transform/jsx/ast-builders.js +120 -0
- package/src/transform/jsx/index.js +563 -1328
- package/src/transform/lazy.js +19 -60
- package/src/transform/scoping.js +9 -45
- package/src/transform/segments.js +164 -11
- package/src/utils/builders.js +51 -13
- package/types/jsx-platform.d.ts +10 -0
- package/types/runtime/index.d.ts +13 -0
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.
|
|
6
|
+
"version": "0.1.6",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -31,9 +31,16 @@
|
|
|
31
31
|
"types": "./types/runtime/ref.d.ts",
|
|
32
32
|
"default": "./src/runtime/ref.js"
|
|
33
33
|
},
|
|
34
|
+
"./runtime": {
|
|
35
|
+
"types": "./types/runtime/index.d.ts",
|
|
36
|
+
"default": "./src/runtime/index.js"
|
|
37
|
+
},
|
|
34
38
|
"./runtime/*": "./src/runtime/*.js",
|
|
35
39
|
"./test-harness/source-mappings": "./tests/shared/source-mappings.js",
|
|
36
|
-
"./test-harness/compile": "./tests/shared/compile.js"
|
|
40
|
+
"./test-harness/compile": "./tests/shared/compile.js",
|
|
41
|
+
"./test-harness/runtime/*": "./tests/shared/runtime/*.js",
|
|
42
|
+
"./test-harness/runtime/*.js": "./tests/shared/runtime/*.js",
|
|
43
|
+
"./test-harness/runtime/*.tsrx": "./tests/shared/runtime/*.tsrx"
|
|
37
44
|
},
|
|
38
45
|
"dependencies": {
|
|
39
46
|
"@jridgewell/sourcemap-codec": "^1.5.5",
|
|
@@ -42,17 +49,28 @@
|
|
|
42
49
|
"@types/estree-jsx": "^1.0.5",
|
|
43
50
|
"@types/estree": "^1.0.8",
|
|
44
51
|
"acorn": "^8.15.0",
|
|
45
|
-
"esrap": "^2.
|
|
52
|
+
"esrap": "^2.2.7",
|
|
46
53
|
"is-reference": "^3.0.3",
|
|
47
54
|
"magic-string": "^0.30.18",
|
|
48
55
|
"zimmerframe": "^1.1.2"
|
|
49
56
|
},
|
|
50
57
|
"devDependencies": {
|
|
58
|
+
"@solidjs/web": "2.0.0-beta.7",
|
|
51
59
|
"@types/node": "^24.3.0",
|
|
52
60
|
"@typescript-eslint/types": "^8.40.0",
|
|
53
|
-
"typescript": "^5.9.3",
|
|
54
61
|
"@volar/language-core": "~2.4.28",
|
|
55
|
-
"
|
|
62
|
+
"preact": "^10.27.0",
|
|
63
|
+
"react": "^19.2.0",
|
|
64
|
+
"react-dom": "^19.2.0",
|
|
65
|
+
"solid-js": "2.0.0-beta.7",
|
|
66
|
+
"typescript": "^5.9.3",
|
|
67
|
+
"vscode-languageserver-types": "^3.17.5",
|
|
68
|
+
"vue": "3.6.0-beta.10",
|
|
69
|
+
"vue-jsx-vapor": "^3.2.12",
|
|
70
|
+
"@tsrx/preact": "0.1.6",
|
|
71
|
+
"@tsrx/react": "0.2.6",
|
|
72
|
+
"@tsrx/solid": "0.1.6",
|
|
73
|
+
"@tsrx/vue": "0.1.6"
|
|
56
74
|
},
|
|
57
75
|
"files": [
|
|
58
76
|
"src",
|
package/src/diagnostics.js
CHANGED
|
@@ -4,4 +4,5 @@ export const DIAGNOSTIC_CODES = {
|
|
|
4
4
|
FUNCTION_COMPONENT_SYNTAX: 'tsrx-function-component-syntax',
|
|
5
5
|
UNCLOSED_TAG: 'tsrx-unclosed-tag',
|
|
6
6
|
MISMATCHED_CLOSING_TAG: 'tsrx-mismatched-closing-tag',
|
|
7
|
+
TEMPLATE_EXPRESSION_TRAILING_SEMICOLON: 'tsrx-template-expression-trailing-semicolon',
|
|
7
8
|
};
|
package/src/index.js
CHANGED
|
@@ -142,6 +142,9 @@ export { escape, escape_script as escapeScript } from './utils/escaping.js';
|
|
|
142
142
|
// Transform
|
|
143
143
|
export {
|
|
144
144
|
add_jsx_setup_declaration as addJsxSetupDeclaration,
|
|
145
|
+
clone_switch_helper_invocation as cloneSwitchHelperInvocation,
|
|
146
|
+
collect_param_bindings as collectParamBindings,
|
|
147
|
+
collect_statement_bindings as collectStatementBindings,
|
|
145
148
|
createJsxTransform,
|
|
146
149
|
CREATE_REF_PROP_INTERNAL_NAME,
|
|
147
150
|
extract_jsx_setup_declarations as extractJsxSetupDeclarations,
|
|
@@ -149,6 +152,7 @@ export {
|
|
|
149
152
|
MERGE_REFS_INTERNAL_NAME,
|
|
150
153
|
merge_duplicate_refs as mergeDuplicateRefs,
|
|
151
154
|
NORMALIZE_SPREAD_PROPS_INTERNAL_NAME,
|
|
155
|
+
plan_switch_lift as planSwitchLift,
|
|
152
156
|
return_value_body_to_expression as returnValueBodyToExpression,
|
|
153
157
|
rewrite_loop_continues_to_bare_returns as rewriteLoopContinuesToBareReturns,
|
|
154
158
|
to_jsx_attribute as toJsxAttribute,
|
|
@@ -165,13 +169,16 @@ export {
|
|
|
165
169
|
clone_expression_node,
|
|
166
170
|
clone_identifier,
|
|
167
171
|
clone_jsx_name,
|
|
172
|
+
contains_component_jsx,
|
|
168
173
|
create_compile_error,
|
|
169
174
|
create_generated_identifier,
|
|
170
175
|
create_null_literal,
|
|
176
|
+
expand_switch_cases_for_fallthrough,
|
|
171
177
|
flatten_switch_consequent,
|
|
172
178
|
get_for_of_iteration_params,
|
|
173
179
|
identifier_to_jsx_name,
|
|
174
180
|
is_bare_render_expression,
|
|
181
|
+
is_component_jsx_name,
|
|
175
182
|
is_dynamic_element_id,
|
|
176
183
|
is_jsx_child,
|
|
177
184
|
set_loc,
|
package/src/plugin.js
CHANGED
|
@@ -224,6 +224,7 @@ export function TSRXPlugin(config) {
|
|
|
224
224
|
#filename = null;
|
|
225
225
|
#componentDepth = 0;
|
|
226
226
|
#functionBodyDepth = 0;
|
|
227
|
+
#allowExpressionContainerTrailingSemicolon = false;
|
|
227
228
|
|
|
228
229
|
/**
|
|
229
230
|
* @type {Parse.Parser['finishNode']}
|
|
@@ -352,7 +353,14 @@ export function TSRXPlugin(config) {
|
|
|
352
353
|
}
|
|
353
354
|
|
|
354
355
|
#parseNativeTemplateExpressionContainer() {
|
|
355
|
-
const
|
|
356
|
+
const allow_trailing_semicolon = this.#allowExpressionContainerTrailingSemicolon;
|
|
357
|
+
this.#allowExpressionContainerTrailingSemicolon = true;
|
|
358
|
+
let node;
|
|
359
|
+
try {
|
|
360
|
+
node = this.jsx_parseExpressionContainer();
|
|
361
|
+
} finally {
|
|
362
|
+
this.#allowExpressionContainerTrailingSemicolon = allow_trailing_semicolon;
|
|
363
|
+
}
|
|
356
364
|
// Keep JSXEmptyExpression as-is (for prettier to handle comments)
|
|
357
365
|
// but convert other expressions to native TSRX child nodes.
|
|
358
366
|
if (node.expression.type !== 'JSXEmptyExpression') {
|
|
@@ -644,6 +652,18 @@ export function TSRXPlugin(config) {
|
|
|
644
652
|
ctx.length = ci - 1;
|
|
645
653
|
return;
|
|
646
654
|
}
|
|
655
|
+
// Statement-bodied `<tsrx>` attributes can leave the attribute's
|
|
656
|
+
// expression contexts above the still-open JSX tag context. Strip
|
|
657
|
+
// those so a following `/>` stays in JSX opening-tag mode.
|
|
658
|
+
if (
|
|
659
|
+
this.type === tt.braceR &&
|
|
660
|
+
top === tstc.tc_expr &&
|
|
661
|
+
second === b_expr &&
|
|
662
|
+
ctx[ci - 2] === tstc.tc_oTag
|
|
663
|
+
) {
|
|
664
|
+
ctx.length = ci - 1;
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
647
667
|
// Closing token after the template at expression position. For `}`
|
|
648
668
|
// only pop if it actually closes this `b_expr` — otherwise the
|
|
649
669
|
// brace targets an inner callback/object body that should pop it
|
|
@@ -1771,6 +1791,16 @@ export function TSRXPlugin(config) {
|
|
|
1771
1791
|
'"style" is a TSRX keyword and must be used in the form {style "class_name"}',
|
|
1772
1792
|
);
|
|
1773
1793
|
}
|
|
1794
|
+
if (this.#allowExpressionContainerTrailingSemicolon && this.type === tt.semi) {
|
|
1795
|
+
if (this.#collect) {
|
|
1796
|
+
this.#report_recoverable_error(
|
|
1797
|
+
this.start,
|
|
1798
|
+
'TSRX expression containers do not use semicolons. Remove this semicolon.',
|
|
1799
|
+
DIAGNOSTIC_CODES.TEMPLATE_EXPRESSION_TRAILING_SEMICOLON,
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
this.next();
|
|
1803
|
+
}
|
|
1774
1804
|
this.expect(tt.braceR);
|
|
1775
1805
|
|
|
1776
1806
|
return this.finishNode(node, 'JSXExpressionContainer');
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @template T
|
|
3
|
+
* @template U
|
|
4
|
+
* @param {Iterable<T> | Iterator<T>} iterable
|
|
5
|
+
* @param {(item: T, index: number, is_last: boolean) => U} fn
|
|
6
|
+
* @param {() => U | U[]} [tail]
|
|
7
|
+
* @returns {U[]}
|
|
8
|
+
*/
|
|
9
|
+
export function map_iterable(iterable, fn, tail) {
|
|
10
|
+
if (Array.isArray(iterable)) {
|
|
11
|
+
return map_array(iterable, fn, tail);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @type {Iterator<T>} */
|
|
15
|
+
var iterator;
|
|
16
|
+
var iterable_prop = /** @type {Iterable<T>} */ (iterable)[Symbol.iterator];
|
|
17
|
+
|
|
18
|
+
if (typeof iterable_prop === 'function') {
|
|
19
|
+
iterator = iterable_prop.call(iterable);
|
|
20
|
+
} else if (typeof (/** @type {Iterator<T>} */ (iterable).next) === 'function') {
|
|
21
|
+
iterator = Iterator.from(iterable);
|
|
22
|
+
} else {
|
|
23
|
+
throw new TypeError('The loop target has to be an Iterable');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var current = iterator.next();
|
|
27
|
+
if (current.done) {
|
|
28
|
+
if (!tail) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
var tail_value = tail();
|
|
32
|
+
if (Array.isArray(tail_value)) {
|
|
33
|
+
return tail_value;
|
|
34
|
+
}
|
|
35
|
+
return [tail_value];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
var index = 0;
|
|
39
|
+
var result = [];
|
|
40
|
+
while (true) {
|
|
41
|
+
var next = iterator.next();
|
|
42
|
+
var value = fn(current.value, index++, !!next.done);
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
for (var j = 0; j < value.length; j++) {
|
|
45
|
+
result.push(value[j]);
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
result.push(value);
|
|
49
|
+
}
|
|
50
|
+
if (next.done) {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
current = next;
|
|
54
|
+
}
|
|
55
|
+
if (tail) {
|
|
56
|
+
var tail_value = tail();
|
|
57
|
+
if (Array.isArray(tail_value)) {
|
|
58
|
+
for (var j = 0; j < tail_value.length; j++) {
|
|
59
|
+
result.push(tail_value[j]);
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
result.push(tail_value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @template T
|
|
70
|
+
* @template U
|
|
71
|
+
* @param {Array<T>} array
|
|
72
|
+
* @param {(item: T, index: number, is_last: boolean) => U} fn
|
|
73
|
+
* @param {() => U | U[]} [tail]
|
|
74
|
+
* @returns {U[]}
|
|
75
|
+
*/
|
|
76
|
+
function map_array(array, fn, tail) {
|
|
77
|
+
var length = array.length;
|
|
78
|
+
if (length === 0) {
|
|
79
|
+
if (!tail) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
var tail_value = tail();
|
|
83
|
+
if (Array.isArray(tail_value)) {
|
|
84
|
+
return tail_value;
|
|
85
|
+
}
|
|
86
|
+
return [tail_value];
|
|
87
|
+
}
|
|
88
|
+
var result = [];
|
|
89
|
+
for (var i = 0; i < length; i++) {
|
|
90
|
+
var value = fn(array[i], i, i === length - 1);
|
|
91
|
+
if (Array.isArray(value)) {
|
|
92
|
+
for (var j = 0; j < value.length; j++) {
|
|
93
|
+
result.push(value[j]);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
result.push(value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (tail) {
|
|
100
|
+
var tail_value = tail();
|
|
101
|
+
if (Array.isArray(tail_value)) {
|
|
102
|
+
for (var j = 0; j < tail_value.length; j++) {
|
|
103
|
+
result.push(tail_value[j]);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
result.push(tail_value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
package/src/source-map-utils.js
CHANGED
|
@@ -17,10 +17,15 @@
|
|
|
17
17
|
* css?: AST.Element['metadata']['css']
|
|
18
18
|
* },
|
|
19
19
|
* }} CodePosition
|
|
20
|
+
* @typedef {{
|
|
21
|
+
* column: number,
|
|
22
|
+
* position: CodePosition,
|
|
23
|
+
* }} SourceLineGeneratedPosition
|
|
20
24
|
*/
|
|
21
25
|
|
|
22
26
|
/** @typedef {Map<string, CodePosition[]>} CodeToGeneratedMap */
|
|
23
27
|
/** @typedef {Map<string, {line: number, column: number}[]>} GeneratedToSourceMap */
|
|
28
|
+
/** @typedef {Map<number, SourceLineGeneratedPosition[]>} SourceLineGeneratedMap */
|
|
24
29
|
|
|
25
30
|
import { decode } from '@jridgewell/sourcemap-codec';
|
|
26
31
|
|
|
@@ -83,18 +88,22 @@ export const offset_to_line_col = (offset, line_offsets) => {
|
|
|
83
88
|
* @param {PostProcessingChanges} post_processing_changes - Optional post-processing changes to apply
|
|
84
89
|
* @param {LineOffsets} line_offsets - Pre-computed line offsets array
|
|
85
90
|
* @param {string} generated_code - The final generated code (after post-processing)
|
|
86
|
-
* @
|
|
91
|
+
* @param {boolean} [include_source_line_generated_map] - Whether to build the optional source-line predecessor lookup
|
|
92
|
+
* @returns {[CodeToGeneratedMap, GeneratedToSourceMap, SourceLineGeneratedMap | null]} Tuple of [source-to-generated map, generated-to-source map, source-line generated map]
|
|
87
93
|
*/
|
|
88
94
|
export function build_src_to_gen_map(
|
|
89
95
|
source_map,
|
|
90
96
|
post_processing_changes,
|
|
91
97
|
line_offsets,
|
|
92
98
|
generated_code,
|
|
99
|
+
include_source_line_generated_map = false,
|
|
93
100
|
) {
|
|
94
101
|
/** @type {CodeToGeneratedMap} */
|
|
95
102
|
const map = new Map();
|
|
96
103
|
/** @type {GeneratedToSourceMap} */
|
|
97
104
|
const reverse_map = new Map();
|
|
105
|
+
/** @type {SourceLineGeneratedMap | null} */
|
|
106
|
+
const source_line_generated_map = include_source_line_generated_map ? new Map() : null;
|
|
98
107
|
|
|
99
108
|
// Decode the VLQ-encoded mappings string
|
|
100
109
|
const decoded = decode(source_map.mappings);
|
|
@@ -192,6 +201,14 @@ export function build_src_to_gen_map(
|
|
|
192
201
|
map.set(key, []);
|
|
193
202
|
}
|
|
194
203
|
/** @type {CodePosition[]} */ (map.get(key)).push(gen_pos);
|
|
204
|
+
if (source_line_generated_map) {
|
|
205
|
+
if (!source_line_generated_map.has(segment.sourceLine)) {
|
|
206
|
+
source_line_generated_map.set(segment.sourceLine, []);
|
|
207
|
+
}
|
|
208
|
+
/** @type {SourceLineGeneratedPosition[]} */ (
|
|
209
|
+
source_line_generated_map.get(segment.sourceLine)
|
|
210
|
+
).push({ column: segment.sourceColumn, position: gen_pos });
|
|
211
|
+
}
|
|
195
212
|
|
|
196
213
|
// Store reverse mapping (generated to source)
|
|
197
214
|
const gen_key = `${gen_pos.line}:${gen_pos.column}`;
|
|
@@ -206,7 +223,7 @@ export function build_src_to_gen_map(
|
|
|
206
223
|
}
|
|
207
224
|
}
|
|
208
225
|
|
|
209
|
-
return [map, reverse_map];
|
|
226
|
+
return [map, reverse_map, source_line_generated_map];
|
|
210
227
|
}
|
|
211
228
|
|
|
212
229
|
/**
|
|
@@ -162,6 +162,74 @@ export function identifier_to_jsx_name(id) {
|
|
|
162
162
|
return id;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
/**
|
|
166
|
+
* A JSX tag name refers to a *component* (rather than a host/DOM tag) iff:
|
|
167
|
+
* - it's a `JSXIdentifier` whose first character is uppercase (the convention
|
|
168
|
+
* every framework's JSX runtime keys off — `<div>` is a host element,
|
|
169
|
+
* `<Foo>` is a component), or
|
|
170
|
+
* - it's a `JSXMemberExpression` (e.g. `<Icons.Button />`).
|
|
171
|
+
*
|
|
172
|
+
* Used by platforms that veto static-hoisting of component JSX (Vue, Solid)
|
|
173
|
+
* and by core's narrower bare-component-invocation predicate.
|
|
174
|
+
*
|
|
175
|
+
* @param {any} name
|
|
176
|
+
* @returns {boolean}
|
|
177
|
+
*/
|
|
178
|
+
export function is_component_jsx_name(name) {
|
|
179
|
+
if (!name || typeof name !== 'object') {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (name.type === 'JSXIdentifier') {
|
|
184
|
+
const first = name.name?.[0];
|
|
185
|
+
return first != null && first >= 'A' && first <= 'Z';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (name.type === 'JSXMemberExpression') {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Does this JSX subtree contain any component-shaped element (anywhere —
|
|
197
|
+
* including nested under host elements or inside expression containers)?
|
|
198
|
+
* Vue and Solid use this as their `canHoistStaticNode` predicate: hoisting a
|
|
199
|
+
* subtree that invokes a component into a module-level constant pins that
|
|
200
|
+
* component instance to module identity, which doesn't help either framework
|
|
201
|
+
* the way it helps React, so it's wasted output.
|
|
202
|
+
*
|
|
203
|
+
* @param {any} node
|
|
204
|
+
* @returns {boolean}
|
|
205
|
+
*/
|
|
206
|
+
export function contains_component_jsx(node) {
|
|
207
|
+
if (!node || typeof node !== 'object') {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (node.type === 'JSXElement') {
|
|
212
|
+
if (is_component_jsx_name(node.openingElement?.name)) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
return node.children?.some(contains_component_jsx) ?? false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (node.type === 'JSXFragment') {
|
|
219
|
+
return node.children?.some(contains_component_jsx) ?? false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (node.type === 'JSXExpressionContainer') {
|
|
223
|
+
return contains_component_jsx(node.expression);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (Array.isArray(node)) {
|
|
227
|
+
return node.some(contains_component_jsx);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
165
233
|
/**
|
|
166
234
|
* @param {any} node
|
|
167
235
|
* @returns {boolean}
|
|
@@ -302,6 +370,58 @@ export function flatten_switch_consequent(consequent) {
|
|
|
302
370
|
return result;
|
|
303
371
|
}
|
|
304
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Compute fall-through expansions for each `case` in a `switch`. JavaScript
|
|
375
|
+
* `switch` semantics say that once a case body executes, execution continues
|
|
376
|
+
* into the bodies of subsequent cases until a `break` or terminal `return` is
|
|
377
|
+
* hit. We pre-compute, per case, the flat list of statements that should run
|
|
378
|
+
* when that case is the entry point — so downstream targets (which render each
|
|
379
|
+
* case independently rather than executing fall-through at runtime) still
|
|
380
|
+
* produce the right output.
|
|
381
|
+
*
|
|
382
|
+
* Walking right-to-left lets each case reuse the next case's already-expanded
|
|
383
|
+
* tail without recomputation. Downstream nodes are deep-cloned when absorbed
|
|
384
|
+
* so each case's expanded body owns its own AST subtree.
|
|
385
|
+
*
|
|
386
|
+
* @param {any[]} cases
|
|
387
|
+
* @returns {Array<{ test: any, body: any[], source: any }>}
|
|
388
|
+
*/
|
|
389
|
+
export function expand_switch_cases_for_fallthrough(cases) {
|
|
390
|
+
/** @type {Array<{ test: any, body: any[], source: any }>} */
|
|
391
|
+
const expanded = new Array(cases.length);
|
|
392
|
+
for (let i = cases.length - 1; i >= 0; i--) {
|
|
393
|
+
const consequent = flatten_switch_consequent(cases[i].consequent || []);
|
|
394
|
+
const body = [];
|
|
395
|
+
let has_terminal = false;
|
|
396
|
+
for (const child of consequent) {
|
|
397
|
+
if (child.type === 'BreakStatement') {
|
|
398
|
+
has_terminal = true;
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
body.push(child);
|
|
402
|
+
if (child.type === 'ReturnStatement') {
|
|
403
|
+
has_terminal = true;
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// Strip locations from cloned downstream nodes. Only the original case
|
|
408
|
+
// (one entry up the chain) keeps `loc`/`start`/`end`; clones inlined
|
|
409
|
+
// into upstream cases would otherwise point editor IntelliSense at the
|
|
410
|
+
// same source range multiple times (one hover/go-to-definition per
|
|
411
|
+
// fall-through entry point), producing double/triple results in Volar.
|
|
412
|
+
const downstream =
|
|
413
|
+
!has_terminal && i + 1 < cases.length
|
|
414
|
+
? expanded[i + 1].body.map((n) => clone_expression_node(n, false))
|
|
415
|
+
: [];
|
|
416
|
+
expanded[i] = {
|
|
417
|
+
test: cases[i].test,
|
|
418
|
+
body: [...body, ...downstream],
|
|
419
|
+
source: cases[i],
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
return expanded;
|
|
423
|
+
}
|
|
424
|
+
|
|
305
425
|
/**
|
|
306
426
|
* @param {AST.Expression | null | undefined} expression
|
|
307
427
|
* @returns {boolean}
|