bynk_ts/printer.rs
1//! The printer — `TsProgram -> Artefacts` (R7.3: "printing is `TsProgram ->
2//! Artefacts`. The printer owns the buffer, the indentation and the offset
3//! arithmetic. It is the only code in the compiler that writes a
4//! character."). Owns one buffer for the whole [`TsProgram`], so unlike
5//! `bynk-emit`'s own splice-based lowering (`crate::source_map`'s own module
6//! doc), it never needs to rebase a sub-buffer's checkpoints — it records
7//! directly from each statement's own `span` as it writes, which is what
8//! makes R7.4 ("the source map is produced by the printer from
9//! `TsNode.span`. No phase before the printer records an offset") true by
10//! construction for this path, from this slice.
11//!
12//! # Readability policy (R7.5)
13//!
14//! R7.5: "Readable output is a printer policy with a name and a test, not a
15//! property of how carefully strings were typed." P7.7 named the policy's
16//! then-whole surface (statement separation); P7.8 (#1313) extends it now
17//! that the printer has real structured nodes to have an opinion about
18//! (`TsExpr`/`TsType`/`TsDecl`, plus real `TsStmt` variants):
19//!
20//! - **Every statement starts on its own generated line, so two statements
21//! can never share one.** Unchanged from P7.7 — see [`print()`]'s own
22//! loop. Still true for the new node kinds: every one of their own
23//! renderers ends its own output in `\n`.
24//! - **Two-space indentation, one level per nesting depth.** The only
25//! indentation width this printer produces — chosen because it's the one
26//! `events_fanout.rs` (this slice's own grounding file) already uses, not
27//! because of any earlier general readability decision; matches the
28//! depth threaded through [`render_stmt`]/[`render_block_body`] exactly.
29//! - **One blank line separates top-level declarations, except two
30//! consecutive `import`s or two consecutive `Comment`s.** [`print()`]'s
31//! own loop implements this by peeking at the next statement — matching
32//! `events_fanout.rs`'s own real spacing (its two `import` lines sit
33//! adjacent, and so do its own two header-comment lines; every other pair
34//! of top-level declarations has a blank line between). Added for Arc C's
35//! own first real conversion slice (#1317): every `bynk-emit`-generated
36//! file opens with a multi-line header banner, one [`TsStmtKind::Comment`]
37//! per real line.
38//! - **[`TsStmt::no_blank_before`] suppresses that same blank immediately
39//! before the statement carrying it**, composing with every rule above
40//! rather than replacing any of them — a property of the *statement*, not
41//! of a pair, so it stays correct however many different kinds end up
42//! adjacent to it. Scoped to exactly [`print()`]'s own top-level loop: a
43//! statement carrying it inside a `Block`/`InlineBlock`/function body, or
44//! printed through [`print_stmt`] / `bynk-emit`'s own `extend_printed`
45//! splicing helpers, is silently ignored there today — harmless only
46//! because none of those paths inserts an automatic blank of its own in
47//! the first place. Added for #1486: `emit_agent`'s own already-shipped
48//! registry-`const`-then-zero-factory pairing needs zero blank lines
49//! between two adjacent real top-level statements, a shape no existing
50//! pair rule above can express without a `TsDecl`-pair special case.
51//! - **Inside a `class`, no blank line between fields and the constructor;
52//! one blank line before each method.** [`render_decl_body`]'s own
53//! `TsDecl::Class` arm — again, this is what `events_fanout.rs` itself
54//! does, not a general rule derived some other way.
55//! - **`if`/`for...of`'s own body prints with braces when it's a
56//! `TsStmtKind::Block`, and inline on the same line otherwise** —
57//! `if (!Array.isArray(subs)) continue;` has no braces in the grounding
58//! file; a `for...of` always does. See [`render_branch`].
59//! - **A statement's own interior is not the printer's concern for
60//! `Verbatim` content — still.** `Verbatim` text renders exactly as
61//! written (P7.7's own boundary, unchanged); everything above applies
62//! only to the new, real node kinds this slice adds.
63//! - **A shorthand async method entry in an object literal
64//! (`TsObjectEntry::Method`) prints exactly like a class method** —
65//! `async name(params) { <body> },` with a trailing comma, body indented
66//! one level deeper than the entry itself, closing `},` back at the
67//! entry's own indent. Added for Arc C's own third slice (#1321,
68//! `workers.rs`): the `compose`-returned surface object's dominant real
69//! shape, one entry per wrapper.
70//! - **`?.`/`?.[...]` print with no surrounding spaces** — `object?.property`/
71//! `object?.[index]`, matching plain `Member`/`Index`'s own spacing
72//! exactly, just with the extra `?`. Added for #1321: `workers.rs`'s own
73//! secret-probe idiom.
74//! - **An optional field in a type-position object literal prints `key?:
75//! ty`** — the direct type-side counterpart to `TsParam.optional`'s
76//! already-established `name?: ty` for parameters. Added for #1321.
77//! - **An arrow function prints `(params) => body` on one line, expression
78//! body only** — no block-body form exists yet (nothing built here needs
79//! one). Added for #1321.
80//! - **A nested `Binary` operand of another `Binary` parenthesizes only
81//! when the inner operator's precedence is not strictly higher than the
82//! outer's, or when the same operator nests in itself and that operator
83//! is `||`/`&&`** (`render_binary_operand`) — every other operand
84//! position (`Member`/`Index`/`Call`/`New`/`Await`/`Unary`,
85//! `render_operand`) still always parenthesizes a nested `Binary`/`As`/
86//! `Arrow`/`Conditional`, unchanged. Added for #1321: `workers.rs`'s own
87//! `__authz === null || !__authz.startsWith(...)` has no parens around
88//! the comparison in the byte-golden fixtures, and `??` mixed with
89//! `||`/`&&` still always parenthesizes (TS forbids that combination
90//! unparenthesized). Extended for #1323: a 3+-term `||`/`&&` chain of the
91//! *same* operator (`typeof args !== "object" || args === null ||
92//! Array.isArray(args)`) prints flat, with no parens around the nested
93//! pair — `??` chained with itself keeps the pre-#1321 conservative
94//! choice (still parenthesized), since nothing real needs it flattened.
95//! - **`switch (<discriminant>) { <cases> }` — a non-`default` `case` is
96//! always `{ }`-blocked; a `default` case never is.** Added for #1323:
97//! `workers_entry.rs`'s four real `switch` statements are the first real
98//! content this tree represents with a genuinely new statement-grouping
99//! construct, not a single expression/type variant.
100//! - **`export default <expr>;` prints the expression through the same
101//! depth-aware multiline-object handling `const`/`let`/`return`/`Assign`
102//! already get.** Added for #1323: `workers_entry.rs`'s own top-level
103//! shape is a default-exported object literal.
104//! - **`export { a, b } from "spec";` — a re-export — prints with no blank
105//! line rule of its own** (it is not classified alongside `import`/
106//! `import * as` for the "no blank line between adjacent imports"
107//! exception, so the ordinary "one blank line between top-level
108//! declarations" default applies both before and after it). Added for
109//! #1323: `workers_entry.rs`'s own agent/fan-out-DO re-exports, each
110//! already separated from its neighbours by a blank line in the
111//! pre-conversion output.
112//! - **`test ? consequent : alternate` prints on one line, with `test`
113//! parenthesized only when it is itself an `Arrow` or a `Conditional`.**
114//! Added for #1323: `workers_entry.rs`'s own two real ternaries.
115//! - **A `readonly`/method-signature/index-signature member of a
116//! type-position object literal prints `readonly name: ty` /
117//! `name(params): ret` / `[key: key_ty]: value_ty`.** Added for #1323:
118//! `workers_entry.rs`'s own `scheduled`/`queue` parameter types and its
119//! multi-param `on call` dispatch's `args as { [k: string]: JsonValue }`.
120//! - **An explicit `Paren` always prints its own literal parentheses,
121//! independent of the wrapped expression's own precedence.** Added for
122//! #1323: `workers_entry.rs`'s CORS-preflight guard condition, which the
123//! pre-conversion code always wrapped in `(...)` even when nothing about
124//! the wrapped expression's own precedence required it.
125//! - **A bare blank line (`Blank`) prints exactly one empty line**, usable
126//! at any nesting depth — distinct from `print()`'s own top-level-only
127//! blank-line policy above. Added for #1323: three specific points inside
128//! `workers_entry.rs`'s `fetch` method body.
129//! - **An `if`'s own `else` prints on its own fresh line, at the `if`'s own
130//! indent, then follows the same block-vs-inline branch rule the `if`
131//! itself does.** Added for #1323: `workers_entry.rs`'s queue-consumer
132//! ack/retry dispatch.
133//! - **`InlineBlock` prints `{ stmt; stmt; ... }` on one generated line**,
134//! distinct from `Block`'s always-multi-line form — reachable only as an
135//! `if`/`else` branch in real content today. Added for #1323:
136//! `workers_entry.rs`'s own two real sites (a deserialise-failure guard,
137//! and the ack/retry dispatch's `else`), both hand-written as compact
138//! one-liners in the pre-conversion text.
139//! - **An array literal supports the same `multiline` shape an object
140//! literal does** — one item per line, each with its own trailing comma,
141//! closing `]` at the statement's own indent, only through
142//! `render_stmt_level_expr`. Added for #1325: `emit_test_main`'s own
143//! `modules` array.
144//! - **A template literal's static parts print verbatim, with no escaping of
145//! their own.** Added for #1325 — see `TsExpr::TemplateLit`'s own doc for
146//! why (a generic escaper would double an already-pre-formed JS unicode
147//! escape's own literal backslash).
148//! - **`declare const name: ty;` prints with no initialiser at all** —
149//! distinct from every other `const` form, which always has one. Added
150//! for #1325: `emit_test_main`'s own ambient `process` declaration.
151//! - **An `async function` prints `async function name(...)`** — the
152//! top-level sibling to `TsObjectEntry::Method`'s own `is_async` handling.
153//! Added for #1325: `emit_test_main`'s own top-level `main`.
154//! - **`<expr>++;` prints a bare postfix increment as a whole statement.**
155//! Added for #1325: `emit_test_main`'s own `passed++;`/`failed++;`
156//! counters.
157//! - **An `if`'s own `else` prints `} else {` on the same physical line when
158//! `same_line_else` is set** (only reachable when `then_branch` is a
159//! `Block` or `InlineBlock`) — a second, real convention alongside the
160//! fresh-line default just above, not a replacement for it. Added for
161//! #1325: all three of `emit_test_main`'s own real `if`/`else` sites use
162//! this spacing, none use the fresh-line form `workers_entry.rs`'s own
163//! real content needs — two already-real files disagreeing on the same
164//! construct, the same tension the `Await`-under-`As` correction
165//! (#1323/#1324) found for parenthesisation.
166//! - **`export * from "spec";` — a wildcard re-export — groups with itself
167//! AND with an immediately-preceding header `Comment`, no blank line
168//! either way.** A different spacing shape from `ReExport`'s own (no
169//! grouping rule, see above): `emit_commons_barrel`'s own real barrel
170//! module is one header comment followed by one `export *` line per
171//! constituent source file, every one of those lines adjacent with no
172//! blank line anywhere. Added for #1329.
173//! - **A `TsStmtKind::DocComment` prints a JSDoc block comment**, one
174//! ` * <line>` per non-blank source line, a blank source line as a bare
175//! ` *`. Distinct from `Comment`'s own `//`-per-line form, and printed
176//! only through [`print_stmt`] (`bynk-emit`'s own `emit_doc_block`, a
177//! shared helper spliced into still-unconverted callers' buffers), never
178//! through [`print()`]'s own `TsProgram` loop — so this shape has no
179//! blank-line grouping rule of its own to name here. Added for #1333.
180//! - **A `TsStmtKind::Raw` prints its own text verbatim** — no leading
181//! indent, no added punctuation, the same rendering `Verbatim` gets
182//! (deliberately a distinct kind, not a reuse of it — see
183//! [`crate::program::TsStmtKind::Raw`]'s own doc for why). Never reached
184//! through [`print()`]'s own `TsProgram` loop — no blank-line grouping
185//! rule of its own to name here either. Added for #1337:
186//! `emit_method`'s own body, delegated wholesale to `emitter/lower.rs`'s
187//! `emit_block_as_function_body_with_return` — a permanent Arc C
188//! exclusion (ADR `arc-c-lower-rs-permanent-exclusion`), not residue.
189//! #1339 added a second, differently-reasoned real use:
190//! `emit_refined_type`'s own `of()` guard body, carrying `emit_refined_
191//! checks`'s already-printed output — not a permanent exclusion, just
192//! scope that function's own conversion didn't reach. Both uses carry
193//! text pre-indented at a fixed absolute depth by their own caller, so
194//! `render_multiline_object_entry`'s own `debug_assert!` guards that this
195//! only renders correctly at `depth == 0` — see its doc and
196//! [`print_object_entry`]'s. **Reached through more than a
197//! `TsObjectEntry::Method`'s own `body` now** (review of #1370 caught
198//! this doc drifting stale): `emit_free_fn` (#1352) and `emit_agent`'s
199//! own rehydrate function (#1369) both hold `Raw` statements inside an
200//! ordinary `TsDecl::Function`'s own `body`, rendered through
201//! `render_block_stmts` at whatever `depth` the enclosing `print_stmt`
202//! call used (always 0 for a top-level declaration in every real site
203//! today, satisfying the same depth-0 assumption named above). Since
204//! #1369 also added `TsDecl::Function.inline`, a `Raw`-bearing function
205//! body reached with `inline: true` would route through
206//! `render_inline_block`/`render_compact_stmts` instead — see
207//! [`render_inline_stmt`]'s own `Raw` arm for why that combination stays
208//! deliberately unbuilt.
209//!
210//! None of the above is claimed as *the* TypeScript style this printer will
211//! use forever — it's what this slice's own grounding file needs, named
212//! rather than left implicit, the same posture the node algebra itself
213//! takes (`program.rs`'s own module doc). A future file with a construct
214//! this policy doesn't cover yet (an `else` branch, a multi-field class with
215//! blank lines between fields, …) extends it the same way `program.rs`'s
216//! own node list grows: file by file, against real content.
217
218use crate::program::{
219 TsArrowBody, TsBinaryOp, TsBindingName, TsClassMethod, TsDecl, TsExpr, TsLit, TsObjectEntry,
220 TsParam, TsProgram, TsStmt, TsStmtKind, TsType, TsTypeMember, TsUnaryOp,
221};
222use crate::source_map::SourceMapBuilder;
223
224/// The result of printing a [`TsProgram`]: the emitted text, and its source
225/// map — `None` when no checkpoint resolved, either because no statement
226/// carried a span or every span fell outside `source_text`
227/// ([`SourceMapBuilder::to_v3`]'s own "nothing resolves" case).
228#[derive(Debug)]
229pub struct Printed {
230 pub text: String,
231 pub source_map: Option<String>,
232}
233
234/// Print `program` to TypeScript text. `source_name`/`source_text` register
235/// the `.bynk` source every statement's own span is measured against —
236/// today always exactly one, since nothing spans two files yet;
237/// `output_file` names the generated file in the source map's own `file`
238/// field. Only a *top-level* statement's own span is recorded as a
239/// checkpoint (R7.4's existing scope, unchanged by P7.8 — see
240/// [`TsStmt::span`]'s own doc for why a nested statement's span isn't
241/// recorded yet).
242pub fn print(
243 program: &TsProgram,
244 source_name: &str,
245 source_text: &str,
246 output_file: &str,
247) -> Printed {
248 let mut map = SourceMapBuilder::new();
249 map.add_source(source_name, source_text);
250 print_with_map(program, map, output_file)
251}
252
253/// #1486's own multi-source sibling of [`print()`]: a test/integration
254/// module's own aggregate map spans one source per fragment file a case's
255/// body was spliced in from (`emit_test_module`/`emit_integration_module`,
256/// `project/tests_emit.rs`), not the single `.bynk` file `print`'s own
257/// signature assumes — see [`TsStmt::nested_map_source_id`]'s own doc for
258/// why a per-statement source selector needs a caller that can register
259/// more than one source in the first place. Takes the caller's own already-
260/// built `map` directly (rather than a `(name, text)` list built fresh
261/// here) so a caller can keep registering sources incrementally as it
262/// discovers them — `emit_test_module`'s own per-case loop, one `add_source`
263/// call per case, exactly as it already did before this function existed —
264/// and get each call's own returned id back immediately, to set on that
265/// case's own top-level statement's `nested_map_source_id`, instead of
266/// having to pre-compute the full source list up front. Every statement's
267/// own `nested_map_source_id` selects which of `map`'s already-registered
268/// sources (by id) its own `nested_map` merges against; a statement with no
269/// `span` of its own (this whole module's own shape — an aggregate with no
270/// single "primary" source of its own) never records a top-level
271/// checkpoint, so `map`'s own id-`0` source (if any) needs no special
272/// "primary" meaning here the way [`print()`]'s single source does.
273pub fn print_multi_source(
274 program: &TsProgram,
275 map: SourceMapBuilder,
276 output_file: &str,
277) -> Printed {
278 print_with_map(program, map, output_file)
279}
280
281fn print_with_map(program: &TsProgram, mut map: SourceMapBuilder, output_file: &str) -> Printed {
282 let mut out = String::new();
283 for (i, stmt) in program.stmts.iter().enumerate() {
284 if let Some(span) = stmt.span {
285 map.record(out.len(), span);
286 }
287 // #1477: also merges any nested checkpoints this top-level
288 // statement (or anything nested inside it) itself carries — the
289 // same mechanism `print_stmt_and_merge`/`print_class_method_and_
290 // merge` expose to a caller printing one fragment at a time,
291 // applied here automatically for every top-level statement in a
292 // real `TsProgram`. `source_id` defaults to `stmt.
293 // nested_map_source_id`'s own default of `0`, matching `record`'s
294 // own unconditional targeting of the primary source just above for
295 // every single-source module; #1486's own multi-source case
296 // (`nested_map_source_id`'s own doc comment) sets it per statement.
297 render_stmt(
298 &mut out,
299 stmt,
300 0,
301 Some(MergeTarget {
302 map: &mut map,
303 source_id: stmt.nested_map_source_id,
304 }),
305 );
306 // The printer owns line structure (R7.3), so a statement's own text
307 // not ending in its own newline can't leave two statements sharing
308 // a generated line — review of #1308, finding 2: nothing required
309 // `Verbatim` text to be newline-terminated, and two that weren't
310 // would jam onto one line *and* silently lose the earlier
311 // statement's own checkpoint (`SourceMapBuilder::record`'s
312 // same-offset dedup, and `to_v3`'s one-checkpoint-per-line forward
313 // pass, both keep only the later one). Every real (non-`Verbatim`)
314 // renderer already ends its own output in `\n`, so this is a no-op
315 // for them; kept unconditional so `Verbatim`'s own guarantee stays
316 // exactly as it was.
317 if !out.ends_with('\n') {
318 out.push('\n');
319 }
320 // Readability policy (this module's own doc): one blank line
321 // between top-level declarations, except two consecutive
322 // `import`s, two consecutive `Comment`s, two consecutive
323 // `ReExportAll`s, a `Comment` immediately before a `ReExportAll`,
324 // or the next statement's own `no_blank_before` (#1486's own
325 // escape hatch — a real, already-shipped multi-statement sequence
326 // that genuinely needs no blank between two adjacent real
327 // statements, not expressible as a `TsStmtKind`-pair rule the way
328 // every case above is) — and never after `Verbatim` (P7.7's own
329 // boundary — `Verbatim` content's own spacing is not this
330 // printer's decision).
331 if let Some(next) = program.stmts.get(i + 1) {
332 // #1321: `workers.rs`'s own header has `import { ... } from
333 // "runtime"` (named) adjacent to `import * as handlers from
334 // "./handlers.js"` (namespace) adjacent to one `import * as
335 // {ns} from "..."` per referenced unit — all import-like, all
336 // real-content-adjacent with no blank line, matching the same
337 // "no blank line between two adjacent imports" rule
338 // `events_fanout.rs`'s own two named imports already exercised.
339 fn is_import_decl(kind: &TsStmtKind) -> bool {
340 matches!(
341 kind,
342 TsStmtKind::Decl(TsDecl::Import { .. })
343 | TsStmtKind::Decl(TsDecl::ImportNamespace { .. })
344 | TsStmtKind::Decl(TsDecl::ImportDefault { .. })
345 )
346 }
347 // #1329: `emit_commons_barrel`'s own real barrel module is one
348 // header `Comment` immediately followed by an `export *` line
349 // per constituent source file, every one of those lines
350 // adjacent to the next with no blank line anywhere — a
351 // genuinely different spacing shape from `ReExport`'s own
352 // (#1323's `workers_entry.rs` re-exports, each already
353 // separated from its neighbours by a blank line in the
354 // pre-conversion output, so `ReExport` itself still gets no
355 // grouping rule of its own). Scoped to exactly this new
356 // adjacency — a `Comment` before an ordinary `Import`/other
357 // decl still gets its blank line (see `events_fanout.rs`'s own
358 // header-comment-then-blank-then-import shape), unchanged.
359 fn is_reexport_all(kind: &TsStmtKind) -> bool {
360 matches!(kind, TsStmtKind::Decl(TsDecl::ReExportAll { .. }))
361 }
362 let both_imports = is_import_decl(&stmt.kind) && is_import_decl(&next.kind);
363 let both_comments = matches!(&stmt.kind, TsStmtKind::Comment(_))
364 && matches!(&next.kind, TsStmtKind::Comment(_));
365 let both_reexport_all = is_reexport_all(&stmt.kind) && is_reexport_all(&next.kind);
366 let comment_then_reexport_all =
367 matches!(&stmt.kind, TsStmtKind::Comment(_)) && is_reexport_all(&next.kind);
368 if !both_imports
369 && !both_comments
370 && !both_reexport_all
371 && !comment_then_reexport_all
372 && !next.no_blank_before
373 && !matches!(stmt.kind, TsStmtKind::Verbatim { .. })
374 {
375 out.push('\n');
376 }
377 }
378 }
379 let source_map = map.to_v3(&out, output_file);
380 Printed {
381 text: out,
382 source_map,
383 }
384}
385
386fn indent(depth: usize) -> String {
387 " ".repeat(depth)
388}
389
390/// `emit_doc_block`'s own real rendering, byte-for-byte (#1333): `/**`
391/// opens; one ` * <line>` per non-blank source line, a literal `*/`
392/// escaped to `*\/` (issue #720 — otherwise it would close the comment
393/// early and let trailing text land as executable top-level TypeScript); a
394/// blank source line prints as a bare ` *`, no trailing space; `*/`
395/// closes. All at `depth`'s own 2-space-per-level indent.
396fn render_doc_comment(out: &mut String, text: &str, depth: usize) {
397 let ind = indent(depth);
398 out.push_str(&ind);
399 out.push_str("/**\n");
400 for line in text.lines() {
401 let trimmed = line.trim_end();
402 out.push_str(&ind);
403 if trimmed.is_empty() {
404 out.push_str(" *\n");
405 } else {
406 out.push_str(" * ");
407 out.push_str(&trimmed.replace("*/", "*\\/"));
408 out.push('\n');
409 }
410 }
411 out.push_str(&ind);
412 out.push_str(" */\n");
413}
414
415/// #1477's own carrier for a nested-checkpoint merge target: which map to
416/// merge a [`crate::program::TsStmt::nested_map`]-bearing statement's own
417/// checkpoints into, and which of that map's own registered sources they
418/// belong to. A bare `&mut SourceMapBuilder` isn't enough on its own
419/// (review of #1488, finding 1): hardcoding source `0` is correct for
420/// `print`'s own single-primary-source map and for every real caller today
421/// (`emit_free_fn`/`emit_class_method_and_merge_source_map` both already
422/// pass `0`), but silently wrong for a multi-source aggregate map — a test
423/// module's own map spans several `.bynk` files, and its real per-body
424/// merges already pass a non-zero id (`bynk-emit/src/project/tests_emit.rs:
425/// 537`/`:1936`) — exactly the shape #1475 found is this arc's own next
426/// conversion target. Threading `source_id` alongside `map` here, not
427/// hardcoded inside `SourceMapBuilder::merge`'s own caller here, keeps that
428/// conversion correct from the start rather than needing a second pass to
429/// discover the same gap.
430struct MergeTarget<'a> {
431 map: &'a mut SourceMapBuilder,
432 source_id: usize,
433}
434
435impl MergeTarget<'_> {
436 /// A fresh, independently-usable borrow of the same target — `Option<
437 /// MergeTarget>` isn't `Copy` (it holds a `&mut`), so a caller handing
438 /// the same target to more than one child (a `Vec<TsStmt>`'s own loop,
439 /// `TsDecl::Class`'s own constructor-then-methods sequence) reborrows
440 /// once per child via this, the same shape `Option::as_deref_mut` gives
441 /// a bare `&mut`.
442 fn reborrow(&mut self) -> MergeTarget<'_> {
443 MergeTarget {
444 map: self.map,
445 source_id: self.source_id,
446 }
447 }
448}
449
450/// Render one statement, including its own leading indent.
451///
452/// `map`: #1477's own thread-through, checked *here*, once, rather than
453/// re-checked at every one of this function's own many call sites (review
454/// of #1488, finding 2: the first version of this mechanism only checked a
455/// `Vec<TsStmt>` body's own *direct* children, inside `render_block_stmts`
456/// — silently dropping a `nested_map` set on a statement handed to `print`/
457/// `print_stmt_and_merge` directly, or on a statement nested one level
458/// deeper still, inside a `Block`/`Switch` case/`try`/`catch` body within a
459/// real function body). Checking unconditionally at this one entry point,
460/// before dispatching on `stmt.kind`, means every caller — however deep —
461/// gets the merge for free by simply forwarding `map` on, the same way
462/// `depth` already threads through unconditionally; a caller that has
463/// nothing to merge just passes `None`, as most still do.
464fn render_stmt(out: &mut String, stmt: &TsStmt, depth: usize, mut map: Option<MergeTarget<'_>>) {
465 if let Some(nested) = &stmt.nested_map
466 && let Some(target) = map.as_mut()
467 {
468 let base = out.len();
469 render_stmt_kind(out, stmt, depth, None);
470 target
471 .map
472 .merge(nested, &out[base..], out, base, target.source_id);
473 return;
474 }
475 render_stmt_kind(out, stmt, depth, map);
476}
477
478/// [`render_stmt`]'s own per-`kind` dispatch, split out so the nested-
479/// checkpoint check above runs exactly once per statement regardless of
480/// which arm below ends up recursing back into `render_stmt` for a child.
481fn render_stmt_kind(
482 out: &mut String,
483 stmt: &TsStmt,
484 depth: usize,
485 mut map: Option<MergeTarget<'_>>,
486) {
487 match &stmt.kind {
488 TsStmtKind::Verbatim { text, .. } => {
489 out.push_str(text);
490 }
491 TsStmtKind::Decl(decl) => render_decl(out, decl, depth, map),
492 TsStmtKind::Const { name, ty, init } => {
493 out.push_str(&indent(depth));
494 out.push_str("const ");
495 render_binding_name(out, name);
496 if let Some(ty) = ty {
497 out.push_str(": ");
498 render_type(out, ty);
499 }
500 out.push_str(" = ");
501 render_stmt_level_expr(out, init, depth);
502 out.push_str(";\n");
503 }
504 TsStmtKind::Let { name, ty, init } => {
505 out.push_str(&indent(depth));
506 out.push_str("let ");
507 render_binding_name(out, name);
508 if let Some(ty) = ty {
509 out.push_str(": ");
510 render_type(out, ty);
511 }
512 if let Some(init) = init {
513 out.push_str(" = ");
514 render_stmt_level_expr(out, init, depth);
515 }
516 out.push_str(";\n");
517 }
518 TsStmtKind::ExprStmt(expr) => {
519 out.push_str(&indent(depth));
520 render_expr(out, expr);
521 out.push_str(";\n");
522 }
523 TsStmtKind::Return(expr) => {
524 out.push_str(&indent(depth));
525 out.push_str("return");
526 if let Some(expr) = expr {
527 out.push(' ');
528 // #1321: `return { ... }` needs the same depth-aware
529 // multiline-object handling `Const`/`Let`/`Assign` already
530 // get — `workers.rs`'s own `emit_worker_compose` returns
531 // its whole compose surface this way (one shorthand async
532 // `Method` entry per wrapper, one per line), a shape
533 // `render_expr`'s plain recursion can't render correctly
534 // (see `TsExpr::Object`'s own doc).
535 render_stmt_level_expr(out, expr, depth);
536 }
537 out.push_str(";\n");
538 }
539 TsStmtKind::Throw(expr) => {
540 out.push_str(&indent(depth));
541 out.push_str("throw ");
542 render_stmt_level_expr(out, expr, depth);
543 out.push_str(";\n");
544 }
545 TsStmtKind::If {
546 cond,
547 then_branch,
548 else_branch,
549 same_line_else,
550 } => {
551 out.push_str(&indent(depth));
552 out.push_str("if (");
553 render_expr(out, cond);
554 out.push(')');
555 // `} else {` on one line — #1325's own real gap, all three of
556 // `emit_test_main`'s own real `if`/`else` sites (one `Block`,
557 // one `InlineBlock`). Only reachable when `then_branch` is
558 // itself braced (`Block` or `InlineBlock` — every real
559 // `same_line_else` site is one or the other); a brace-free
560 // `then_branch` has no closing `}` for `else` to sit against,
561 // so this falls back to the ordinary fresh-line rendering
562 // rather than producing `<inline-stmt> else {`, which nothing
563 // real needs. Review of #1326, finding 2: the two-variant
564 // check needs no `Option<()>`/re-match/`unreachable!()` — a
565 // plain `matches!` guard plus one `if let ... else` dispatch
566 // says the same thing with no wildcard arm.
567 let then_is_braced = matches!(
568 &then_branch.kind,
569 TsStmtKind::Block(_) | TsStmtKind::InlineBlock(_)
570 );
571 if *same_line_else
572 && then_is_braced
573 && let Some(else_branch) = else_branch
574 {
575 // `render_inline_block`'s own `{ stmts }` shape, minus its
576 // own trailing `\n` and leading space (added here instead,
577 // matching `render_branch`'s own InlineBlock arm) so
578 // `else` continues on the same physical line;
579 // `render_block_body`'s own `{ stmts }` (no trailing
580 // newline, the exact shape `TryCatch`'s own `} catch (e) {`
581 // continuation already reuses) for the `Block` case,
582 // guaranteed by `then_is_braced` above.
583 if let TsStmtKind::InlineBlock(stmts) = &then_branch.kind {
584 out.push_str(" { ");
585 render_compact_stmts(out, stmts);
586 out.push_str(" }");
587 } else {
588 render_block_body(out, then_branch, depth, None);
589 }
590 out.push_str(" else");
591 render_branch(out, else_branch, depth);
592 } else {
593 render_branch(out, then_branch, depth);
594 if let Some(else_branch) = else_branch {
595 out.push_str(&indent(depth));
596 out.push_str("else");
597 render_branch(out, else_branch, depth);
598 }
599 }
600 }
601 TsStmtKind::ForOf {
602 binding,
603 iter,
604 body,
605 } => {
606 out.push_str(&indent(depth));
607 out.push_str("for (const ");
608 out.push_str(binding);
609 out.push_str(" of ");
610 render_expr(out, iter);
611 out.push(')');
612 render_branch(out, body, depth);
613 }
614 TsStmtKind::For {
615 name,
616 init,
617 test,
618 body,
619 } => {
620 out.push_str(&indent(depth));
621 out.push_str("for (let ");
622 out.push_str(name);
623 out.push_str(" = ");
624 render_expr(out, init);
625 out.push_str("; ");
626 render_expr(out, test);
627 out.push_str("; ");
628 out.push_str(name);
629 out.push_str("++)");
630 render_branch(out, body, depth);
631 }
632 TsStmtKind::TryCatch {
633 try_block,
634 catch_param,
635 catch_block,
636 } => {
637 out.push_str(&indent(depth));
638 out.push_str("try");
639 render_block_body(out, try_block, depth, map.as_mut().map(|t| t.reborrow()));
640 out.push_str(" catch");
641 if let Some(p) = catch_param {
642 out.push_str(" (");
643 out.push_str(p);
644 out.push(')');
645 }
646 render_block_body(out, catch_block, depth, map.as_mut().map(|t| t.reborrow()));
647 out.push('\n');
648 }
649 TsStmtKind::Block(stmts) => {
650 out.push_str(&indent(depth));
651 out.push_str("{\n");
652 for s in stmts {
653 render_stmt(out, s, depth + 1, map.as_mut().map(|t| t.reborrow()));
654 }
655 out.push_str(&indent(depth));
656 out.push_str("}\n");
657 }
658 TsStmtKind::Continue => {
659 out.push_str(&indent(depth));
660 out.push_str("continue;\n");
661 }
662 TsStmtKind::Assign { target, value } => {
663 out.push_str(&indent(depth));
664 render_expr(out, target);
665 out.push_str(" = ");
666 render_stmt_level_expr(out, value, depth);
667 out.push_str(";\n");
668 }
669 TsStmtKind::Comment(text) => {
670 for line in text.split('\n') {
671 out.push_str(&indent(depth));
672 out.push_str("// ");
673 out.push_str(line);
674 out.push('\n');
675 }
676 }
677 TsStmtKind::DocComment(text) => render_doc_comment(out, text, depth),
678 TsStmtKind::Blank => out.push('\n'),
679 TsStmtKind::Switch {
680 discriminant,
681 cases,
682 } => {
683 out.push_str(&indent(depth));
684 out.push_str("switch (");
685 render_expr(out, discriminant);
686 out.push_str(") {\n");
687 for case in cases {
688 debug_assert!(
689 !(case.test.is_some() && case.default_braced),
690 "TsSwitchCase.default_braced only means something on the default (test: None) case — review of #1402's own nit"
691 );
692 // Review of #1446: the mirror invariant for `case_braced`
693 // (Arc E slice 6, #1445) — it only means something on a
694 // non-`default` (`test: Some(..)`) case; every real
695 // `test: None` call site sets it `true` with no effect
696 // (`default_braced` owns that arm's bracing instead), which
697 // would silently swallow a future caller's `false` there.
698 debug_assert!(
699 case.test.is_some() || case.case_braced,
700 "TsSwitchCase.case_braced only means something on a non-default (test: Some(..)) case"
701 );
702 out.push_str(&indent(depth + 1));
703 match &case.test {
704 Some(test) => {
705 out.push_str("case ");
706 render_expr(out, test);
707 // Arc E slice 6 (#1445): `case_braced` is the
708 // non-`default` mirror of `default_braced` just
709 // below — `emit_sum_codec`'s own payload-free
710 // variant case (`case "Pending":\n return { kind:
711 // "Pending" };`) is real, unbraced content sitting
712 // right beside a braced payload-carrying sibling in
713 // the same switch (`212_json_codec`'s own `Status`
714 // fixture).
715 if case.case_braced {
716 out.push_str(": {\n");
717 for s in &case.body {
718 render_stmt(out, s, depth + 2, map.as_mut().map(|t| t.reborrow()));
719 }
720 out.push_str(&indent(depth + 1));
721 out.push_str("}\n");
722 } else {
723 out.push_str(":\n");
724 for s in &case.body {
725 render_stmt(out, s, depth + 2, map.as_mut().map(|t| t.reborrow()));
726 }
727 }
728 }
729 None => {
730 if case.default_braced {
731 out.push_str("default: {\n");
732 for s in &case.body {
733 render_stmt(out, s, depth + 2, map.as_mut().map(|t| t.reborrow()));
734 }
735 out.push_str(&indent(depth + 1));
736 out.push_str("}\n");
737 } else {
738 out.push_str("default:\n");
739 for s in &case.body {
740 render_stmt(out, s, depth + 2, map.as_mut().map(|t| t.reborrow()));
741 }
742 }
743 }
744 }
745 }
746 out.push_str(&indent(depth));
747 out.push_str("}\n");
748 }
749 TsStmtKind::InlineBlock(stmts) => {
750 out.push_str(&indent(depth));
751 render_inline_block(out, stmts);
752 }
753 TsStmtKind::Increment(expr) => {
754 out.push_str(&indent(depth));
755 render_expr(out, expr);
756 out.push_str("++;\n");
757 }
758 // Same rendering as `Verbatim` above — the text is printed exactly
759 // as given, no `indent(depth)` prefix, no added punctuation (see
760 // `TsStmtKind::Raw`'s own doc for why this is a distinct kind, not
761 // a reuse of `Verbatim`).
762 TsStmtKind::Raw(text) => out.push_str(text),
763 }
764}
765
766/// `if`/`for...of`'s own body: braces (and a nested block) when `branch` is
767/// itself a `Block`, otherwise printed inline on the same line — matching
768/// `events_fanout.rs`'s own `if (!Array.isArray(subs)) continue;` (no
769/// braces) alongside its always-braced `for...of` bodies.
770fn render_branch(out: &mut String, branch: &TsStmt, depth: usize) {
771 match &branch.kind {
772 TsStmtKind::Block(_) => {
773 render_block_body(out, branch, depth, None);
774 out.push('\n');
775 }
776 TsStmtKind::InlineBlock(stmts) => {
777 out.push(' ');
778 render_inline_block(out, stmts);
779 }
780 _ => {
781 out.push(' ');
782 render_inline_stmt(out, branch);
783 }
784 }
785}
786
787/// `{ stmt; stmt; ... }` on one generated line — [`TsStmtKind::InlineBlock`]'s
788/// own renderer, shared by [`render_branch`] (an `if`/`else` branch) and
789/// [`render_stmt`]'s own top-level/nested-statement-list arm (not reachable
790/// from any real content at that position today, but every `TsStmtKind`
791/// variant still needs a real rendering, not a wildcard). Each statement
792/// renders through [`render_inline_stmt`] (reusing its own exhaustive
793/// per-kind handling) with its own trailing newline trimmed, then a single
794/// space — matching the real, hand-written `{ a; b; c; }` shape exactly.
795fn render_inline_block(out: &mut String, stmts: &[TsStmt]) {
796 out.push_str("{ ");
797 render_compact_stmts(out, stmts);
798 out.push_str(" }\n");
799}
800
801/// `stmt1; stmt2; ...` — every statement in `stmts` rendered on the current
802/// line, semicolon-and-space-separated, no leading/trailing space or
803/// surrounding braces of its own. The shared core both real compact shapes
804/// need: [`TsStmtKind::InlineBlock`]'s own single-line-*braced* form
805/// (`render_inline_block`, braces on the same line as the content) and
806/// [`render_block_body`]'s own special case for a `TryCatch` block whose
807/// body is an `InlineBlock` (braces on their own lines, per that shape's
808/// usual convention, but the body itself still one compact line) —
809/// `workers_entry.rs`'s own two real shapes, distinguished by whether the
810/// pre-conversion `writeln!` code put the opening brace on the same
811/// physical line as the content or not.
812fn render_compact_stmts(out: &mut String, stmts: &[TsStmt]) {
813 for (i, s) in stmts.iter().enumerate() {
814 if i > 0 {
815 out.push(' ');
816 }
817 let mut piece = String::new();
818 render_inline_stmt(&mut piece, s);
819 // Review of #1324, finding 2: only `trim_end_matches('\n')`'s own
820 // trailing newline is stripped — an *embedded* newline (a genuinely
821 // multi-line statement, e.g. `Block`/`Switch`/`TryCatch`, rendered
822 // via `render_inline_stmt`'s own `render_stmt(out, stmt, 0)`
823 // fallback) would break the one-line shape this function promises.
824 // Not reachable today — `workers_entry.rs`'s own three real
825 // `InlineBlock` sites are `ExprStmt`/`Continue` only — but worth a
826 // loud check rather than a silent multi-line break if a future
827 // slice's `InlineBlock` ever holds one.
828 debug_assert!(
829 !piece.trim_end_matches('\n').contains('\n'),
830 "render_compact_stmts: a genuinely multi-line statement can't render on one line"
831 );
832 out.push_str(piece.trim_end_matches('\n'));
833 }
834}
835
836/// A statement rendered without its own leading indent, for the "no braces"
837/// half of [`render_branch`] — only the shapes `events_fanout.rs` actually
838/// needs inline (`continue`, `return`, a bare expression) get dedicated
839/// handling; anything else falls back to a normal indented render (depth 0)
840/// rather than panicking, so an unanticipated future shape still prints
841/// something plausible instead of crashing.
842fn render_inline_stmt(out: &mut String, stmt: &TsStmt) {
843 match &stmt.kind {
844 TsStmtKind::Continue => out.push_str("continue;\n"),
845 TsStmtKind::Return(expr) => {
846 out.push_str("return");
847 if let Some(expr) = expr {
848 out.push(' ');
849 render_expr(out, expr);
850 }
851 out.push_str(";\n");
852 }
853 TsStmtKind::ExprStmt(expr) => {
854 render_expr(out, expr);
855 out.push_str(";\n");
856 }
857 // Review of #1317/#1318, finding 2: every other variant in the
858 // fallback group below is safe to render via `render_stmt`'s own
859 // top-level `//`-line-comment form — `Comment` is not. A `//`
860 // comment run through the generic fallback (`if (cond) // text`)
861 // comments out the rest of the physical line, leaving the `if`
862 // with no body at all — a TypeScript parse error, not merely an
863 // unlikely shape. Not reachable today (`events_fanout.rs` never
864 // puts a bare `Comment` in a brace-free `if`/`for...of` body), but
865 // the fallback group exists precisely so a future slice trips over
866 // a missing case at compile time, not at parse time in emitted
867 // output — so `Comment` needs its own real inline shape now, a
868 // block comment (`/* text */`), which cannot swallow anything
869 // after it. Embedded newlines flatten to spaces, keeping this
870 // strictly one generated line like every other inline shape here.
871 // Review of #1324, finding 2: `Blank`'s own top-level rendering is a
872 // bare `'\n'` (see `render_stmt`), which the generic fallback below
873 // would inherit unchanged — reaching `render_branch`'s brace-free
874 // arm (`if (cond) <inline>`), that prints `if (cond) \n`, and since
875 // JS/TS statement grammar doesn't care about the newline, the very
876 // next statement in the enclosing block silently becomes the `if`'s
877 // own body. Exactly the same swallowing-bug class the #1317/#1318
878 // review already fixed for `Comment` just above — an honest empty
879 // statement (`;`) is what a "no code here" body actually means in an
880 // inline TS position, and cannot swallow anything after it.
881 TsStmtKind::Blank => out.push_str(";\n"),
882 TsStmtKind::Comment(text) => {
883 out.push_str("/* ");
884 out.push_str(&text.replace('\n', " "));
885 out.push_str(" */\n");
886 }
887 // No inline shape of its own — `events_fanout.rs` never puts one of
888 // these in a brace-free `if`/`for...of` body — but listed by name,
889 // not a wildcard (review of #1314, finding 3): a future Arc C
890 // slice's new `TsStmtKind` variant must fail to compile here rather
891 // than silently inherit this fallback, the same exhaustiveness
892 // discipline `VerbatimOrigin`'s own doc states for `bynk-emit`.
893 TsStmtKind::Verbatim { .. }
894 | TsStmtKind::Decl(_)
895 | TsStmtKind::Const { .. }
896 | TsStmtKind::Let { .. }
897 | TsStmtKind::If { .. }
898 | TsStmtKind::ForOf { .. }
899 // Arc E slice 7 (#1447): not reachable today — `ListInst`/
900 // `MapInst`'s two real `For` sites both use a braced `Block` body,
901 // which `render_branch` dispatches to its own multi-line arm before
902 // this fallback group is ever consulted — but safe for the same
903 // reason `ForOf` already is: a brace-free `For` body renders through
904 // `render_stmt`'s own indented, newline-terminated form, which is
905 // exactly what this fallback provides.
906 | TsStmtKind::For { .. }
907 | TsStmtKind::TryCatch { .. }
908 | TsStmtKind::Block(_)
909 | TsStmtKind::Assign { .. }
910 | TsStmtKind::Switch { .. }
911 | TsStmtKind::InlineBlock(_)
912 // #1325: `passed++;`/`failed++;` inside `emit_test_main`'s own
913 // compact `{ passed++; console.log(...); }` shape — always complete,
914 // single-line, semicolon-terminated content (never a bare newline
915 // the way `Blank`'s own top-level form is), so the same fallback
916 // that's safe for `Const`/`If`/etc. above is safe here too.
917 | TsStmtKind::Increment(_)
918 // #1353: `emit_contract_guarded_body`'s own precondition/
919 // postcondition guards are exactly this fallback's own target case —
920 // a real, reachable `InlineBlock` site (`if (!(pred)) { const __e =
921 // ...; __e.name = ...; throw __e; }`), not a defensive placeholder.
922 // Safe for the same reason as `Const`/`Assign` above, with the same
923 // caveat those two already carry: `render_stmt_level_expr` renders a
924 // `multiline: true` operand with embedded newlines, which would
925 // break this fallback's one-line contract — untested today only
926 // because the one real call site (`throw __e;`, a bare `Ident`)
927 // never reaches that case, not because `Throw` structurally cannot.
928 | TsStmtKind::Throw(_)
929 // #1333: not reachable today — every real `DocComment` reaches the
930 // printer only via `print_stmt`, never through a `TsProgram`'s own
931 // tree — which is what actually makes this fallback safe: unlike
932 // `Comment`'s own `//` form, a `/** ... */` block always closes
933 // itself before any subsequent text, so it cannot swallow the next
934 // statement the way an unterminated `//` line comment could — but
935 // the fallback's own `render_stmt(out, stmt, 0)` still emits the
936 // block's embedded newlines, which would break an `InlineBlock`'s
937 // single-line contract if this arm ever became reachable through
938 // one. Listed by name per this group's own exhaustiveness
939 // discipline, not folded into a wildcard.
940 | TsStmtKind::DocComment(_)
941 // #1337: not reachable today — every real `Raw`-bearing body
942 // (`TsObjectEntry::Method`, and since #1352/#1369 also
943 // `TsDecl::Function`) renders through `render_block_stmts`, never
944 // through `render_inline_stmt`'s own call path (an `if`/`for...of`
945 // branch or an `InlineBlock`). Unsafe if it ever became reachable
946 // there for the same reason `DocComment` is: `Raw`'s own text is
947 // typically a whole multi-statement function body, never
948 // single-line, so the fallback's embedded newlines would break an
949 // `InlineBlock`'s single-line contract. Since #1369 added
950 // `TsDecl::Function.inline`, this is now one field flip away from
951 // live rather than purely hypothetical — `render_compact_stmts`'s
952 // own `debug_assert!` is the actual backstop if a future call site
953 // ever combines `inline: true` with a `Raw` body statement;
954 // `emit_agent`'s own rehydrate function (#1369) deliberately keeps
955 // `inline: false` specifically to avoid exercising this arm.
956 // Listed by name, not folded into a wildcard, for the same reason
957 // as every other arm in this group.
958 | TsStmtKind::Raw(_) => render_stmt(out, stmt, 0, None),
959 }
960}
961
962/// `{ <stmts> }`, with the closing brace at `depth`'s own indent and no
963/// trailing newline — the shared shape [`TsStmtKind::TryCatch`] needs to
964/// keep writing on the same generated line (`} catch (e) {`) and
965/// [`render_branch`]'s own `Block` case needs with a `\n` appended after.
966/// `block` is expected to be a [`TsStmtKind::Block`]; anything else is
967/// still rendered sensibly (as a single-statement body) rather than
968/// panicking.
969///
970/// `map`: forwarded into `stmts` when `block` is a real `Block` (#1477) —
971/// `TryCatch`'s own `try`/`catch` bodies are the one real site this covers
972/// (a `nested_map`-bearing statement inside either); `render_branch`'s own
973/// `If`/`ForOf`/`For` call sites always pass `None`, since nothing threads
974/// a map into `render_branch` itself (out of scope — ADR 0391's own
975/// permanently-opaque content is always a whole function/method/constructor
976/// body, never embedded directly inside an `if`/loop branch).
977fn render_block_body(
978 out: &mut String,
979 block: &TsStmt,
980 depth: usize,
981 mut map: Option<MergeTarget<'_>>,
982) {
983 out.push_str(" {\n");
984 if let TsStmtKind::Block(stmts) = &block.kind {
985 for s in stmts {
986 render_stmt(out, s, depth + 1, map.as_mut().map(|t| t.reborrow()));
987 }
988 } else if let TsStmtKind::InlineBlock(stmts) = &block.kind {
989 // The braces sit on their own lines here (this function's own usual
990 // shape, e.g. `TryCatch`'s `catch (e) { ... }`), but the body itself
991 // is one compact line — `workers_entry.rs`'s own real queue-consumer
992 // catch clause (`console.error(...); msg.retry();`).
993 out.push_str(&indent(depth + 1));
994 render_compact_stmts(out, stmts);
995 out.push('\n');
996 } else {
997 render_stmt(out, block, depth + 1, None);
998 }
999 out.push_str(&indent(depth));
1000 out.push('}');
1001}
1002
1003fn render_binding_name(out: &mut String, name: &TsBindingName) {
1004 match name {
1005 TsBindingName::Ident(s) => out.push_str(s),
1006 TsBindingName::ObjectPattern(names) => {
1007 out.push_str("{ ");
1008 for (i, n) in names.iter().enumerate() {
1009 if i > 0 {
1010 out.push_str(", ");
1011 }
1012 out.push_str(n);
1013 }
1014 out.push_str(" }");
1015 }
1016 }
1017}
1018
1019/// Whether `expr`, printed in a position member access, indexing, a call
1020/// callee, `await`, or unary `!`/`typeof` binds tighter than, needs its own
1021/// parens to preserve the built tree's meaning — `Binary`/`As`/`Arrow`/
1022/// `Conditional` all bind looser than any of those (review of #1314, finding
1023/// 2: `!a ?? b`/`a ?? b.c`/`await a ?? b` all silently changed meaning
1024/// without this; review of #1322, finding 1: `Arrow` was missing from this
1025/// same rule — `(x) => x(1)` is a call whose *body* is `x(1)`, not the IIFE
1026/// `((x) => x)(1)` a `Call { callee: Arrow, .. }` node means; #1323 adds
1027/// `Conditional` proactively, the same class of gap, before any real content
1028/// or review round hits it). Deliberately conservative for *these* contexts
1029/// specifically: a nested `Binary`/`Arrow`/`Conditional` is always
1030/// parenthesized here regardless of which operator/shape it is — correct in
1031/// every case that class of bug can hit (none of these
1032/// six positions is ever real, grounded content in this file today), at
1033/// the cost of an occasional pair a real precedence table could omit.
1034///
1035/// **Not** used for `Binary`'s own left/right operands — see
1036/// [`render_binary_operand`]/[`binary_precedence`] for why that context
1037/// needed to become precedence-aware once #1321 added more than one binary
1038/// operator (this function's own always-parenthesize rule would print
1039/// `(__authz === null) || …` for `workers.rs`'s own real
1040/// `__authz === null || !__authz.startsWith(...)`, which the byte-golden
1041/// fixtures don't have parens around).
1042fn needs_parens_as_operand(expr: &TsExpr) -> bool {
1043 matches!(
1044 expr,
1045 TsExpr::Binary { .. }
1046 | TsExpr::As { .. }
1047 | TsExpr::Arrow { .. }
1048 | TsExpr::Conditional { .. }
1049 )
1050}
1051
1052fn render_operand(out: &mut String, expr: &TsExpr) {
1053 if needs_parens_as_operand(expr) {
1054 out.push('(');
1055 render_expr(out, expr);
1056 out.push(')');
1057 } else {
1058 render_expr(out, expr);
1059 }
1060}
1061
1062/// Standard JS/TS relative precedence for the operators [`TsBinaryOp`]
1063/// currently has — only used to decide a nested `Binary` operand's own
1064/// parenthesisation (see [`render_binary_operand`]); higher binds tighter.
1065fn binary_precedence(op: TsBinaryOp) -> u8 {
1066 match op {
1067 TsBinaryOp::NullishCoalescing => 1,
1068 TsBinaryOp::Or => 2,
1069 TsBinaryOp::And => 3,
1070 TsBinaryOp::StrictEq | TsBinaryOp::StrictNotEq => 4,
1071 TsBinaryOp::GreaterThan
1072 | TsBinaryOp::LessThan
1073 | TsBinaryOp::InstanceOf
1074 | TsBinaryOp::In
1075 | TsBinaryOp::GreaterThanEq
1076 | TsBinaryOp::LessThanEq => 5,
1077 TsBinaryOp::Add => 6,
1078 }
1079}
1080
1081/// Render `expr` as one side of a `Binary { op: outer_op, .. }` — #1321's
1082/// own real gap (`workers.rs`'s own `__authz === null ||
1083/// !__authz.startsWith(...)` / `__authz !== null &&
1084/// __authz.startsWith(...)`): with only `??` in the algebra,
1085/// [`render_operand`]'s "always parenthesize a nested `Binary`" rule never
1086/// mismatched real content; a comparison (`===`/`!==`) nested inside `||`/
1087/// `&&` needs *no* parens to preserve meaning (`===` binds tighter), and
1088/// the byte-golden fixtures have none — so this context needed to become
1089/// precedence-aware rather than reusing [`render_operand`] unchanged.
1090/// `??` mixed with `||`/`&&` is a real TS syntax error without explicit
1091/// parens (not just a precedence question), so that combination always
1092/// parenthesizes regardless of the numeric table above; nothing in
1093/// `bynk-emit` builds that combination today, but a future caller that did
1094/// still gets correct, parseable output rather than a silent `tsc` error.
1095///
1096/// Equal precedence still parenthesizes for two *different* operators
1097/// (`<=`, not `<`) — deliberately preserving [`render_operand`]'s own
1098/// pre-#1321 "always parenthesize a same/lower-precedence nested chain,
1099/// even where associativity would read the same without it" conservatism
1100/// for that case; only a *strictly higher*-precedence nested operator — the
1101/// one real, grounded case #1321 needs — omits parens there. #1323 adds one
1102/// more real case: the exact *same* operator nested in itself omits parens
1103/// too, for `||`/`&&` specifically (both associative, and real content —
1104/// `emit_call_handler_dispatch`'s 3-term `||` chain — needs the flat,
1105/// unparenthesized reading) — `??` keeps the pre-#1321 conservatism
1106/// unchanged (pinned by `parenthesises_a_nested_binary_operand_of_another_
1107/// binary`'s own test, still `a ?? (b ?? c)`), since nothing in real
1108/// content needs a flattened `??` chain and this slice's own grounding
1109/// gives no reason to widen that boundary opportunistically.
1110fn render_binary_operand(out: &mut String, outer_op: TsBinaryOp, expr: &TsExpr, is_left: bool) {
1111 let needs_parens = match expr {
1112 // `Arrow`/`Conditional` are the two lowest-precedence expression
1113 // forms in JS/TS — `(x) => y || z` as a binary operand must print
1114 // `(x) => (y || z)`, never the bare `(x) => y || z` (which reparses
1115 // as the arrow's own *body* being `y || z`, not the arrow itself
1116 // being one operand of `||`); a `Conditional` operand needs the
1117 // identical protection for the identical reason (review of #1322,
1118 // finding 1, for `Arrow`; #1323 adds `Conditional` proactively, the
1119 // same class of gap).
1120 TsExpr::As { .. } | TsExpr::Arrow { .. } | TsExpr::Conditional { .. } => true,
1121 TsExpr::Binary { op: inner_op, .. } => {
1122 let mixes_nullish = (outer_op == TsBinaryOp::NullishCoalescing)
1123 != (*inner_op == TsBinaryOp::NullishCoalescing);
1124 if mixes_nullish {
1125 true
1126 } else if *inner_op == outer_op {
1127 // Same operator chained with itself: only `||`/`&&` are
1128 // associative, so only they may print flat with no parens —
1129 // #1323's own `typeof args !== "object" || args === null ||
1130 // Array.isArray(args)` (`emit_call_handler_dispatch`), a
1131 // genuine 3-term chain the pre-#1323 "always parenthesize
1132 // equal precedence" rule would have wrongly printed as
1133 // `(typeof args !== "object" || args === null) ||
1134 // Array.isArray(args)`. Every other operator — including
1135 // `??` (kept at its pre-#1321 conservative choice; nothing in
1136 // real content needs a flattened `??` chain, and
1137 // `parenthesises_a_nested_binary_operand_of_another_binary`
1138 // pins that `a ?? (b ?? c)` still gets its parens) and, as of
1139 // this fix, the equality/relational operators (`===`/`!==`/
1140 // `>`) — is NOT associative: `a === b === c` parses as
1141 // `(a === b) === c`, not the tree's real `a === (b === c)`,
1142 // so a same-operator nesting of any of those still needs its
1143 // parens regardless of which side it's nested on (review of
1144 // #1324, finding 1 — the original fix wrongly generalized
1145 // the `||`/`&&` exemption to every operator). `+` (Arc C,
1146 // step (11), #1388) does NOT join that exemption on both
1147 // sides — review of #1389, finding 1: `||`/`&&` are
1148 // SEMANTICALLY associative (`(a || b) || c` and
1149 // `a || (b || c)` always agree), but `+` is only
1150 // GRAMMATICALLY left-associative — with a mixed number/string
1151 // chain, `1 + (2 + "3")` (`"123"`) and `(1 + 2) + "3"`
1152 // (`"33"`) disagree, so flattening a RIGHT-nested `Add` would
1153 // silently change the value. `join_plus`
1154 // (`bynk-emit/src/emitter/emit.rs`) only ever left-folds, so
1155 // this asymmetry never showed up in that caller's own
1156 // zero-diff fixture run — but `bynk-ts` is a shared
1157 // primitive, not scoped to that one caller's own usage.
1158 match outer_op {
1159 TsBinaryOp::Or | TsBinaryOp::And => false,
1160 TsBinaryOp::Add => !is_left,
1161 _ => true,
1162 }
1163 } else {
1164 binary_precedence(*inner_op) <= binary_precedence(outer_op)
1165 }
1166 }
1167 _ => false,
1168 };
1169 if needs_parens {
1170 out.push('(');
1171 render_expr(out, expr);
1172 out.push(')');
1173 } else {
1174 render_expr(out, expr);
1175 }
1176}
1177
1178/// Render `expr` as a statement or declaration's own top-level expression
1179/// (a `const`/`let` initialiser, an assignment's own value, …) — one of the
1180/// two places `depth` is available to render a `multiline: true`
1181/// [`TsExpr::Object`]/[`TsExpr::Array`] correctly (see `Object`'s own doc);
1182/// the other is [`render_multiline_object_entry`]'s own `Prop` arm (#1355 —
1183/// a `Prop`'s own value can itself be a nested multiline object/array,
1184/// `emit_messages_bundle`'s own real doubly-nested table). Every other
1185/// shape defers to the ordinary, depth-unaware [`render_expr`] unchanged;
1186/// this exists only to intercept the one shape that needs `depth` before it
1187/// gets there.
1188fn render_stmt_level_expr(out: &mut String, expr: &TsExpr, depth: usize) {
1189 if let TsExpr::Object {
1190 entries,
1191 multiline: true,
1192 } = expr
1193 {
1194 render_multiline_object(out, entries, depth);
1195 } else if let TsExpr::Array {
1196 items,
1197 multiline: true,
1198 } = expr
1199 {
1200 render_multiline_array(out, items, depth);
1201 } else {
1202 render_expr(out, expr);
1203 }
1204}
1205
1206/// `{ <newline> (" "*depth+1)<key>: <value>,<newline> ... (" "*depth)}` —
1207/// one entry per line, each with its own trailing comma (including the
1208/// last), closing brace back at `depth`'s own indent. `events_fanout.rs`'s
1209/// own `__eventRoutes` table is the real, grounded shape this exists for
1210/// (#1317) — TypeScript's ordinary multi-line object-literal convention.
1211/// #1321 (`workers.rs`): entries may now be [`TsObjectEntry`], not just
1212/// `Prop` — the `compose`-returned surface object is one shorthand async
1213/// `Method` entry per wrapper, one per line.
1214fn render_multiline_object(out: &mut String, entries: &[TsObjectEntry], depth: usize) {
1215 // Deliberately no empty-entries shortcut (review of #1317/#1318, finding
1216 // 1): `events_fanout.rs`'s own `__eventRoutes` table is reachable with
1217 // zero entries (a context can `ctx_uses_emit` while publishing only
1218 // events nobody subscribes to — `own_event_routes` filters down to
1219 // exactly that empty case, `bynk-emit/src/project.rs`'s own
1220 // `own_event_routes` computation), and the pre-conversion `writeln!`
1221 // code always wrote the open-brace line and the closing `};` line
1222 // unconditionally, regardless of how many times its own `for` loop
1223 // iterated — an empty table printed `{\n};`, not the tight `{}` a
1224 // single-line object's own empty case uses. Matching that byte-for-byte
1225 // means never taking the single-line shortcut here, not even for zero
1226 // entries.
1227 out.push_str("{\n");
1228 for entry in entries {
1229 render_multiline_object_entry(out, entry, depth, None);
1230 }
1231 out.push_str(&indent(depth));
1232 out.push('}');
1233}
1234
1235/// One [`TsObjectEntry`], as [`render_multiline_object`]'s own per-entry
1236/// loop body — factored out so [`print_object_entry`] can render exactly
1237/// one entry without a whole object's own opening/closing braces, sharing
1238/// this one real per-kind dispatch rather than a second copy (the same
1239/// "one document-fragment entry point, no duplicated rendering" posture
1240/// [`print_stmt`]/[`print_type`] already established).
1241/// `map`: see [`render_block_stmts`]'s own doc (#1477) — reaches a `Method`
1242/// entry's own body unchanged; every other entry kind ignores it (no body
1243/// of its own).
1244fn render_multiline_object_entry(
1245 out: &mut String,
1246 entry: &TsObjectEntry,
1247 depth: usize,
1248 map: Option<MergeTarget<'_>>,
1249) {
1250 // Review of #1340, finding 1: a `Raw`-bodied `Method` entry's own text
1251 // is captured pre-indented at a fixed absolute depth by its caller (see
1252 // `print_object_entry`'s own doc) — correct only when `depth` is `0`.
1253 // Originally guarded only in `print_object_entry`, the one entry point
1254 // #1337 had in mind; #1339's `emit_refined_type` reaches this function
1255 // directly (via `TsExpr::multiline_object_entries`/`render_expr`, never
1256 // through `print_object_entry`), which the original guard's placement
1257 // silently missed. Asserted here instead, where both callers converge,
1258 // so neither path can bypass it again.
1259 if let TsObjectEntry::Method { body, .. } = entry {
1260 debug_assert!(
1261 depth == 0 || !contains_raw(body),
1262 "render_multiline_object_entry: a Raw-bodied Method entry's own baked-in indent only matches depth 0"
1263 );
1264 }
1265 match entry {
1266 TsObjectEntry::Prop(k, v) => {
1267 out.push_str(&indent(depth + 1));
1268 out.push_str(k);
1269 out.push_str(": ");
1270 // #1355: a `Prop`'s own value sits one level deeper than the
1271 // entry itself (matching this function's own `indent(depth +
1272 // 1)` above) — the same depth `render_stmt_level_expr`'s own
1273 // doc names as "the only place `depth` is available to render a
1274 // `multiline: true` `TsExpr::Object`/`Array` correctly."
1275 // `emit_messages_bundle`'s own real, doubly-nested `{ locale: {
1276 // code: expr, ... }, ... }` table is this gap's real grounding
1277 // — a nested multiline object value previously fell back to
1278 // single-line silently (the plain, depth-unaware `render_expr`
1279 // recursion ignores `multiline` entirely).
1280 render_stmt_level_expr(out, v, depth + 1);
1281 out.push_str(",\n");
1282 }
1283 TsObjectEntry::Shorthand(name) => {
1284 out.push_str(&indent(depth + 1));
1285 out.push_str(name);
1286 out.push_str(",\n");
1287 }
1288 TsObjectEntry::Spread(e) => {
1289 out.push_str(&indent(depth + 1));
1290 out.push_str("...");
1291 render_expr(out, e);
1292 out.push_str(",\n");
1293 }
1294 // `workers.rs`'s own dominant real shape (Decision A, gap 1):
1295 // `async {name}({params}) { <body> },`, body indented one
1296 // level deeper still — the same shape a class method's own
1297 // body gets (`render_decl_body`'s `TsDecl::Class` arm), here
1298 // for one object-literal entry instead.
1299 TsObjectEntry::Method {
1300 name,
1301 is_async,
1302 generics,
1303 params,
1304 return_type,
1305 doc,
1306 inline,
1307 body,
1308 } => {
1309 if let Some(text) = doc {
1310 render_doc_comment(out, text, depth + 1);
1311 }
1312 out.push_str(&indent(depth + 1));
1313 if *is_async {
1314 out.push_str("async ");
1315 }
1316 out.push_str(name);
1317 render_bare_generics(out, generics);
1318 out.push('(');
1319 render_params(out, params);
1320 out.push(')');
1321 if let Some(rt) = return_type {
1322 out.push_str(": ");
1323 render_type(out, rt);
1324 }
1325 if *inline {
1326 out.push_str(" { ");
1327 render_compact_stmts(out, body);
1328 out.push_str(" }");
1329 } else {
1330 render_block_stmts(out, body, depth + 1, map);
1331 }
1332 out.push_str(",\n");
1333 }
1334 }
1335}
1336
1337/// `<{names.join(", ")}>`, or nothing at all when `names` is empty — bare
1338/// names only, matching `bynk-emit`'s own `ts_type_params` rendering
1339/// exactly. Originally `TsObjectEntry::Method`'s own generic parameter list
1340/// (#1337); generalised by #1339 into the one shared renderer for every
1341/// bare-name generics/type-params list this crate builds
1342/// (`TsDecl::Interface.type_params`, `TsDecl::TypeAlias.type_params`,
1343/// `TsExpr::Arrow.generics`) rather than four near-identical copies.
1344fn render_bare_generics(out: &mut String, names: &[String]) {
1345 if names.is_empty() {
1346 return;
1347 }
1348 out.push('<');
1349 for (i, g) in names.iter().enumerate() {
1350 if i > 0 {
1351 out.push_str(", ");
1352 }
1353 out.push_str(g);
1354 }
1355 out.push('>');
1356}
1357
1358/// `[ <newline> (" "*depth+1)<item>,<newline> ... (" "*depth)]` — one
1359/// item per line, each with its own trailing comma, closing bracket back at
1360/// `depth`'s own indent. [`render_multiline_object`]'s own sibling for array
1361/// literals — #1325's own real, grounded shape: `emit_test_main`'s own
1362/// `modules` array (one `{ name, run }` entry per test). Same "no
1363/// empty-items shortcut" discipline as `render_multiline_object` — nothing
1364/// in `emit_test_main`'s own real content reaches this with zero tests, but
1365/// matching the pre-conversion `writeln!` code's own unconditional
1366/// open/close-bracket-on-separate-lines shape (rather than assuming an
1367/// untested empty case) is the same precedent review of #1317/#1318 set.
1368fn render_multiline_array(out: &mut String, items: &[TsExpr], depth: usize) {
1369 out.push_str("[\n");
1370 for item in items {
1371 out.push_str(&indent(depth + 1));
1372 render_expr(out, item);
1373 out.push_str(",\n");
1374 }
1375 out.push_str(&indent(depth));
1376 out.push(']');
1377}
1378
1379/// One [`TsObjectEntry`] rendered inline (no leading indent, no trailing
1380/// comma/newline) — [`render_expr`]'s own `TsExpr::Object` arm's per-entry
1381/// renderer, shared so the single-line and multi-line forms agree on what
1382/// each entry kind looks like. A `Method` entry has no real single-line
1383/// call site today (`workers.rs`'s own method entries are always the
1384/// multi-line `return { ... }` shape), but is still rendered plausibly
1385/// (not panicking) rather than left unreachable, matching this crate's own
1386/// "no wildcard, but still sensible on an unanticipated shape" posture
1387/// elsewhere (e.g. `render_block_body`'s own non-`Block` fallback).
1388fn render_object_entry_inline(out: &mut String, entry: &TsObjectEntry) {
1389 match entry {
1390 TsObjectEntry::Prop(k, v) => {
1391 out.push_str(k);
1392 out.push_str(": ");
1393 render_expr(out, v);
1394 }
1395 TsObjectEntry::Shorthand(name) => out.push_str(name),
1396 TsObjectEntry::Spread(e) => {
1397 out.push_str("...");
1398 render_expr(out, e);
1399 }
1400 TsObjectEntry::Method {
1401 name,
1402 is_async,
1403 generics,
1404 params,
1405 return_type,
1406 doc,
1407 inline,
1408 body,
1409 } => {
1410 // Review of #1338, finding 2: a JSDoc block has no single-line
1411 // form — not reachable through this function today: every real
1412 // `Method` entry (`inline` or not) is placed via
1413 // `render_multiline_object_entry`, never here (this function
1414 // renders one ENTRY inline for when the whole ENCLOSING OBJECT
1415 // is single-line, a different concept from a method's own body
1416 // being compact). Silently dropping a JSDoc block would be a
1417 // strictly worse failure mode than a loud check — the same
1418 // "not reachable today, but worth a loud check" posture
1419 // `render_compact_stmts`'s own `debug_assert!` (above) already
1420 // established for an analogous inline-rendering hazard.
1421 debug_assert!(
1422 doc.is_none(),
1423 "render_object_entry_inline: a Method entry's own doc comment has no single-line form"
1424 );
1425 if *is_async {
1426 out.push_str("async ");
1427 }
1428 out.push_str(name);
1429 render_bare_generics(out, generics);
1430 out.push('(');
1431 render_params(out, params);
1432 out.push(')');
1433 if let Some(rt) = return_type {
1434 out.push_str(": ");
1435 render_type(out, rt);
1436 }
1437 if *inline {
1438 out.push_str(" { ");
1439 render_compact_stmts(out, body);
1440 out.push_str(" }");
1441 } else {
1442 render_block_stmts(out, body, 0, None);
1443 }
1444 }
1445 }
1446}
1447
1448fn render_expr(out: &mut String, expr: &TsExpr) {
1449 match expr {
1450 TsExpr::Ident(name) => out.push_str(name),
1451 // #1539: printed exactly as `Ident` always was — raw text, no
1452 // escaping of its own. The origin exists for `verbatim_origins`/
1453 // `verbatim_sites` (xtask's gated probes) to line-scan, not for the
1454 // printer to branch on.
1455 TsExpr::VerbatimExpr(text, _origin) => out.push_str(text),
1456 TsExpr::Member { object, property } => {
1457 render_operand(out, object);
1458 out.push('.');
1459 out.push_str(property);
1460 }
1461 TsExpr::OptionalMember { object, property } => {
1462 render_operand(out, object);
1463 out.push_str("?.");
1464 out.push_str(property);
1465 }
1466 TsExpr::Index { object, index } => {
1467 render_operand(out, object);
1468 out.push('[');
1469 render_expr(out, index);
1470 out.push(']');
1471 }
1472 TsExpr::OptionalIndex { object, index } => {
1473 render_operand(out, object);
1474 out.push_str("?.[");
1475 render_expr(out, index);
1476 out.push(']');
1477 }
1478 TsExpr::Arrow {
1479 params,
1480 is_async,
1481 generics,
1482 return_type,
1483 body,
1484 } => {
1485 if *is_async {
1486 out.push_str("async ");
1487 }
1488 render_bare_generics(out, generics);
1489 out.push('(');
1490 render_params(out, params);
1491 out.push(')');
1492 if let Some(rt) = return_type {
1493 out.push_str(": ");
1494 render_type(out, rt);
1495 }
1496 out.push_str(" => ");
1497 render_arrow_body(out, body);
1498 }
1499 TsExpr::Call { callee, args } => {
1500 render_operand(out, callee);
1501 out.push('(');
1502 render_expr_list(out, args);
1503 out.push(')');
1504 }
1505 TsExpr::New { callee, args } => {
1506 out.push_str("new ");
1507 render_operand(out, callee);
1508 out.push('(');
1509 render_expr_list(out, args);
1510 out.push(')');
1511 }
1512 // `multiline` is ignored here deliberately: this recursion has no
1513 // `depth` to render a multi-line object correctly against (see
1514 // `TsExpr::Object`'s own doc). Every real `bynk-emit` call site
1515 // that needs `multiline: true` reaches it through
1516 // `render_stmt_level_expr` instead, which intercepts the shape
1517 // before it gets here.
1518 TsExpr::Object { entries, .. } => {
1519 if entries.is_empty() {
1520 out.push_str("{}");
1521 } else {
1522 out.push_str("{ ");
1523 for (i, entry) in entries.iter().enumerate() {
1524 if i > 0 {
1525 out.push_str(", ");
1526 }
1527 render_object_entry_inline(out, entry);
1528 }
1529 out.push_str(" }");
1530 }
1531 }
1532 // `multiline` is ignored here deliberately, for the same reason
1533 // `TsExpr::Object`'s own arm just above ignores it: this recursion
1534 // has no `depth` to render a multi-line array correctly against.
1535 // Every real `bynk-emit` call site that needs `multiline: true`
1536 // reaches it through `render_stmt_level_expr` instead.
1537 TsExpr::Array { items, .. } => {
1538 out.push('[');
1539 render_expr_list(out, items);
1540 out.push(']');
1541 }
1542 TsExpr::TemplateLit { parts, exprs } => {
1543 out.push('`');
1544 for (i, part) in parts.iter().enumerate() {
1545 out.push_str(part);
1546 if let Some(e) = exprs.get(i) {
1547 out.push_str("${");
1548 render_expr(out, e);
1549 out.push('}');
1550 }
1551 }
1552 out.push('`');
1553 }
1554 TsExpr::Await(inner) => {
1555 out.push_str("await ");
1556 render_operand(out, inner);
1557 }
1558 TsExpr::As { expr, ty } => {
1559 // `(a ?? b) as T` / `((x) => y) as T` / `((a ? b : c)) as T` — a
1560 // binary expression, an arrow, or a conditional all need parens
1561 // before `as` (`(x) => y as T` binds the cast to the arrow's own
1562 // *body*, not the whole arrow — review of #1322, finding 1;
1563 // #1323 adds `Conditional` proactively, the same class of gap).
1564 // `Await` is deliberately NOT in this list (corrected by #1323):
1565 // `as` binds looser than `await`, so `await x as T` already
1566 // parses as `(await x) as T` with no parens needed — P7.8's
1567 // original reasoning conflated "this file's real text has
1568 // parens" with "parens are grammatically required"; `workers_
1569 // entry.rs`'s own real `await request.json() as JsonValue` has
1570 // none. A caller whose own real content *does* want them (like
1571 // `events_fanout.rs`'s own historical text) wraps its own
1572 // `Await` in an explicit [`TsExpr::Paren`] instead of relying on
1573 // an implicit per-shape rule here.
1574 let needs_parens = matches!(
1575 **expr,
1576 TsExpr::Binary { .. } | TsExpr::Arrow { .. } | TsExpr::Conditional { .. }
1577 );
1578 if needs_parens {
1579 out.push('(');
1580 render_expr(out, expr);
1581 out.push(')');
1582 } else {
1583 render_expr(out, expr);
1584 }
1585 out.push_str(" as ");
1586 render_type(out, ty);
1587 }
1588 TsExpr::Unary { op, expr } => {
1589 out.push_str(match op {
1590 TsUnaryOp::Not => "!",
1591 TsUnaryOp::Typeof => "typeof ",
1592 });
1593 render_operand(out, expr);
1594 }
1595 TsExpr::Binary { op, left, right } => {
1596 render_binary_operand(out, *op, left, true);
1597 out.push_str(match op {
1598 TsBinaryOp::NullishCoalescing => " ?? ",
1599 TsBinaryOp::Or => " || ",
1600 TsBinaryOp::And => " && ",
1601 TsBinaryOp::StrictEq => " === ",
1602 TsBinaryOp::StrictNotEq => " !== ",
1603 TsBinaryOp::GreaterThan => " > ",
1604 TsBinaryOp::LessThan => " < ",
1605 TsBinaryOp::InstanceOf => " instanceof ",
1606 TsBinaryOp::Add => " + ",
1607 TsBinaryOp::In => " in ",
1608 TsBinaryOp::GreaterThanEq => " >= ",
1609 TsBinaryOp::LessThanEq => " <= ",
1610 });
1611 render_binary_operand(out, *op, right, false);
1612 }
1613 TsExpr::Conditional {
1614 test,
1615 consequent,
1616 alternate,
1617 } => {
1618 // `test` prints like a `ShortCircuitExpression` (the real JS/TS
1619 // grammar position) — an `Arrow` or a nested `Conditional` there
1620 // needs parens (grammar), a `Binary`/`As`/anything else doesn't.
1621 // `consequent`/`alternate` are `AssignmentExpression` positions
1622 // and admit a nested `Arrow`/`Conditional` with no parens at all
1623 // (right-associative chaining, e.g. `a ? b : c ? d : e`, is
1624 // already the correct reading) — neither needs the operand
1625 // machinery the way `test` does.
1626 let test_needs_parens =
1627 matches!(**test, TsExpr::Arrow { .. } | TsExpr::Conditional { .. });
1628 if test_needs_parens {
1629 out.push('(');
1630 render_expr(out, test);
1631 out.push(')');
1632 } else {
1633 render_expr(out, test);
1634 }
1635 out.push_str(" ? ");
1636 render_expr(out, consequent);
1637 out.push_str(" : ");
1638 render_expr(out, alternate);
1639 }
1640 TsExpr::Paren(inner) => {
1641 out.push('(');
1642 render_expr(out, inner);
1643 out.push(')');
1644 }
1645 TsExpr::Lit(lit) => render_lit(out, lit),
1646 }
1647}
1648
1649/// [`TsExpr::Arrow`]'s own `body` renderer — see [`TsArrowBody`]'s own doc
1650/// for why there are exactly two shapes. `Expr` is the ordinary case, no
1651/// braces, the same plain `render_expr` recursion every arrow used before
1652/// #1435. `Block` reuses [`render_compact_stmts`] (the same shared core
1653/// `TsStmtKind::InlineBlock`'s own renderer already reduces to), not a
1654/// fresh copy of its "one physical line, semicolon-separated" logic — every
1655/// real `Block` site today (`serialisation.rs`'s `Float` guard) is exactly
1656/// this one-line-IIFE shape, so no trailing newline is added here (unlike
1657/// `render_inline_block`'s own statement-position form): this text is
1658/// embedded mid-expression, not standing alone as a statement.
1659fn render_arrow_body(out: &mut String, body: &TsArrowBody) {
1660 match body {
1661 TsArrowBody::Expr(expr) => render_expr(out, expr),
1662 TsArrowBody::Block(stmts) => {
1663 out.push_str("{ ");
1664 render_compact_stmts(out, stmts);
1665 out.push_str(" }");
1666 }
1667 }
1668}
1669
1670fn render_expr_list(out: &mut String, exprs: &[TsExpr]) {
1671 for (i, e) in exprs.iter().enumerate() {
1672 if i > 0 {
1673 out.push_str(", ");
1674 }
1675 render_expr(out, e);
1676 }
1677}
1678
1679fn render_lit(out: &mut String, lit: &TsLit) {
1680 match lit {
1681 TsLit::Str(s) => {
1682 out.push('"');
1683 // Matches `bynk_check::wire_default::escape_ts_literal`
1684 // byte-for-byte (review of #1314, finding 1) — the previous
1685 // version escaped only `"`/`\`, so a `\n`/`\t`/`\r` inside a
1686 // literal printed raw into the output, an unterminated string
1687 // literal `tsc` can't parse. `bynk-ts` can't depend on
1688 // `bynk-check` to share that function directly (this crate's
1689 // own module doc: `bynk-syntax` only) — inlined instead, kept
1690 // deliberately identical rather than reinvented, since both
1691 // this printer and `bynk-emit`'s `escape_ts_string` splice into
1692 // generated TypeScript and must never disagree.
1693 for c in s.chars() {
1694 match c {
1695 '\\' => out.push_str("\\\\"),
1696 '"' => out.push_str("\\\""),
1697 '\n' => out.push_str("\\n"),
1698 '\t' => out.push_str("\\t"),
1699 '\r' => out.push_str("\\r"),
1700 c => out.push(c),
1701 }
1702 }
1703 out.push('"');
1704 }
1705 TsLit::Num(n) => out.push_str(n),
1706 TsLit::Null => out.push_str("null"),
1707 TsLit::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
1708 // The whole literal, printed exactly as given — see `TsLit::Raw`'s
1709 // own doc for why: this is the one place in this printer that must
1710 // NOT apply `Str`'s own escaping.
1711 TsLit::Raw(text) => out.push_str(text),
1712 }
1713}
1714
1715fn render_type(out: &mut String, ty: &TsType) {
1716 match ty {
1717 TsType::Named { name, type_args } => {
1718 out.push_str(name);
1719 if !type_args.is_empty() {
1720 out.push('<');
1721 for (i, arg) in type_args.iter().enumerate() {
1722 if i > 0 {
1723 out.push_str(", ");
1724 }
1725 render_type(out, arg);
1726 }
1727 out.push('>');
1728 }
1729 }
1730 TsType::Array { element, readonly } => {
1731 if *readonly {
1732 out.push_str("readonly ");
1733 }
1734 render_type(out, element);
1735 out.push_str("[]");
1736 }
1737 TsType::Object(members) => {
1738 // Review of #1358, finding 1: a `Method` member's own `doc` has
1739 // no single-line form — this inline shape has nowhere to put a
1740 // JSDoc block, unlike `TsDecl::Interface`'s own render arm
1741 // (which calls `render_doc_comment` before a documented
1742 // member's own line, since it has `depth` and a real newline
1743 // budget to work with). The identical case #1338's own review
1744 // already ruled on for `TsObjectEntry::Method.doc` in
1745 // `render_object_entry_inline` — a loud check, not a silent
1746 // drop, since silently dropping a JSDoc block is a strictly
1747 // worse failure mode than a loud one. Not reachable today
1748 // (`workers_entry.rs`'s own one real `TsType::Object` method
1749 // member goes through `TsTypeMember::method`, so `doc` is
1750 // always `None`) — the #1337 case was equally unreachable,
1751 // which is exactly why it got the assert.
1752 debug_assert!(
1753 !members
1754 .iter()
1755 .any(|m| matches!(m, TsTypeMember::Method { doc: Some(_), .. })),
1756 "render_type: a Method member's own doc comment has no single-line form"
1757 );
1758 if members.is_empty() {
1759 out.push_str("{}");
1760 } else {
1761 out.push_str("{ ");
1762 for (i, m) in members.iter().enumerate() {
1763 if i > 0 {
1764 out.push_str("; ");
1765 }
1766 render_type_member(out, m);
1767 }
1768 out.push_str(" }");
1769 }
1770 }
1771 TsType::Fn { params, ret } => {
1772 out.push('(');
1773 for (i, p) in params.iter().enumerate() {
1774 if i > 0 {
1775 out.push_str(", ");
1776 }
1777 out.push('a');
1778 out.push_str(&i.to_string());
1779 out.push_str(": ");
1780 render_type(out, p);
1781 }
1782 out.push_str(") => ");
1783 render_type(out, ret);
1784 }
1785 TsType::Union {
1786 members,
1787 multiline: false,
1788 } => {
1789 for (i, m) in members.iter().enumerate() {
1790 if i > 0 {
1791 out.push_str(" | ");
1792 }
1793 render_type(out, m);
1794 }
1795 }
1796 // #1339: `emit_sum_type`'s own real shape — one variant per line, a
1797 // leading `|` on every line except the first (which gets equivalent
1798 // spacing instead, matching the pre-conversion `writeln!` code's own
1799 // `let pipe = if i == 0 { " " } else { "|" };` exactly), the closing
1800 // `;` appended by `TsDecl::TypeAlias`'s own caller directly after
1801 // the last member's own line — see `TsType::Union`'s own doc for
1802 // why this needs no depth parameter despite the multi-line shape.
1803 TsType::Union {
1804 members,
1805 multiline: true,
1806 } => {
1807 for (i, m) in members.iter().enumerate() {
1808 if i > 0 {
1809 out.push('\n');
1810 }
1811 out.push_str(" ");
1812 out.push_str(if i == 0 { " " } else { "|" });
1813 out.push(' ');
1814 render_type(out, m);
1815 }
1816 }
1817 TsType::Intersection(members) => {
1818 for (i, m) in members.iter().enumerate() {
1819 if i > 0 {
1820 out.push_str(" & ");
1821 }
1822 render_type(out, m);
1823 }
1824 }
1825 }
1826}
1827
1828/// One [`TsTypeMember`] rendered inline (no leading indent, no trailing
1829/// separator) — [`render_type`]'s own `TsType::Object` arm's per-member
1830/// renderer.
1831fn render_type_member(out: &mut String, member: &TsTypeMember) {
1832 match member {
1833 TsTypeMember::Prop {
1834 name,
1835 ty,
1836 optional,
1837 readonly,
1838 } => {
1839 if *readonly {
1840 out.push_str("readonly ");
1841 }
1842 out.push_str(name);
1843 if *optional {
1844 out.push('?');
1845 }
1846 out.push_str(": ");
1847 render_type(out, ty);
1848 }
1849 TsTypeMember::Method {
1850 name,
1851 generics,
1852 params,
1853 ret,
1854 doc: _,
1855 } => {
1856 out.push_str(name);
1857 render_bare_generics(out, generics);
1858 out.push('(');
1859 render_params(out, params);
1860 out.push_str("): ");
1861 render_type(out, ret);
1862 }
1863 TsTypeMember::Index {
1864 key_name,
1865 key_ty,
1866 value_ty,
1867 } => {
1868 out.push('[');
1869 out.push_str(key_name);
1870 out.push_str(": ");
1871 render_type(out, key_ty);
1872 out.push_str("]: ");
1873 render_type(out, value_ty);
1874 }
1875 }
1876}
1877
1878/// Print a single [`TsType`] on its own — the real callers this closes R7.2
1879/// for (`bynk-emit`'s `ts_type_ref*`/`ts_ty` families, P7.9, #1315) each
1880/// want one type fragment to interpolate into a larger, still-hand-built
1881/// line (a field's own type annotation, a parameter list, …), not a whole
1882/// [`TsProgram`]. No source-map/buffer machinery — [`print()`] owns that
1883/// for a whole document; this is the printer's other, narrower entry
1884/// point, sharing the same internal recursion rather than a second copy.
1885pub fn print_type(ty: &TsType) -> String {
1886 let mut out = String::new();
1887 render_type(&mut out, ty);
1888 out
1889}
1890
1891/// Print a single [`TsExpr`] on its own — the expression-level sibling of
1892/// [`print_type`]'s own "one fragment, not a whole document" entry point.
1893/// Arc C, step (11) (#1388) need: `emit_icu_placeholder`'s own `Select` arm
1894/// stays one opaque, hand-built block-bodied IIFE (at the time, `TsExpr::
1895/// Arrow` had no block-body variant at all, and extending it for that one
1896/// site was rejected as disproportionate) — its own arm VALUES,
1897/// `emit_sub_message`'s now-real `TsExpr` results, still need stringifying
1898/// back into that opaque host text. #1435 (Arc E slice 1) later added
1899/// [`TsArrowBody::Block`] for a genuinely different real site
1900/// (`serialisation.rs`'s `Float` guard); `emit_icu_placeholder`'s own
1901/// `Select` arm was not reconverted along with it — out of that slice's own
1902/// scope — so this call site's opaque text stays exactly as it was. No
1903/// source-map/buffer machinery, matching [`print_type`]/[`print_stmt`]'s
1904/// own scope exactly.
1905pub fn print_expr(expr: &TsExpr) -> String {
1906 let mut out = String::new();
1907 render_expr(&mut out, expr);
1908 out
1909}
1910
1911/// Print a single [`TsStmt`] on its own, at `depth` — the statement-level
1912/// sibling of [`print_type`]'s own "one fragment, not a whole document"
1913/// entry point. `bynk-emit`'s own #1333 need (`emit_doc_block`, a shared
1914/// helper spliced into ~14 still-unconverted callers' own buffers) wants
1915/// one statement's own printed text at a caller-supplied depth, not a
1916/// whole [`TsProgram`] — no source-map/buffer machinery, matching
1917/// [`print_type`]'s own scope exactly, and reusing `render_stmt`'s own
1918/// exhaustive per-kind dispatch (this module's own private renderer)
1919/// rather than a second copy.
1920/// Review of #1402: a `TsStmtKind::Raw` nested inside a `Switch` case's own
1921/// body (`emit_stub_rhs`'s own `ReturnsEach` dispatch, Arc C slice 33,
1922/// `tests_emit.rs` slice C) carries the identical "no indent of its own,
1923/// pre-indented at a fixed absolute depth" hazard `render_class_method`'s
1924/// and `render_multiline_object_entry`'s own `debug_assert!`s already guard
1925/// — `stmt_contains_raw` already recurses into `Switch` cases, so the same
1926/// check applies here, this fragment entry point's own first `Raw`-bearing
1927/// `Switch` caller. A bare `Raw` passed directly as `stmt` itself is exempt
1928/// (not a false negative — `render_stmt`'s own `Raw` arm never reads
1929/// `depth` at all, so calling `print_stmt` on a bare `Raw` is safe at any
1930/// depth, the established `print_stmt_renders_raw_text_verbatim_with_no_
1931/// added_indent_or_punctuation` contract below): only a Raw *nested inside*
1932/// a depth-using wrapper (like this `Switch` case) is the real hazard.
1933pub fn print_stmt(stmt: &TsStmt, depth: usize) -> String {
1934 debug_assert!(
1935 depth == 0 || matches!(stmt.kind, TsStmtKind::Raw(_)) || !stmt_contains_raw(stmt),
1936 "print_stmt: a Raw-bearing statement's own baked-in indent only matches depth 0"
1937 );
1938 let mut out = String::new();
1939 render_stmt(&mut out, stmt, depth, None);
1940 out
1941}
1942
1943/// [`print_stmt`]'s own sibling (#1477), the `TsDecl::Function`-body
1944/// counterpart to [`print_class_method_and_merge`]: identical rendering,
1945/// but merges any `nested_map` `stmt` carries — on `stmt` itself, or on
1946/// anything nested inside it (a function's body, a class's constructor/
1947/// methods, …) — into `map` as it prints, at the real print-time offset —
1948/// the same offset `bynk-emit`'s own `emit_free_fn` (`emitter/emit.rs`) used
1949/// to recover by exact arithmetic over the returned text, guarded by a
1950/// `debug_assert!`, before #1480 converted it to set `nested_map` directly
1951/// instead. Kept separate from `print_stmt` itself so every existing
1952/// caller's own call sites are untouched. `source_id` is `map`'s
1953/// own registered source this statement's content belongs to (see
1954/// this crate's own private `MergeTarget`'s own doc for why this can't just be hardcoded).
1955///
1956/// **Appends to the caller's own `out`, unlike `print_stmt`'s own
1957/// return-a-fresh-`String` shape** — the merge computes each checkpoint's
1958/// absolute offset from `out.len()` *as this statement's own text is
1959/// written*, so it must be the caller's real, already-populated buffer, not
1960/// a throwaway local one starting at 0. Returning a fresh string here (the
1961/// first shape this function took, caught before merge) would silently
1962/// record every checkpoint at the wrong offset the moment the caller
1963/// spliced the returned text anywhere but the very start of its own buffer.
1964pub fn print_stmt_and_merge(
1965 out: &mut String,
1966 stmt: &TsStmt,
1967 depth: usize,
1968 map: &mut SourceMapBuilder,
1969 source_id: usize,
1970) {
1971 debug_assert!(
1972 depth == 0 || matches!(stmt.kind, TsStmtKind::Raw(_)) || !stmt_contains_raw(stmt),
1973 "print_stmt_and_merge: a Raw-bearing statement's own baked-in indent only matches depth 0"
1974 );
1975 render_stmt(out, stmt, depth, Some(MergeTarget { map, source_id }));
1976}
1977
1978/// Print a single [`TsObjectEntry`] on its own, at `depth` — the
1979/// object-entry sibling of [`print_stmt`]'s own "one fragment, not a whole
1980/// document" entry point, `depth` meaning the SAME thing it does for this
1981/// crate's own internal multi-line-object renderer: the *object's* own
1982/// depth, so the entry itself lands one level deeper, matching an object built by that
1983/// renderer exactly. #1337's own real need: `emit_attached_methods` (a
1984/// shared helper spliced into `emit_refined_type`/`emit_record_type`/
1985/// `emit_sum_type`'s own still-unconverted `&mut String` buffers) now
1986/// returns `Vec<TsObjectEntry>` instead of writing text directly — each
1987/// caller renders the returned entries one at a time through this, the
1988/// same P7.9/#1333 "keep the caller's own signature, print just the
1989/// fragment" pattern applied to an object-entry-shaped fragment instead of
1990/// a whole statement or type.
1991///
1992/// Review of #1338, finding 3: a `TsStmtKind::Raw` body statement (e.g.
1993/// `emit_method`'s own opaque `lower.rs`-sourced body, or `emit_refined_
1994/// type`'s own opaque `emit_refined_checks`-sourced guard body, #1339's
1995/// second real use) carries NO indent of its own — its text is captured
1996/// pre-indented at a fixed absolute depth by its own caller — so it only
1997/// renders correctly when `depth` is `0`. The guard for this now lives in
1998/// `render_multiline_object_entry` itself (a private renderer, so named
1999/// here in text rather than linked — moved there by review of #1340,
2000/// finding 1: this function's own copy missed the `TsExpr::
2001/// multiline_object_entries`/`render_expr` call path #1339 added, which
2002/// reaches that renderer directly, never through this one), so it fires
2003/// for every caller, not just this entry point.
2004pub fn print_object_entry(entry: &TsObjectEntry, depth: usize) -> String {
2005 let mut out = String::new();
2006 render_multiline_object_entry(&mut out, entry, depth, None);
2007 out
2008}
2009
2010/// [`print_object_entry`]'s own sibling (#1477), the `TsObjectEntry::Method`
2011/// counterpart to [`print_class_method_and_merge`]/[`print_stmt_and_merge`]:
2012/// identical rendering, but merges a `Method` entry's own body `nested_map`
2013/// into `map` as it prints, at the real print-time offset. Kept separate
2014/// from `print_object_entry` itself so every existing caller's own call
2015/// sites are untouched.
2016///
2017/// Appends to the caller's own `out`, the same "must be the real buffer,
2018/// not a throwaway local one" reasoning [`print_stmt_and_merge`]'s own doc
2019/// explains in full. `source_id`: see this crate's own private `MergeTarget`'s own doc.
2020pub fn print_object_entry_and_merge(
2021 out: &mut String,
2022 entry: &TsObjectEntry,
2023 depth: usize,
2024 map: &mut SourceMapBuilder,
2025 source_id: usize,
2026) {
2027 render_multiline_object_entry(out, entry, depth, Some(MergeTarget { map, source_id }));
2028}
2029
2030/// Whether `stmts`, or any block/branch/case nested inside them at any
2031/// depth, contains a [`TsStmtKind::Raw`] — the recursive check
2032/// [`render_class_method`]'s and [`render_multiline_object_entry`]'s own
2033/// `debug_assert!`s need. Review of #1374: a top-level-only scan
2034/// (`body.iter().any(...)`) is blind to a `Raw` buried inside an `If`'s own
2035/// `Block` — `emit_agent`'s own `commitState` (#1373) is exactly this shape,
2036/// a transition's own hoisted-statement `Raw`s sitting inside
2037/// `if (__prior !== undefined) { ... }` — so the two guards this exists for
2038/// would have silently missed exactly the case they exist to catch. Every
2039/// `TsStmtKind` variant is named explicitly, not a wildcard, matching this
2040/// crate's own exhaustiveness discipline.
2041fn contains_raw(stmts: &[TsStmt]) -> bool {
2042 stmts.iter().any(stmt_contains_raw)
2043}
2044
2045fn stmt_contains_raw(stmt: &TsStmt) -> bool {
2046 match &stmt.kind {
2047 TsStmtKind::Raw(_) => true,
2048 TsStmtKind::Verbatim { .. }
2049 | TsStmtKind::Decl(_)
2050 | TsStmtKind::Const { .. }
2051 | TsStmtKind::Let { .. }
2052 | TsStmtKind::ExprStmt(_)
2053 | TsStmtKind::Return(_)
2054 | TsStmtKind::Throw(_)
2055 | TsStmtKind::Continue
2056 | TsStmtKind::Assign { .. }
2057 | TsStmtKind::Comment(_)
2058 | TsStmtKind::DocComment(_)
2059 | TsStmtKind::Blank
2060 | TsStmtKind::Increment(_) => false,
2061 TsStmtKind::If {
2062 then_branch,
2063 else_branch,
2064 ..
2065 } => {
2066 stmt_contains_raw(then_branch) || else_branch.as_deref().is_some_and(stmt_contains_raw)
2067 }
2068 TsStmtKind::ForOf { body, .. } | TsStmtKind::For { body, .. } => stmt_contains_raw(body),
2069 TsStmtKind::TryCatch {
2070 try_block,
2071 catch_block,
2072 ..
2073 } => stmt_contains_raw(try_block) || stmt_contains_raw(catch_block),
2074 TsStmtKind::Block(stmts) | TsStmtKind::InlineBlock(stmts) => contains_raw(stmts),
2075 TsStmtKind::Switch { cases, .. } => cases.iter().any(|c| contains_raw(&c.body)),
2076 }
2077}
2078
2079/// Print a single [`TsClassMethod`] on its own, at `depth` — the
2080/// class-method sibling of [`print_object_entry`]'s own "one fragment, not
2081/// a whole document" entry point. `depth` means the *class's* own depth
2082/// (matching `print_object_entry`'s own convention exactly), so the method
2083/// itself lands at `depth + 1`, its own body at `depth + 2`. #1359's own
2084/// real need: `emit_provider`'s own class wrapper stays hand-written text
2085/// (each method's own body needs a per-method source-map sub-builder/
2086/// `merge`, and the wrapper's own real spacing — no blank line between
2087/// methods — genuinely differs from [`TsDecl::Class`]'s own "one blank
2088/// line before each method" policy, `events_fanout.rs`'s real convention,
2089/// #1317), so each real method prints through here directly into that
2090/// still-hand-written wrapper. Deliberately no automatic blank-line
2091/// insertion of its own — the same "caller controls spacing" contract
2092/// `print_object_entry` already established.
2093pub fn print_class_method(method: &TsClassMethod, depth: usize) -> String {
2094 let mut out = String::new();
2095 render_class_method(&mut out, method, depth, None);
2096 out
2097}
2098
2099/// [`print_class_method`]'s own sibling (#1477): identical rendering, but
2100/// merges any of `method.body`'s own direct-child `nested_map`s into `map`
2101/// as they print, at the real print-time offset — no reverse-engineering
2102/// the offset from the returned text afterward, the way `bynk-emit`'s own
2103/// `emit_class_method_and_merge_source_map` (`emitter/emit.rs`) has to
2104/// today. Kept as a separate function, not an added parameter on
2105/// `print_class_method` itself, so every existing caller's own call sites
2106/// are untouched — this is a strict addition.
2107///
2108/// Appends to the caller's own `out`, the same "must be the real buffer,
2109/// not a throwaway local one" reasoning [`print_stmt_and_merge`]'s own doc
2110/// explains in full. `source_id`: see this crate's own private `MergeTarget`'s own doc.
2111pub fn print_class_method_and_merge(
2112 out: &mut String,
2113 method: &TsClassMethod,
2114 depth: usize,
2115 map: &mut SourceMapBuilder,
2116 source_id: usize,
2117) {
2118 render_class_method(out, method, depth, Some(MergeTarget { map, source_id }));
2119}
2120
2121/// The one real per-method render, shared by [`print_class_method`] and
2122/// [`TsDecl::Class`]'s own methods-loop arm — review of #1360, finding 4:
2123/// keeping two independent copies of this shape was exactly the drift risk
2124/// this track exists to design against (two printers disagreeing about a
2125/// formatting convention). `depth` means the *class's* own depth, matching
2126/// both callers' own convention.
2127///
2128/// Review of #1360, finding 1: a `TsStmtKind::Raw` method body has no
2129/// indent of its own — its text is captured pre-indented at a fixed
2130/// absolute depth by its own caller — so it only renders correctly when
2131/// `depth` is `0`, the same hazard `render_multiline_object_entry`'s own
2132/// identical `debug_assert!` guards (review of #1338 finding 3, relocated
2133/// by review of #1340 finding 1). `print_class_method`'s own doc frames it
2134/// as a general fragment entry point, so a future caller at a non-zero
2135/// depth is the expected case this guards against, not a hypothetical.
2136///
2137/// `map`: see [`render_block_stmts`]'s own doc (#1477) — forwarded
2138/// unchanged to `method.body`'s own rendering.
2139fn render_class_method(
2140 out: &mut String,
2141 method: &TsClassMethod,
2142 depth: usize,
2143 map: Option<MergeTarget<'_>>,
2144) {
2145 debug_assert!(
2146 depth == 0 || !contains_raw(&method.body),
2147 "render_class_method: a Raw method body's own baked-in indent only matches depth 0"
2148 );
2149 if let Some(text) = &method.doc {
2150 render_doc_comment(out, text, depth + 1);
2151 }
2152 out.push_str(&indent(depth + 1));
2153 if method.private {
2154 out.push_str("private ");
2155 }
2156 if method.is_async {
2157 out.push_str("async ");
2158 }
2159 out.push_str(&method.name);
2160 out.push('(');
2161 render_params(out, &method.params);
2162 out.push(')');
2163 if let Some(rt) = &method.return_type {
2164 out.push_str(": ");
2165 render_type(out, rt);
2166 }
2167 render_block_stmts(out, &method.body, depth + 1, map);
2168 out.push('\n');
2169}
2170
2171fn render_params(out: &mut String, params: &[TsParam]) {
2172 for (i, p) in params.iter().enumerate() {
2173 if i > 0 {
2174 out.push_str(", ");
2175 }
2176 out.push_str(&p.name);
2177 if p.optional {
2178 out.push('?');
2179 }
2180 if let Some(ty) = &p.ty {
2181 out.push_str(": ");
2182 render_type(out, ty);
2183 }
2184 }
2185}
2186
2187fn render_decl(out: &mut String, decl: &TsDecl, depth: usize, map: Option<MergeTarget<'_>>) {
2188 // `render_decl_body`'s own `TsDecl::Export` arm already writes
2189 // `"export "` before recursing into the inner declaration — the
2190 // `if let Export` special case this replaced produced byte-identical
2191 // output by duplicating that same logic one layer up (review of
2192 // #1314, smaller note: the two branches were provably the same for
2193 // every input, so only one needs to exist).
2194 out.push_str(&indent(depth));
2195 render_decl_body(out, decl, depth, map);
2196}
2197
2198/// The declaration's own text, with no leading indent — [`render_decl`]
2199/// writes the indent (and, for `Export`, the `export ` keyword) once, then
2200/// hands off here so `Export(inner)` doesn't duplicate `inner`'s own
2201/// leading whitespace.
2202///
2203/// `map`: see [`render_block_stmts`]'s own doc (#1477) — reaches a
2204/// `TsDecl::Function`'s own body, and a `TsDecl::Class`'s own constructor/
2205/// methods, unchanged; every other arm ignores it (no body of its own to
2206/// carry a `nested_map`).
2207fn render_decl_body(
2208 out: &mut String,
2209 decl: &TsDecl,
2210 depth: usize,
2211 mut map: Option<MergeTarget<'_>>,
2212) {
2213 match decl {
2214 TsDecl::Import {
2215 type_only,
2216 names,
2217 from,
2218 } => {
2219 out.push_str(if *type_only {
2220 "import type { "
2221 } else {
2222 "import { "
2223 });
2224 out.push_str(&names.join(", "));
2225 out.push_str(" } from \"");
2226 out.push_str(from);
2227 out.push_str("\";\n");
2228 }
2229 TsDecl::ImportNamespace {
2230 type_only,
2231 alias,
2232 from,
2233 } => {
2234 out.push_str(if *type_only {
2235 "import type * as "
2236 } else {
2237 "import * as "
2238 });
2239 out.push_str(alias);
2240 out.push_str(" from \"");
2241 out.push_str(from);
2242 out.push_str("\";\n");
2243 }
2244 TsDecl::ImportDefault { alias, from } => {
2245 out.push_str("import ");
2246 out.push_str(alias);
2247 out.push_str(" from \"");
2248 out.push_str(from);
2249 out.push_str("\";\n");
2250 }
2251 TsDecl::ReExport { names, from } => {
2252 out.push_str("export { ");
2253 out.push_str(&names.join(", "));
2254 out.push_str(" } from \"");
2255 out.push_str(from);
2256 out.push_str("\";\n");
2257 }
2258 TsDecl::ReExportAll { from } => {
2259 out.push_str("export * from \"");
2260 out.push_str(from);
2261 out.push_str("\";\n");
2262 }
2263 TsDecl::Export(inner) => {
2264 out.push_str("export ");
2265 render_decl_body(out, inner, depth, map);
2266 }
2267 TsDecl::Interface {
2268 name,
2269 type_params,
2270 members,
2271 } => {
2272 out.push_str("interface ");
2273 out.push_str(name);
2274 render_bare_generics(out, type_params);
2275 out.push_str(" {\n");
2276 for member in members {
2277 // #1357: a `Method` member's own `doc` renders here, not in
2278 // `render_type_member` itself — that function has no
2279 // `depth` to give `render_doc_comment`, the same "doc lives
2280 // at the call site that carries depth" split
2281 // `render_multiline_object_entry`'s own `Method` arm
2282 // already uses for the identical field.
2283 if let TsTypeMember::Method {
2284 doc: Some(text), ..
2285 } = member
2286 {
2287 render_doc_comment(out, text, depth + 1);
2288 }
2289 out.push_str(&indent(depth + 1));
2290 render_type_member(out, member);
2291 out.push_str(";\n");
2292 }
2293 out.push_str(&indent(depth));
2294 out.push_str("}\n");
2295 }
2296 TsDecl::ConstDecl { name, ty, init } => {
2297 out.push_str("const ");
2298 out.push_str(name);
2299 if let Some(ty) = ty {
2300 out.push_str(": ");
2301 render_type(out, ty);
2302 }
2303 out.push_str(" = ");
2304 render_stmt_level_expr(out, init, depth);
2305 out.push_str(";\n");
2306 }
2307 TsDecl::Class {
2308 name,
2309 fields,
2310 constructor,
2311 methods,
2312 } => {
2313 out.push_str("class ");
2314 out.push_str(name);
2315 out.push_str(" {\n");
2316 for f in fields {
2317 out.push_str(&indent(depth + 1));
2318 if f.private {
2319 out.push_str("private ");
2320 }
2321 out.push_str(&f.name);
2322 out.push_str(": ");
2323 render_type(out, &f.ty);
2324 out.push_str(";\n");
2325 }
2326 // Readability policy (this module's own doc): no blank line
2327 // between fields and the constructor; one blank line before
2328 // each method — `events_fanout.rs`'s own real class spacing.
2329 let mut wrote_member = !fields.is_empty();
2330 if let Some(ctor) = constructor {
2331 out.push_str(&indent(depth + 1));
2332 out.push_str("constructor(");
2333 render_params(out, &ctor.params);
2334 out.push(')');
2335 render_block_stmts(
2336 out,
2337 &ctor.body,
2338 depth + 1,
2339 map.as_mut().map(|t| t.reborrow()),
2340 );
2341 out.push('\n');
2342 wrote_member = true;
2343 }
2344 for m in methods {
2345 if wrote_member {
2346 out.push('\n');
2347 }
2348 render_class_method(out, m, depth, map.as_mut().map(|t| t.reborrow()));
2349 wrote_member = true;
2350 }
2351 out.push_str(&indent(depth));
2352 out.push_str("}\n");
2353 }
2354 TsDecl::Function {
2355 name,
2356 generics,
2357 params,
2358 return_type,
2359 body,
2360 is_async,
2361 inline,
2362 } => {
2363 if *is_async {
2364 out.push_str("async ");
2365 }
2366 out.push_str("function ");
2367 out.push_str(name);
2368 render_bare_generics(out, generics);
2369 out.push('(');
2370 render_params(out, params);
2371 out.push(')');
2372 if let Some(rt) = return_type {
2373 out.push_str(": ");
2374 render_type(out, rt);
2375 }
2376 if *inline {
2377 out.push(' ');
2378 render_inline_block(out, body);
2379 } else {
2380 render_block_stmts(out, body, depth, map);
2381 out.push('\n');
2382 }
2383 }
2384 TsDecl::TypeAlias {
2385 name,
2386 type_params,
2387 ty,
2388 } => {
2389 out.push_str("type ");
2390 out.push_str(name);
2391 render_bare_generics(out, type_params);
2392 // #1339: a multiline Union's own first rendered character is
2393 // its first member's leading indent, not a newline — the `=`
2394 // itself must be followed directly by `\n` here (no space
2395 // before it), matching the pre-conversion `writeln!(out,
2396 // "export type {name}{params} =")` line's own exact bytes;
2397 // every other `ty` keeps the ordinary `" = "` (space both
2398 // sides) ordinary single-line form.
2399 if matches!(
2400 ty,
2401 TsType::Union {
2402 multiline: true,
2403 ..
2404 }
2405 ) {
2406 out.push_str(" =\n");
2407 } else {
2408 out.push_str(" = ");
2409 }
2410 render_type(out, ty);
2411 out.push_str(";\n");
2412 }
2413 TsDecl::ExportDefault(expr) => {
2414 out.push_str("export default ");
2415 render_stmt_level_expr(out, expr, depth);
2416 out.push_str(";\n");
2417 }
2418 TsDecl::DeclareConst { name, ty } => {
2419 out.push_str("declare const ");
2420 out.push_str(name);
2421 out.push_str(": ");
2422 render_type(out, ty);
2423 out.push_str(";\n");
2424 }
2425 }
2426}
2427
2428/// `{ <stmts> }` for a constructor/method body — same shape as
2429/// [`render_block_body`] but over a plain `&[TsStmt]` (a `TsClassCtor`/
2430/// `TsClassMethod`'s own `body` field, not a boxed `TsStmt`), and always
2431/// followed by the caller's own `\n` (constructor/method declarations, not
2432/// a same-line `try`/`catch` continuation).
2433///
2434/// `map: Some(_)` when the caller wants a `nested_map`-bearing statement
2435/// reachable from `stmts` merged into a module source map as it prints
2436/// (#1477) — see [`crate::program::TsStmt::nested_map`]'s own doc for the
2437/// problem this solves. Reborrowed once per child and forwarded to
2438/// [`render_stmt`], which does the actual check — this function itself
2439/// holds none of that logic.
2440fn render_block_stmts(
2441 out: &mut String,
2442 stmts: &[TsStmt],
2443 depth: usize,
2444 mut map: Option<MergeTarget<'_>>,
2445) {
2446 out.push_str(" {\n");
2447 for s in stmts {
2448 render_stmt(out, s, depth + 1, map.as_mut().map(|t| t.reborrow()));
2449 }
2450 out.push_str(&indent(depth));
2451 out.push('}');
2452}
2453
2454#[cfg(test)]
2455mod tests {
2456 use super::*;
2457 use crate::program::{TsClassCtor, TsClassField, TsClassMethod, TsSwitchCase, VerbatimOrigin};
2458 use bynk_syntax::span::Span;
2459
2460 /// Pins the readability policy's statement-separation guarantee (R7.5,
2461 /// this module's own doc): every statement starts on its own generated
2462 /// line.
2463 #[test]
2464 fn prints_every_statement_in_order() {
2465 let mut program = TsProgram::new();
2466 program.push(TsStmt::verbatim(
2467 VerbatimOrigin::Contracts,
2468 "const a = 1;\n",
2469 None,
2470 ));
2471 program.push(TsStmt::verbatim(
2472 VerbatimOrigin::Secrets,
2473 "const b = 2;\n",
2474 None,
2475 ));
2476 let printed = print(&program, "x.bynk", "", "x.ts");
2477 assert_eq!(printed.text, "const a = 1;\nconst b = 2;\n");
2478 }
2479
2480 /// Pins the readability policy's statement-separation guarantee (R7.5,
2481 /// this module's own doc) in its sharpest form: nothing requires
2482 /// `Verbatim` text to be newline-terminated — the printer owns line
2483 /// structure (R7.3), so it's the one that guarantees two statements
2484 /// never share a generated line, not a `TsStmt::verbatim` caller
2485 /// obligation nobody enforces (review of #1308, finding 2).
2486 #[test]
2487 fn a_statement_missing_its_own_trailing_newline_still_gets_its_own_line() {
2488 let mut program = TsProgram::new();
2489 program.push(TsStmt::verbatim(
2490 VerbatimOrigin::Contracts,
2491 "const a = 1;",
2492 None,
2493 ));
2494 program.push(TsStmt::verbatim(
2495 VerbatimOrigin::Secrets,
2496 "const b = 2;",
2497 None,
2498 ));
2499 let printed = print(&program, "x.bynk", "", "x.ts");
2500 assert_eq!(printed.text, "const a = 1;\nconst b = 2;\n");
2501 }
2502
2503 /// Production `Verbatim` content is rarely one line — `VerbatimOrigin::
2504 /// NotYetConverted` (P7.6) wraps a whole generated document per
2505 /// statement, so one statement routinely spans dozens of generated
2506 /// lines. Neither of the two tests above exercised that shape (review
2507 /// of #1312, finding 1) — this one confirms the readability policy's
2508 /// real invariant (the *next* statement always starts on a fresh line)
2509 /// holds for multi-line content too, not just the single-line text
2510 /// those tests happened to use.
2511 #[test]
2512 fn a_multi_line_statement_still_starts_the_next_statement_on_a_fresh_line() {
2513 let mut program = TsProgram::new();
2514 program.push(TsStmt::verbatim(
2515 VerbatimOrigin::Contracts,
2516 "function a() {\n return 1;\n}\n",
2517 None,
2518 ));
2519 program.push(TsStmt::verbatim(
2520 VerbatimOrigin::Secrets,
2521 "const b = 2;\n",
2522 None,
2523 ));
2524 let printed = print(&program, "x.bynk", "", "x.ts");
2525 assert_eq!(
2526 printed.text,
2527 "function a() {\n return 1;\n}\nconst b = 2;\n"
2528 );
2529 }
2530
2531 /// `print` decides whether to append a trailing newline by checking the
2532 /// whole *buffer*, not the statement's own text (review of #1312,
2533 /// finding 2) — an empty statement's text changes nothing, so whether
2534 /// it gets a line depends entirely on what's already in the buffer.
2535 /// First in a program, the buffer is still empty, so the check fires
2536 /// and the output opens with a blank line. Pinned here so this edge
2537 /// stays a known, deliberate reading of the code rather than an
2538 /// untested accident.
2539 #[test]
2540 fn an_empty_statement_first_in_the_program_yields_a_leading_blank_line() {
2541 let mut program = TsProgram::new();
2542 program.push(TsStmt::verbatim(VerbatimOrigin::Contracts, "", None));
2543 program.push(TsStmt::verbatim(
2544 VerbatimOrigin::Secrets,
2545 "const b = 2;\n",
2546 None,
2547 ));
2548 let printed = print(&program, "x.bynk", "", "x.ts");
2549 assert_eq!(printed.text, "\nconst b = 2;\n");
2550 }
2551
2552 /// The other half of the buffer-vs-statement-text distinction (review
2553 /// of #1312, finding 2): an empty statement following one that already
2554 /// ended its own text in `\n` contributes no line at all — the buffer
2555 /// already ends in `\n`, so the check doesn't fire, and the empty
2556 /// statement's own (non-)content and the next statement's text end up
2557 /// on what reads as one generated line for the next statement alone.
2558 #[test]
2559 fn an_empty_statement_after_a_newline_terminated_one_contributes_no_line_of_its_own() {
2560 let mut program = TsProgram::new();
2561 program.push(TsStmt::verbatim(
2562 VerbatimOrigin::Contracts,
2563 "const a = 1;\n",
2564 None,
2565 ));
2566 program.push(TsStmt::verbatim(VerbatimOrigin::Secrets, "", None));
2567 program.push(TsStmt::verbatim(
2568 VerbatimOrigin::RuntimeUse,
2569 "const c = 3;\n",
2570 None,
2571 ));
2572 let printed = print(&program, "x.bynk", "", "x.ts");
2573 assert_eq!(printed.text, "const a = 1;\nconst c = 3;\n");
2574 }
2575
2576 #[test]
2577 fn no_spans_at_all_means_no_source_map() {
2578 let mut program = TsProgram::new();
2579 program.push(TsStmt::verbatim(VerbatimOrigin::Contracts, "x;\n", None));
2580 let printed = print(&program, "x.bynk", "let x = 1\n", "x.ts");
2581 assert_eq!(printed.source_map, None);
2582 }
2583
2584 /// The property that actually matters (R7.4): the printer's own
2585 /// buffer-position bookkeeping is correct — a checkpoint recorded from a
2586 /// later statement's span still resolves to the *generated* line that
2587 /// statement's own text landed on, not (as the old splice-based
2588 /// mechanism could get wrong, #4) some other buffer's offset.
2589 ///
2590 /// Review of #1308, finding 3: the original version of this test
2591 /// asserted only that the source map's `sources` array was present —
2592 /// true even with the second statement's checkpoint silently dropped,
2593 /// exactly the failure this doc comment claims is ruled out. Asserting
2594 /// full equality against a map built independently (replicating the
2595 /// printer's own record-then-write order by hand, not calling `print`)
2596 /// actually pins both statements' own offsets.
2597 #[test]
2598 fn each_statements_span_resolves_to_its_own_generated_line() {
2599 let source = "let a = 1\nlet b = 2\n";
2600 let off_a = source.find("let a").unwrap();
2601 let off_b = source.find("let b").unwrap();
2602 let span_a = Span::new(off_a, off_a + 5);
2603 let span_b = Span::new(off_b, off_b + 5);
2604 let text_a = "const a = 1;\n";
2605 let text_b = "const b = 2;\n";
2606 let mut program = TsProgram::new();
2607 program.push(TsStmt::verbatim(
2608 VerbatimOrigin::Contracts,
2609 text_a,
2610 Some(span_a),
2611 ));
2612 program.push(TsStmt::verbatim(
2613 VerbatimOrigin::Secrets,
2614 text_b,
2615 Some(span_b),
2616 ));
2617
2618 let printed = print(&program, "x.bynk", source, "x.ts");
2619
2620 let mut expected_map = SourceMapBuilder::new();
2621 expected_map.add_source("x.bynk", source);
2622 expected_map.record(0, span_a);
2623 expected_map.record(text_a.len(), span_b);
2624 let expected = expected_map.to_v3(&printed.text, "x.ts");
2625
2626 assert_eq!(printed.source_map, expected);
2627 }
2628
2629 // -- #1477: nested source-map checkpoints on an opaque body blob. --
2630
2631 /// `print_stmt_and_merge` — the `TsDecl::Function` shape (`emit_free_fn`'s
2632 /// own real site): a `Raw` body statement's own `nested_map` merges into
2633 /// the caller's map at the real print-time offset, appended into a
2634 /// buffer that already has *other* content first (`base != 0`) — proving
2635 /// this reads the caller's own real buffer position, not a fresh
2636 /// throwaway one starting at 0 (the bug this function's own first draft
2637 /// had, caught before merge: returning a `String` computes the offset
2638 /// against that empty local buffer, silently wrong the moment the
2639 /// caller splices the result anywhere but the very start of its own
2640 /// `out`).
2641 #[test]
2642 fn print_stmt_and_merge_merges_a_function_bodys_own_nested_checkpoint() {
2643 let source = "fn f() { return 1 }\n";
2644 let ret_off = source.find("return 1").unwrap();
2645 let ret_span = Span::new(ret_off, ret_off + 8);
2646
2647 let body_text = " return 1;\n";
2648 let mut nested = SourceMapBuilder::new();
2649 nested.record(0, ret_span);
2650
2651 let mut raw = TsStmt::raw(body_text, None);
2652 raw.nested_map = Some(nested);
2653
2654 let func = TsStmt::decl(
2655 TsDecl::Function {
2656 name: "f".to_string(),
2657 generics: Vec::new(),
2658 params: Vec::new(),
2659 return_type: None,
2660 body: vec![raw],
2661 is_async: false,
2662 inline: false,
2663 },
2664 None,
2665 );
2666
2667 let mut out = String::from("// header\n");
2668 let mut map = SourceMapBuilder::new();
2669 map.add_source("x.bynk", source);
2670
2671 print_stmt_and_merge(&mut out, &func, 0, &mut map, 0);
2672
2673 assert_eq!(out, "// header\nfunction f() {\n return 1;\n}\n");
2674 let base = out.find(body_text).unwrap();
2675
2676 let mut expected = SourceMapBuilder::new();
2677 expected.add_source("x.bynk", source);
2678 expected.record(base, ret_span);
2679 assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
2680 }
2681
2682 /// `print_class_method_and_merge` — the `TsClassMethod` shape
2683 /// (`emit_provider`/`emit_agent`'s own real site, today reverse-engineered
2684 /// by `emit_class_method_and_merge_source_map`'s own string-matching).
2685 /// Same non-zero-`base` proof as the `TsDecl::Function` test above.
2686 #[test]
2687 fn print_class_method_and_merge_merges_a_methods_own_nested_checkpoint() {
2688 let source = "on call m() { commit }\n";
2689 let commit_off = source.find("commit").unwrap();
2690 let commit_span = Span::new(commit_off, commit_off + 6);
2691
2692 let body_text = " this.commit();\n";
2693 let mut nested = SourceMapBuilder::new();
2694 nested.record(0, commit_span);
2695
2696 let mut raw = TsStmt::raw(body_text, None);
2697 raw.nested_map = Some(nested);
2698
2699 let method = TsClassMethod {
2700 name: "m".to_string(),
2701 private: false,
2702 is_async: false,
2703 params: Vec::new(),
2704 return_type: None,
2705 doc: None,
2706 body: vec![raw],
2707 };
2708
2709 let mut out = String::from("class C {\n");
2710 let mut map = SourceMapBuilder::new();
2711 map.add_source("x.bynk", source);
2712
2713 print_class_method_and_merge(&mut out, &method, 0, &mut map, 0);
2714
2715 let base = out.find(body_text).unwrap();
2716 let mut expected = SourceMapBuilder::new();
2717 expected.add_source("x.bynk", source);
2718 expected.record(base, commit_span);
2719 assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
2720 }
2721
2722 /// `print_object_entry_and_merge` — the `TsObjectEntry::Method` shape
2723 /// (`emit_service`'s own real per-handler site): identical mechanism,
2724 /// exercised through its own, third public entry point.
2725 #[test]
2726 fn print_object_entry_and_merge_merges_a_methods_own_nested_checkpoint() {
2727 let source = "on call h() { reply }\n";
2728 let reply_off = source.find("reply").unwrap();
2729 let reply_span = Span::new(reply_off, reply_off + 5);
2730
2731 let body_text = " return reply();\n";
2732 let mut nested = SourceMapBuilder::new();
2733 nested.record(0, reply_span);
2734
2735 let mut raw = TsStmt::raw(body_text, None);
2736 raw.nested_map = Some(nested);
2737
2738 let entry = TsObjectEntry::Method {
2739 name: "h".to_string(),
2740 is_async: false,
2741 generics: Vec::new(),
2742 params: Vec::new(),
2743 return_type: None,
2744 doc: None,
2745 inline: false,
2746 body: vec![raw],
2747 };
2748
2749 let mut out = String::from("export const svc = {\n");
2750 let mut map = SourceMapBuilder::new();
2751 map.add_source("x.bynk", source);
2752
2753 print_object_entry_and_merge(&mut out, &entry, 0, &mut map, 0);
2754
2755 let base = out.find(body_text).unwrap();
2756 let mut expected = SourceMapBuilder::new();
2757 expected.add_source("x.bynk", source);
2758 expected.record(base, reply_span);
2759 assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
2760 }
2761
2762 /// `print()`'s own top-level loop (#1477): a real `TsProgram` containing
2763 /// a `Decl`-kinded top-level statement whose own body carries a
2764 /// `nested_map` merges it automatically — no `bynk-emit` change needed
2765 /// to benefit once a real construction site sets the field, since
2766 /// `print()` already threads its own module map through `render_stmt`
2767 /// for every top-level statement.
2768 #[test]
2769 fn print_merges_a_top_level_functions_own_nested_checkpoint_automatically() {
2770 let source = "fn f() { return 1 }\n";
2771 let ret_off = source.find("return 1").unwrap();
2772 let ret_span = Span::new(ret_off, ret_off + 8);
2773
2774 let body_text = " return 1;\n";
2775 let mut nested = SourceMapBuilder::new();
2776 nested.record(0, ret_span);
2777
2778 let mut raw = TsStmt::raw(body_text, None);
2779 raw.nested_map = Some(nested);
2780
2781 let mut program = TsProgram::new();
2782 program.push(TsStmt::decl(
2783 TsDecl::Function {
2784 name: "f".to_string(),
2785 generics: Vec::new(),
2786 params: Vec::new(),
2787 return_type: None,
2788 body: vec![raw],
2789 is_async: false,
2790 inline: false,
2791 },
2792 None,
2793 ));
2794
2795 let printed = print(&program, "x.bynk", source, "x.ts");
2796 let base = printed.text.find(body_text).unwrap();
2797
2798 let mut expected = SourceMapBuilder::new();
2799 expected.add_source("x.bynk", source);
2800 expected.record(base, ret_span);
2801 assert_eq!(printed.source_map, expected.to_v3(&printed.text, "x.ts"));
2802 }
2803
2804 /// Review of #1488, finding 1: `source_id` must reach the merge, not be
2805 /// hardcoded — a multi-source module map (a test module aggregating
2806 /// several `.bynk` files, `tests_emit.rs:537`/`:1936`'s own real shape)
2807 /// tags a nested checkpoint to the *wrong* source silently if the merge
2808 /// always assumes index 0.
2809 ///
2810 /// Decisive by construction, not just an assertion on the map's own
2811 /// `sources` list (which would pass either way — every registered
2812 /// source name appears there regardless of which one a checkpoint
2813 /// actually resolves against, `to_v3`'s own doc/body confirms `sources`/
2814 /// `sourcesContent` iterate the full registered list unconditionally):
2815 /// source 0's own text is a single short line, too short to contain the
2816 /// real checkpoint's span. If the merge resolves the checkpoint against
2817 /// source 0 (the bug this test exists to catch), `to_v3`'s own
2818 /// out-of-range guard (`span.start > text.len()`) silently drops it and
2819 /// the whole map comes back `None` (its only checkpoint). Only
2820 /// resolving against the real source (id 1, correctly threaded through)
2821 /// keeps the span in range and produces a real map.
2822 #[test]
2823 fn print_stmt_and_merge_tags_a_nested_checkpoint_to_the_given_source_id() {
2824 let primary = "x\n"; // deliberately shorter than `ret_span`'s own offset
2825 let secondary = "fn f() { return 1 }\n";
2826 let ret_off = secondary.find("return 1").unwrap();
2827 let ret_span = Span::new(ret_off, ret_off + 8);
2828 assert!(
2829 ret_span.start > primary.len(),
2830 "sanity: the span must fall outside source 0's own short text"
2831 );
2832
2833 let body_text = " return 1;\n";
2834 let mut nested = SourceMapBuilder::new();
2835 nested.record(0, ret_span);
2836
2837 let mut raw = TsStmt::raw(body_text, None);
2838 raw.nested_map = Some(nested);
2839
2840 let func = TsStmt::decl(
2841 TsDecl::Function {
2842 name: "f".to_string(),
2843 generics: Vec::new(),
2844 params: Vec::new(),
2845 return_type: None,
2846 body: vec![raw],
2847 is_async: false,
2848 inline: false,
2849 },
2850 None,
2851 );
2852
2853 let mut out = String::new();
2854 let mut map = SourceMapBuilder::new();
2855 map.add_source("primary.bynk", primary); // id 0
2856 let secondary_id = map.add_source("secondary.bynk", secondary); // id 1
2857 assert_eq!(secondary_id, 1);
2858
2859 print_stmt_and_merge(&mut out, &func, 0, &mut map, secondary_id);
2860
2861 assert!(
2862 map.to_v3(&out, "x.ts").is_some(),
2863 "the checkpoint resolved against the wrong source (id 0) and was silently dropped"
2864 );
2865 }
2866
2867 /// #1486's own sibling gap to the one above, but in [`print_multi_
2868 /// source`]'s own automatic top-level loop rather than the manual
2869 /// `print_stmt_and_merge` entry point: a real `TsProgram` built for a
2870 /// multi-source module (a test/integration module aggregating several
2871 /// `.bynk` fragment files, one per case) needs each case's own
2872 /// top-level statement merged against *its own* registered source, not
2873 /// the module's primary one (id 0) — `nested_map_source_id`'s own doc
2874 /// comment. Decisive the same way the sibling test above is: source 0's
2875 /// text is too short to contain the real span, so a wrong-source merge
2876 /// silently drops the checkpoint (`to_v3`'s own out-of-range guard) and
2877 /// the map comes back missing it.
2878 #[test]
2879 fn print_multi_source_honours_a_top_level_statements_own_nested_map_source_id() {
2880 let primary = "x\n";
2881 let secondary = "fn f() { return 1 }\n";
2882 let ret_off = secondary.find("return 1").unwrap();
2883 let ret_span = Span::new(ret_off, ret_off + 8);
2884 assert!(
2885 ret_span.start > primary.len(),
2886 "sanity: the span must fall outside source 0's own short text"
2887 );
2888
2889 let body_text = " return 1;\n";
2890 let mut nested = SourceMapBuilder::new();
2891 nested.record(0, ret_span);
2892
2893 let mut raw = TsStmt::raw(body_text, None);
2894 raw.nested_map = Some(nested);
2895
2896 let mut func = TsStmt::decl(
2897 TsDecl::Function {
2898 name: "f".to_string(),
2899 generics: Vec::new(),
2900 params: Vec::new(),
2901 return_type: None,
2902 body: vec![raw],
2903 is_async: false,
2904 inline: false,
2905 },
2906 None,
2907 );
2908 func.nested_map_source_id = 1;
2909
2910 let mut program = TsProgram::new();
2911 program.push(func);
2912
2913 let mut map = SourceMapBuilder::new();
2914 map.add_source("primary.bynk", primary);
2915 map.add_source("secondary.bynk", secondary);
2916 let printed = print_multi_source(&program, map, "x.ts");
2917 assert!(
2918 printed.source_map.is_some(),
2919 "the checkpoint resolved against the wrong source (id 0) and was silently dropped"
2920 );
2921 }
2922
2923 /// Review of #1488, finding 2: a `nested_map` set on the statement handed
2924 /// directly to `print`/`print_stmt_and_merge` itself — not nested inside
2925 /// a `Decl` body — must still merge, not silently vanish. The first
2926 /// version of this mechanism only checked a body's own *direct
2927 /// children* (inside `render_block_stmts`), missing this exact case.
2928 #[test]
2929 fn print_stmt_and_merge_merges_a_bare_top_level_raw_statements_own_nested_checkpoint() {
2930 let source = "contract seal() {}\n";
2931 let off = source.find("contract").unwrap();
2932 let span = Span::new(off, off + 8);
2933
2934 let text = "seal();\n";
2935 let mut nested = SourceMapBuilder::new();
2936 nested.record(0, span);
2937
2938 let mut raw = TsStmt::raw(text, None);
2939 raw.nested_map = Some(nested);
2940
2941 let mut out = String::from("// header\n");
2942 let mut map = SourceMapBuilder::new();
2943 map.add_source("x.bynk", source);
2944
2945 print_stmt_and_merge(&mut out, &raw, 0, &mut map, 0);
2946
2947 let base = out.find(text).unwrap();
2948 let mut expected = SourceMapBuilder::new();
2949 expected.add_source("x.bynk", source);
2950 expected.record(base, span);
2951 assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
2952 }
2953
2954 /// Review of #1488, finding 3 (part 1): the merge's own line-rebasing
2955 /// arithmetic (`out_line = base_line + body_line`) is exercised for
2956 /// real, not just the degenerate `body_line == 0` case every other test
2957 /// here uses — a two-statement body with the checkpoint on the
2958 /// *second* line.
2959 #[test]
2960 fn print_stmt_and_merge_rebases_a_checkpoint_on_a_bodys_second_line() {
2961 let source = "fn f() { let a = 1\n return a }\n";
2962 let ret_off = source.find("return a").unwrap();
2963 let ret_span = Span::new(ret_off, ret_off + 8);
2964
2965 let body_text = " const a = 1;\n return a;\n";
2966 let second_line_off = body_text.find("return a;").unwrap();
2967 let mut nested = SourceMapBuilder::new();
2968 nested.record(second_line_off, ret_span);
2969
2970 let mut raw = TsStmt::raw(body_text, None);
2971 raw.nested_map = Some(nested);
2972
2973 let func = TsStmt::decl(
2974 TsDecl::Function {
2975 name: "f".to_string(),
2976 generics: Vec::new(),
2977 params: Vec::new(),
2978 return_type: None,
2979 body: vec![raw],
2980 is_async: false,
2981 inline: false,
2982 },
2983 None,
2984 );
2985
2986 let mut out = String::from("// header\n");
2987 let mut map = SourceMapBuilder::new();
2988 map.add_source("x.bynk", source);
2989
2990 print_stmt_and_merge(&mut out, &func, 0, &mut map, 0);
2991
2992 let base_of_body = out.find(body_text).unwrap();
2993 let base_of_second_line = out.find(" return a;\n").unwrap();
2994 assert!(
2995 base_of_second_line > base_of_body,
2996 "sanity: second line is after the first"
2997 );
2998
2999 let mut expected = SourceMapBuilder::new();
3000 expected.add_source("x.bynk", source);
3001 expected.record(base_of_second_line, ret_span);
3002 assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
3003 }
3004
3005 /// Review of #1488, finding 3 (part 2): `TsDecl::Class`'s own
3006 /// constructor-then-methods sequence is the one real place `map` is
3007 /// reborrowed across more than one consumer — a class with a
3008 /// `nested_map`-bearing constructor body *and* a `nested_map`-bearing
3009 /// method body proves both reborrows land correctly, not just the
3010 /// first one. `emit_provider`/`emit_agent` (both class-shaped) are
3011 /// exactly the real callers this slice unblocks.
3012 #[test]
3013 fn a_classs_constructor_and_method_both_merge_their_own_nested_checkpoints() {
3014 let source = "on new() { init }\non call m() { commit }\n";
3015 let init_off = source.find("init").unwrap();
3016 let init_span = Span::new(init_off, init_off + 4);
3017 let commit_off = source.find("commit").unwrap();
3018 let commit_span = Span::new(commit_off, commit_off + 6);
3019
3020 let ctor_body_text = " this.init();\n";
3021 let mut ctor_nested = SourceMapBuilder::new();
3022 ctor_nested.record(0, init_span);
3023 let mut ctor_raw = TsStmt::raw(ctor_body_text, None);
3024 ctor_raw.nested_map = Some(ctor_nested);
3025
3026 let method_body_text = " this.commit();\n";
3027 let mut method_nested = SourceMapBuilder::new();
3028 method_nested.record(0, commit_span);
3029 let mut method_raw = TsStmt::raw(method_body_text, None);
3030 method_raw.nested_map = Some(method_nested);
3031
3032 let class = TsDecl::Class {
3033 name: "C".to_string(),
3034 fields: Vec::new(),
3035 constructor: Some(TsClassCtor {
3036 params: Vec::new(),
3037 body: vec![ctor_raw],
3038 }),
3039 methods: vec![TsClassMethod {
3040 name: "m".to_string(),
3041 private: false,
3042 is_async: false,
3043 params: Vec::new(),
3044 return_type: None,
3045 doc: None,
3046 body: vec![method_raw],
3047 }],
3048 };
3049
3050 let mut out = String::new();
3051 let mut map = SourceMapBuilder::new();
3052 map.add_source("x.bynk", source);
3053
3054 print_stmt_and_merge(&mut out, &TsStmt::decl(class, None), 0, &mut map, 0);
3055
3056 let ctor_base = out.find(ctor_body_text).unwrap();
3057 let method_base = out.find(method_body_text).unwrap();
3058
3059 let mut expected = SourceMapBuilder::new();
3060 expected.add_source("x.bynk", source);
3061 expected.record(ctor_base, init_span);
3062 expected.record(method_base, commit_span);
3063 assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
3064 }
3065
3066 // -- P7.8 (#1313): real node rendering. --
3067
3068 #[test]
3069 fn prints_a_const_statement_with_a_destructured_binding_and_a_type_cast() {
3070 let mut program = TsProgram::new();
3071 program.push(TsStmt::const_stmt(
3072 TsBindingName::ObjectPattern(vec!["events".to_string()]),
3073 None,
3074 TsExpr::As {
3075 // #1323: `Await` no longer auto-parenthesizes under `As` —
3076 // this file's own real content wraps it explicitly via
3077 // `Paren` (see `render_expr`'s own `As` arm doc).
3078 expr: Box::new(TsExpr::Paren(Box::new(TsExpr::Await(Box::new(
3079 TsExpr::Call {
3080 callee: Box::new(TsExpr::Member {
3081 object: Box::new(TsExpr::Ident("request".to_string())),
3082 property: "json".to_string(),
3083 }),
3084 args: vec![],
3085 },
3086 ))))),
3087 ty: TsType::Object(vec![TsTypeMember::prop(
3088 "events",
3089 TsType::array(TsType::named("FanoutEvent")),
3090 )]),
3091 },
3092 None,
3093 ));
3094 let printed = print(&program, "x.bynk", "", "x.ts");
3095 assert_eq!(
3096 printed.text,
3097 "const { events } = (await request.json()) as { events: FanoutEvent[] };\n"
3098 );
3099 }
3100
3101 #[test]
3102 fn prints_if_without_braces_when_the_body_is_not_a_block() {
3103 let mut program = TsProgram::new();
3104 program.push(TsStmt::if_stmt(
3105 TsExpr::Unary {
3106 op: TsUnaryOp::Not,
3107 expr: Box::new(TsExpr::Call {
3108 callee: Box::new(TsExpr::Member {
3109 object: Box::new(TsExpr::Ident("Array".to_string())),
3110 property: "isArray".to_string(),
3111 }),
3112 args: vec![TsExpr::Ident("subs".to_string())],
3113 }),
3114 },
3115 TsStmt::continue_stmt(None),
3116 None,
3117 ));
3118 let printed = print(&program, "x.bynk", "", "x.ts");
3119 assert_eq!(printed.text, "if (!Array.isArray(subs)) continue;\n");
3120 }
3121
3122 /// Review of #1317/#1318, finding 2: a bare `Comment` as an `if`'s
3123 /// brace-free body must not fall through to the top-level `//`-line
3124 /// form — `if (cond) // text` comments out the rest of the physical
3125 /// line, leaving `if` with no body at all (a parse error). Renders as
3126 /// a block comment instead, which cannot swallow anything after it.
3127 #[test]
3128 fn a_comment_as_an_ifs_brace_free_body_renders_as_a_block_comment() {
3129 let mut program = TsProgram::new();
3130 program.push(TsStmt::if_stmt(
3131 TsExpr::Ident("cond".to_string()),
3132 TsStmt::comment("annotation", None),
3133 None,
3134 ));
3135 let printed = print(&program, "x.bynk", "", "x.ts");
3136 assert_eq!(printed.text, "if (cond) /* annotation */\n");
3137 }
3138
3139 #[test]
3140 fn prints_a_for_of_loop_with_a_braced_body() {
3141 let mut program = TsProgram::new();
3142 program.push(TsStmt::for_of(
3143 "ev",
3144 TsExpr::Ident("events".to_string()),
3145 TsStmt::block(
3146 vec![TsStmt::return_stmt(
3147 Some(TsExpr::Ident("ev".to_string())),
3148 None,
3149 )],
3150 None,
3151 ),
3152 None,
3153 ));
3154 let printed = print(&program, "x.bynk", "", "x.ts");
3155 assert_eq!(
3156 printed.text,
3157 "for (const ev of events) {\n return ev;\n}\n"
3158 );
3159 }
3160
3161 /// Arc E slice 7 (#1447): `TsStmtKind::For` — pins the exact C-style
3162 /// header shape `ListInst`/`MapInst`'s own real deserialise-side element
3163 /// loops need (`for (let i = 0; i < json.length; i++) { ... }`),
3164 /// including the postfix `++` the printer appends over `name` itself
3165 /// (see `TsStmtKind::For`'s own doc for why there is no separate
3166 /// `update` field to pass).
3167 #[test]
3168 fn prints_a_c_style_for_loop_with_a_braced_body() {
3169 let mut program = TsProgram::new();
3170 program.push(TsStmt::for_stmt(
3171 "i",
3172 TsExpr::Lit(TsLit::Num("0".to_string())),
3173 TsExpr::Binary {
3174 op: TsBinaryOp::LessThan,
3175 left: Box::new(TsExpr::Ident("i".to_string())),
3176 right: Box::new(TsExpr::Member {
3177 object: Box::new(TsExpr::Ident("json".to_string())),
3178 property: "length".to_string(),
3179 }),
3180 },
3181 TsStmt::block(
3182 vec![TsStmt::expr_stmt(TsExpr::Ident("item".to_string()), None)],
3183 None,
3184 ),
3185 None,
3186 ));
3187 let printed = print(&program, "x.bynk", "", "x.ts");
3188 assert_eq!(
3189 printed.text,
3190 "for (let i = 0; i < json.length; i++) {\n item;\n}\n"
3191 );
3192 }
3193
3194 /// A `For` loop's own brace-free body falls back to the same inline
3195 /// rendering `ForOf`'s own brace-free body already uses — exercised as
3196 /// the *outer* statement, over a `Continue` body (not reachable from any
3197 /// real `bynk-emit` content today, but a real, direct shape).
3198 #[test]
3199 fn prints_a_c_style_for_loops_brace_free_body_inline() {
3200 let mut program = TsProgram::new();
3201 program.push(TsStmt::for_stmt(
3202 "i",
3203 TsExpr::Lit(TsLit::Num("0".to_string())),
3204 TsExpr::Binary {
3205 op: TsBinaryOp::LessThan,
3206 left: Box::new(TsExpr::Ident("i".to_string())),
3207 right: Box::new(TsExpr::Ident("n".to_string())),
3208 },
3209 TsStmt::continue_stmt(None),
3210 None,
3211 ));
3212 let printed = print(&program, "x.bynk", "", "x.ts");
3213 assert_eq!(printed.text, "for (let i = 0; i < n; i++) continue;\n");
3214 }
3215
3216 /// Review of #1448, finding 1: the previous test above pins a brace-free
3217 /// `For` as the *outer* statement, which renders through `render_stmt`'s
3218 /// own dedicated `For` arm, not the `render_inline_stmt` fallback this
3219 /// slice's own `| TsStmtKind::For { .. }` line actually added. That
3220 /// fallback only fires when a `For` is itself the brace-free body of an
3221 /// ENCLOSING `if`/`for...of`/`For` — exercised directly here, nesting a
3222 /// brace-free `For` as an `if`'s own brace-free branch.
3223 #[test]
3224 fn prints_a_brace_free_for_loop_nested_as_an_ifs_own_brace_free_body() {
3225 let mut program = TsProgram::new();
3226 program.push(TsStmt::if_stmt(
3227 TsExpr::Ident("cond".to_string()),
3228 TsStmt::for_stmt(
3229 "i",
3230 TsExpr::Lit(TsLit::Num("0".to_string())),
3231 TsExpr::Binary {
3232 op: TsBinaryOp::LessThan,
3233 left: Box::new(TsExpr::Ident("i".to_string())),
3234 right: Box::new(TsExpr::Ident("n".to_string())),
3235 },
3236 TsStmt::continue_stmt(None),
3237 None,
3238 ),
3239 None,
3240 ));
3241 let printed = print(&program, "x.bynk", "", "x.ts");
3242 assert_eq!(
3243 printed.text,
3244 "if (cond) for (let i = 0; i < n; i++) continue;\n"
3245 );
3246 }
3247
3248 #[test]
3249 fn prints_a_try_catch_on_the_shared_closing_brace_line() {
3250 let mut program = TsProgram::new();
3251 program.push(TsStmt::try_catch(
3252 TsStmt::block(
3253 vec![TsStmt::expr_stmt(
3254 TsExpr::Await(Box::new(TsExpr::Call {
3255 callee: Box::new(TsExpr::Ident("deliverEvent".to_string())),
3256 args: vec![TsExpr::Ident("binding".to_string())],
3257 })),
3258 None,
3259 )],
3260 None,
3261 ),
3262 Some("e"),
3263 TsStmt::block(
3264 vec![TsStmt::expr_stmt(
3265 TsExpr::Call {
3266 callee: Box::new(TsExpr::Member {
3267 object: Box::new(TsExpr::Ident("console".to_string())),
3268 property: "error".to_string(),
3269 }),
3270 args: vec![TsExpr::Lit(TsLit::Str("failed".to_string()))],
3271 },
3272 None,
3273 )],
3274 None,
3275 ),
3276 None,
3277 ));
3278 let printed = print(&program, "x.bynk", "", "x.ts");
3279 assert_eq!(
3280 printed.text,
3281 "try {\n await deliverEvent(binding);\n} catch (e) {\n console.error(\"failed\");\n}\n"
3282 );
3283 }
3284
3285 #[test]
3286 fn prints_an_interface_with_an_inline_nested_object_type() {
3287 let mut program = TsProgram::new();
3288 program.push(TsStmt::decl(
3289 TsDecl::Interface {
3290 name: "FanoutEvent".to_string(),
3291 type_params: Vec::new(),
3292 members: vec![
3293 TsTypeMember::prop("type", TsType::named("string")),
3294 TsTypeMember::prop("payload", TsType::named("unknown")),
3295 TsTypeMember::prop(
3296 "envelope",
3297 TsType::Object(vec![
3298 TsTypeMember::prop("eventId", TsType::named("string")),
3299 TsTypeMember::prop("publisherId", TsType::named("string")),
3300 ]),
3301 ),
3302 ],
3303 },
3304 None,
3305 ));
3306 let printed = print(&program, "x.bynk", "", "x.ts");
3307 assert_eq!(
3308 printed.text,
3309 "interface FanoutEvent {\n type: string;\n payload: unknown;\n envelope: { eventId: string; publisherId: string };\n}\n"
3310 );
3311 }
3312
3313 #[test]
3314 fn prints_two_adjacent_imports_with_no_blank_line_between_them() {
3315 let mut program = TsProgram::new();
3316 program.push(TsStmt::decl(
3317 TsDecl::Import {
3318 type_only: true,
3319 names: vec!["A".to_string()],
3320 from: "../x.js".to_string(),
3321 },
3322 None,
3323 ));
3324 program.push(TsStmt::decl(
3325 TsDecl::Import {
3326 type_only: false,
3327 names: vec!["b".to_string()],
3328 from: "../x.js".to_string(),
3329 },
3330 None,
3331 ));
3332 program.push(TsStmt::decl(
3333 TsDecl::ConstDecl {
3334 name: "c".to_string(),
3335 ty: None,
3336 init: TsExpr::Lit(TsLit::Null),
3337 },
3338 None,
3339 ));
3340 let printed = print(&program, "x.bynk", "", "x.ts");
3341 assert_eq!(
3342 printed.text,
3343 "import type { A } from \"../x.js\";\nimport { b } from \"../x.js\";\n\nconst c = null;\n"
3344 );
3345 }
3346
3347 /// #1486: `no_blank_before` suppresses the automatic blank line before
3348 /// exactly the statement that carries it — a `const`/`function` pair
3349 /// neither an import, a comment, a `ReExportAll`, nor `Verbatim`, so
3350 /// none of the existing exemptions apply; without this field the two
3351 /// would print with an unwanted blank between them (confirmed by
3352 /// temporarily reverting the field during this same change — the
3353 /// printer's own default policy really does insert one here). A third,
3354 /// ordinary statement after it confirms the field is scoped to the one
3355 /// carrying it, not sticky for the rest of the program.
3356 #[test]
3357 fn no_blank_before_suppresses_exactly_the_next_statements_own_blank() {
3358 let mut program = TsProgram::new();
3359 program.push(TsStmt::decl(
3360 TsDecl::ConstDecl {
3361 name: "__FooRegistry".to_string(),
3362 ty: None,
3363 init: TsExpr::Lit(TsLit::Null),
3364 },
3365 None,
3366 ));
3367 let mut zero_fn = TsStmt::decl(
3368 TsDecl::Function {
3369 name: "__zeroOfFooState".to_string(),
3370 generics: Vec::new(),
3371 params: Vec::new(),
3372 return_type: None,
3373 body: Vec::new(),
3374 is_async: false,
3375 inline: true,
3376 },
3377 None,
3378 );
3379 zero_fn.no_blank_before = true;
3380 program.push(zero_fn);
3381 program.push(TsStmt::decl(
3382 TsDecl::ConstDecl {
3383 name: "c".to_string(),
3384 ty: None,
3385 init: TsExpr::Lit(TsLit::Null),
3386 },
3387 None,
3388 ));
3389 let printed = print(&program, "x.bynk", "", "x.ts");
3390 assert_eq!(
3391 printed.text,
3392 "const __FooRegistry = null;\nfunction __zeroOfFooState() { }\n\nconst c = null;\n"
3393 );
3394 }
3395
3396 /// The required "does the algebra hold against real content" test
3397 /// (P7.8's own accepted proposal, #1313): builds
3398 /// `bynk-emit/src/emitter/events_fanout.rs`'s own `EventsFanoutDO`
3399 /// class as real nodes (its field, constructor, and `fetch` method,
3400 /// with the exact control flow the real function emits — `for...of`,
3401 /// nested `if`-without-braces, `try`/`catch`) and asserts the printed
3402 /// text is byte-identical to what `emit_events_fanout_do` produces
3403 /// today for that class (transcribed directly from
3404 /// `bynk-emit/src/emitter/events_fanout.rs`'s own `write!` calls, not
3405 /// re-derived — including the constructor body, `this.env = (env ??
3406 /// {}) as Record<string, ServiceBinding>;`, via `TsStmtKind::Assign`,
3407 /// added in review of #1313: the accepted proposal's own grounding
3408 /// catalogue detailed the `fetch` method's body but missed the
3409 /// constructor's one real statement, which turned out to need a
3410 /// variant Decision B's list didn't name). The leading header comment
3411 /// and the `__eventRoutes` table's own per-project literal entries are
3412 /// deliberately not included here — a representative fragment covering
3413 /// the class is exactly what the accepted proposal's own "Done when"
3414 /// allows, and this is the shape that actually exercises every new
3415 /// node kind this slice adds.
3416 #[test]
3417 fn prints_events_fanout_dos_own_class_byte_identical_to_the_real_emitter() {
3418 let real_ctor = TsClassCtor {
3419 params: vec![
3420 TsParam {
3421 name: "_state".to_string(),
3422 ty: Some(TsType::named("DurableObjectState")),
3423 optional: false,
3424 },
3425 TsParam {
3426 name: "env".to_string(),
3427 ty: Some(TsType::named("unknown")),
3428 optional: true,
3429 },
3430 ],
3431 body: vec![TsStmt::assign(
3432 TsExpr::Member {
3433 object: Box::new(TsExpr::Ident("this".to_string())),
3434 property: "env".to_string(),
3435 },
3436 TsExpr::As {
3437 expr: Box::new(TsExpr::Binary {
3438 op: TsBinaryOp::NullishCoalescing,
3439 left: Box::new(TsExpr::Ident("env".to_string())),
3440 right: Box::new(TsExpr::object(vec![])),
3441 }),
3442 ty: TsType::named_with_args(
3443 "Record",
3444 vec![TsType::named("string"), TsType::named("ServiceBinding")],
3445 ),
3446 },
3447 None,
3448 )],
3449 };
3450
3451 let fetch = TsClassMethod {
3452 name: "fetch".to_string(),
3453 private: false,
3454 is_async: true,
3455 params: vec![TsParam {
3456 name: "request".to_string(),
3457 ty: Some(TsType::named("Request")),
3458 optional: false,
3459 }],
3460 return_type: Some(TsType::named_with_args(
3461 "Promise",
3462 vec![TsType::named("Response")],
3463 )),
3464 doc: None,
3465 body: vec![
3466 TsStmt::const_stmt(
3467 TsBindingName::ObjectPattern(vec!["events".to_string()]),
3468 None,
3469 TsExpr::As {
3470 expr: Box::new(TsExpr::Paren(Box::new(TsExpr::Await(Box::new(
3471 TsExpr::Call {
3472 callee: Box::new(TsExpr::Member {
3473 object: Box::new(TsExpr::Ident("request".to_string())),
3474 property: "json".to_string(),
3475 }),
3476 args: vec![],
3477 },
3478 ))))),
3479 ty: TsType::Object(vec![TsTypeMember::prop(
3480 "events",
3481 TsType::array(TsType::named("FanoutEvent")),
3482 )]),
3483 },
3484 None,
3485 ),
3486 TsStmt::for_of(
3487 "ev",
3488 TsExpr::Ident("events".to_string()),
3489 TsStmt::block(
3490 vec![
3491 TsStmt::const_stmt(
3492 TsBindingName::Ident("subs".to_string()),
3493 None,
3494 TsExpr::Index {
3495 object: Box::new(TsExpr::Ident("__eventRoutes".to_string())),
3496 index: Box::new(TsExpr::Member {
3497 object: Box::new(TsExpr::Ident("ev".to_string())),
3498 property: "type".to_string(),
3499 }),
3500 },
3501 None,
3502 ),
3503 TsStmt::if_stmt(
3504 TsExpr::Unary {
3505 op: TsUnaryOp::Not,
3506 expr: Box::new(TsExpr::Call {
3507 callee: Box::new(TsExpr::Member {
3508 object: Box::new(TsExpr::Ident("Array".to_string())),
3509 property: "isArray".to_string(),
3510 }),
3511 args: vec![TsExpr::Ident("subs".to_string())],
3512 }),
3513 },
3514 TsStmt::continue_stmt(None),
3515 None,
3516 ),
3517 TsStmt::for_of(
3518 "sub",
3519 TsExpr::Ident("subs".to_string()),
3520 TsStmt::block(
3521 vec![
3522 TsStmt::const_stmt(
3523 TsBindingName::Ident("binding".to_string()),
3524 None,
3525 TsExpr::Index {
3526 object: Box::new(TsExpr::Member {
3527 object: Box::new(TsExpr::Ident(
3528 "this".to_string(),
3529 )),
3530 property: "env".to_string(),
3531 }),
3532 index: Box::new(TsExpr::Member {
3533 object: Box::new(TsExpr::Ident(
3534 "sub".to_string(),
3535 )),
3536 property: "binding".to_string(),
3537 }),
3538 },
3539 None,
3540 ),
3541 TsStmt::if_stmt(
3542 TsExpr::Unary {
3543 op: TsUnaryOp::Not,
3544 expr: Box::new(TsExpr::Ident(
3545 "binding".to_string(),
3546 )),
3547 },
3548 TsStmt::continue_stmt(None),
3549 None,
3550 ),
3551 TsStmt::try_catch(
3552 TsStmt::block(
3553 vec![TsStmt::expr_stmt(
3554 TsExpr::Await(Box::new(TsExpr::Call {
3555 callee: Box::new(TsExpr::Ident(
3556 "deliverEvent".to_string(),
3557 )),
3558 args: vec![
3559 TsExpr::Ident("binding".to_string()),
3560 TsExpr::Member {
3561 object: Box::new(TsExpr::Ident(
3562 "sub".to_string(),
3563 )),
3564 property: "service".to_string(),
3565 },
3566 TsExpr::Member {
3567 object: Box::new(TsExpr::Ident(
3568 "ev".to_string(),
3569 )),
3570 property: "payload".to_string(),
3571 },
3572 TsExpr::Member {
3573 object: Box::new(TsExpr::Ident(
3574 "ev".to_string(),
3575 )),
3576 property: "envelope".to_string(),
3577 },
3578 ],
3579 })),
3580 None,
3581 )],
3582 None,
3583 ),
3584 Some("e"),
3585 TsStmt::block(
3586 vec![TsStmt::expr_stmt(
3587 TsExpr::Call {
3588 callee: Box::new(TsExpr::Member {
3589 object: Box::new(TsExpr::Ident(
3590 "console".to_string(),
3591 )),
3592 property: "error".to_string(),
3593 }),
3594 args: vec![
3595 TsExpr::Lit(TsLit::Str(
3596 "EventsFanout delivery failed"
3597 .to_string(),
3598 )),
3599 TsExpr::object(vec![
3600 (
3601 "event".to_string(),
3602 TsExpr::Member {
3603 object: Box::new(
3604 TsExpr::Ident(
3605 "ev".to_string(),
3606 ),
3607 ),
3608 property: "type"
3609 .to_string(),
3610 },
3611 ),
3612 (
3613 "service".to_string(),
3614 TsExpr::Member {
3615 object: Box::new(
3616 TsExpr::Ident(
3617 "sub".to_string(),
3618 ),
3619 ),
3620 property: "service"
3621 .to_string(),
3622 },
3623 ),
3624 (
3625 "error".to_string(),
3626 TsExpr::Call {
3627 callee: Box::new(
3628 TsExpr::Ident(
3629 "String"
3630 .to_string(),
3631 ),
3632 ),
3633 args: vec![TsExpr::Ident(
3634 "e".to_string(),
3635 )],
3636 },
3637 ),
3638 ]),
3639 ],
3640 },
3641 None,
3642 )],
3643 None,
3644 ),
3645 None,
3646 ),
3647 ],
3648 None,
3649 ),
3650 None,
3651 ),
3652 ],
3653 None,
3654 ),
3655 None,
3656 ),
3657 TsStmt::return_stmt(
3658 Some(TsExpr::New {
3659 callee: Box::new(TsExpr::Ident("Response".to_string())),
3660 args: vec![
3661 TsExpr::Lit(TsLit::Null),
3662 TsExpr::object(vec![(
3663 "status".to_string(),
3664 TsExpr::Lit(TsLit::Num("204".to_string())),
3665 )]),
3666 ],
3667 }),
3668 None,
3669 ),
3670 ],
3671 };
3672
3673 let class = TsDecl::Export(Box::new(TsDecl::Class {
3674 name: "EventsFanoutDO".to_string(),
3675 fields: vec![TsClassField {
3676 name: "env".to_string(),
3677 ty: TsType::named_with_args(
3678 "Record",
3679 vec![TsType::named("string"), TsType::named("ServiceBinding")],
3680 ),
3681 private: true,
3682 }],
3683 constructor: Some(real_ctor),
3684 methods: vec![fetch],
3685 }));
3686
3687 let mut program = TsProgram::new();
3688 program.push(TsStmt::decl(class, None));
3689 let printed = print(&program, "x.bynk", "", "x.ts");
3690
3691 // Transcribed directly from `bynk-emit/src/emitter/events_fanout.rs`'s
3692 // own `write!` calls, byte-for-byte including the constructor body.
3693 let expected_lines = [
3694 "export class EventsFanoutDO {",
3695 " private env: Record<string, ServiceBinding>;",
3696 " constructor(_state: DurableObjectState, env?: unknown) {",
3697 " this.env = (env ?? {}) as Record<string, ServiceBinding>;",
3698 " }",
3699 "",
3700 " async fetch(request: Request): Promise<Response> {",
3701 " const { events } = (await request.json()) as { events: FanoutEvent[] };",
3702 " for (const ev of events) {",
3703 " const subs = __eventRoutes[ev.type];",
3704 " if (!Array.isArray(subs)) continue;",
3705 " for (const sub of subs) {",
3706 " const binding = this.env[sub.binding];",
3707 " if (!binding) continue;",
3708 " try {",
3709 " await deliverEvent(binding, sub.service, ev.payload, ev.envelope);",
3710 " } catch (e) {",
3711 " console.error(\"EventsFanout delivery failed\", { event: ev.type, service: sub.service, error: String(e) });",
3712 " }",
3713 " }",
3714 " }",
3715 " return new Response(null, { status: 204 });",
3716 " }",
3717 "}",
3718 ];
3719 let expected = format!("{}\n", expected_lines.join("\n"));
3720 assert_eq!(printed.text, expected);
3721 }
3722
3723 /// Coverage gap named in review of #1314, finding 4: `Let` had no test
3724 /// anywhere, including its distinct `init: None` branch (`Const` always
3725 /// has an initialiser; `Let` is the one new statement kind that can
3726 /// omit one).
3727 #[test]
3728 fn prints_a_let_statement_with_no_initialiser() {
3729 let mut program = TsProgram::new();
3730 program.push(TsStmt::let_stmt(
3731 TsBindingName::Ident("subs".to_string()),
3732 None,
3733 None,
3734 None,
3735 ));
3736 let printed = print(&program, "x.bynk", "", "x.ts");
3737 assert_eq!(printed.text, "let subs;\n");
3738 }
3739
3740 /// Coverage gap named in review of #1314, finding 4: `TsExpr::Array`
3741 /// was never constructed in any test.
3742 #[test]
3743 fn prints_an_array_literal() {
3744 let mut program = TsProgram::new();
3745 program.push(TsStmt::expr_stmt(
3746 TsExpr::array(vec![
3747 TsExpr::Lit(TsLit::Num("1".to_string())),
3748 TsExpr::Ident("x".to_string()),
3749 ]),
3750 None,
3751 ));
3752 let printed = print(&program, "x.bynk", "", "x.ts");
3753 assert_eq!(printed.text, "[1, x];\n");
3754 }
3755
3756 /// P7.9 (#1315): `TsType::Array`'s new `readonly` modifier — the shape
3757 /// every `List`/`Query` element type `bynk-emit`'s `ts_type_ref*`/
3758 /// `ts_ty` families build (`readonly T[]`), which `TsType::array`'s own
3759 /// `readonly: false` default cannot represent.
3760 #[test]
3761 fn print_type_renders_a_readonly_array() {
3762 assert_eq!(
3763 print_type(&TsType::readonly_array(TsType::named("Order"))),
3764 "readonly Order[]"
3765 );
3766 assert_eq!(
3767 print_type(&TsType::array(TsType::named("Order"))),
3768 "Order[]"
3769 );
3770 }
3771
3772 /// P7.9 (#1315): `TsType::Fn`'s zero-`params` form — the query-thunk
3773 /// wrapper shape, `bynk-emit`'s own `(() => readonly T[])`.
3774 #[test]
3775 fn print_type_renders_a_zero_param_function_type() {
3776 let ty = TsType::Fn {
3777 params: vec![],
3778 ret: Box::new(TsType::readonly_array(TsType::named("Order"))),
3779 };
3780 assert_eq!(print_type(&ty), "() => readonly Order[]");
3781 }
3782
3783 /// P7.9 (#1315): `TsType::Fn`'s real parametered form — positional
3784 /// `a0`/`a1`/… names, matching `bynk-emit`'s own pre-P7.9 convention
3785 /// exactly (TypeScript requires *some* name in function-type syntax).
3786 #[test]
3787 fn print_type_renders_a_parametered_function_type_with_positional_names() {
3788 let ty = TsType::Fn {
3789 params: vec![TsType::named("string"), TsType::named("number")],
3790 ret: Box::new(TsType::named("void")),
3791 };
3792 assert_eq!(print_type(&ty), "(a0: string, a1: number) => void");
3793 }
3794
3795 /// `TsType::Union`, added during review of #1315's own implementation —
3796 /// a real gap `bynk-emit`'s `ts_ty` needed for a resolved multi-actor
3797 /// sum (`Ty::ActorSum`), beyond the accepted proposal's own `readonly`/
3798 /// `Fn` gap list. Members print `" | "`-joined, in order.
3799 #[test]
3800 fn print_type_renders_a_union_of_named_types() {
3801 let ty = TsType::union(vec![
3802 TsType::named("string"),
3803 TsType::named("number"),
3804 TsType::Object(vec![TsTypeMember::prop(
3805 "tag",
3806 TsType::named("\"literal\""),
3807 )]),
3808 ]);
3809 assert_eq!(print_type(&ty), "string | number | { tag: \"literal\" }");
3810 }
3811
3812 /// `TsStmtKind::Comment`, added for Arc C's own first real conversion
3813 /// slice (#1317). A multi-line comment (embedded `\n`) prints one
3814 /// `// `-prefixed line per real line.
3815 #[test]
3816 fn prints_a_comment_one_prefixed_line_per_embedded_newline() {
3817 let mut program = TsProgram::new();
3818 program.push(TsStmt::comment(
3819 "Generated by bynkc — do not edit by hand.\nSecond line.",
3820 None,
3821 ));
3822 let printed = print(&program, "x.bynk", "", "x.ts");
3823 assert_eq!(
3824 printed.text,
3825 "// Generated by bynkc — do not edit by hand.\n// Second line.\n"
3826 );
3827 }
3828
3829 /// Two adjacent top-level `Comment` statements get no blank line
3830 /// between them — the same exception already established for two
3831 /// adjacent `import`s, matching `events_fanout.rs`'s own real two-line
3832 /// header banner (built as two separate `Comment` statements, not one
3833 /// with an embedded newline).
3834 #[test]
3835 fn no_blank_line_between_two_adjacent_comment_statements() {
3836 let mut program = TsProgram::new();
3837 program.push(TsStmt::comment(
3838 "Generated by bynkc — do not edit by hand.",
3839 None,
3840 ));
3841 program.push(TsStmt::comment("Second line.", None));
3842 program.push(TsStmt::decl(
3843 TsDecl::Interface {
3844 name: "A".to_string(),
3845 type_params: Vec::new(),
3846 members: vec![],
3847 },
3848 None,
3849 ));
3850 let printed = print(&program, "x.bynk", "", "x.ts");
3851 assert_eq!(
3852 printed.text,
3853 "// Generated by bynkc — do not edit by hand.\n// Second line.\n\ninterface A {\n}\n"
3854 );
3855 }
3856
3857 /// `TsExpr::multiline_object`, added for Arc C's own first real
3858 /// conversion slice (#1317): `events_fanout.rs`'s own `__eventRoutes`
3859 /// table is a top-level `const` initialiser with one entry per line,
3860 /// each with its own trailing comma (including the last), closing
3861 /// brace back at the statement's own indent — TypeScript's ordinary
3862 /// multi-line object-literal convention, which `TsExpr::object`'s
3863 /// single-line form cannot represent. Only reachable through a
3864 /// statement/declaration-level renderer that carries `depth`
3865 /// (`render_stmt_level_expr`) — this exercises it via a top-level
3866 /// `ConstDecl`, the real shape `events_fanout.rs` itself uses.
3867 #[test]
3868 fn prints_a_multiline_object_as_a_top_level_const_initialiser() {
3869 let mut program = TsProgram::new();
3870 program.push(TsStmt::decl(
3871 TsDecl::ConstDecl {
3872 name: "table".to_string(),
3873 ty: None,
3874 init: TsExpr::multiline_object(vec![
3875 ("a".to_string(), TsExpr::Lit(TsLit::Num("1".to_string()))),
3876 ("b".to_string(), TsExpr::Lit(TsLit::Num("2".to_string()))),
3877 ]),
3878 },
3879 None,
3880 ));
3881 let printed = print(&program, "x.bynk", "", "x.ts");
3882 assert_eq!(printed.text, "const table = {\n a: 1,\n b: 2,\n};\n");
3883 }
3884
3885 /// Review of #1317/#1318, finding 1: an empty `multiline_object` still
3886 /// prints the open/close-brace-on-separate-lines shape, not the tight
3887 /// `{}` a single-line empty object uses — matching the pre-conversion
3888 /// `writeln!` code's own real behaviour (its `for` loop simply not
3889 /// iterating, while the open-brace and closing `};` lines were written
3890 /// unconditionally either way). This is a real, reachable shape:
3891 /// `events_fanout.rs`'s own `__eventRoutes` table can be empty for a
3892 /// context that publishes only events nobody subscribes to.
3893 #[test]
3894 fn an_empty_multiline_object_still_prints_the_open_and_close_brace_on_separate_lines() {
3895 let mut program = TsProgram::new();
3896 program.push(TsStmt::decl(
3897 TsDecl::ConstDecl {
3898 name: "table".to_string(),
3899 ty: None,
3900 init: TsExpr::multiline_object(vec![]),
3901 },
3902 None,
3903 ));
3904 let printed = print(&program, "x.bynk", "", "x.ts");
3905 assert_eq!(printed.text, "const table = {\n};\n");
3906 }
3907
3908 /// A `multiline_object` reached through the ordinary, depth-unaware
3909 /// `render_expr` recursion (nested inside another expression, not a
3910 /// statement's own top-level initialiser) falls back to single-line
3911 /// rendering — documented on `TsExpr::Object` itself as a real,
3912 /// deliberate boundary, not silently wrong. Pinned here so a future
3913 /// change to that boundary is a visible, intentional decision.
3914 #[test]
3915 fn a_nested_multiline_object_falls_back_to_single_line() {
3916 let mut program = TsProgram::new();
3917 program.push(TsStmt::expr_stmt(
3918 TsExpr::array(vec![TsExpr::multiline_object(vec![(
3919 "a".to_string(),
3920 TsExpr::Lit(TsLit::Num("1".to_string())),
3921 )])]),
3922 None,
3923 ));
3924 let printed = print(&program, "x.bynk", "", "x.ts");
3925 assert_eq!(printed.text, "[{ a: 1 }];\n");
3926 }
3927
3928 /// Coverage gap named in review of #1314, finding 4: the blank-line-
3929 /// between-top-level-declarations policy was never tested for the
3930 /// mixed `Verbatim` + real-`Decl` case — the case that actually keeps
3931 /// every P7.6 fixture's zero-diff claim true today, since every P7.6
3932 /// construction site is all-`Verbatim`. Pins both halves of the rule's
3933 /// asymmetry: no blank line is added *after* a `Verbatim` statement
3934 /// (P7.7's own boundary — its content's own trailing spacing isn't the
3935 /// printer's decision), but a blank line *is* added after a real
3936 /// `Decl` even when `Verbatim` follows it.
3937 #[test]
3938 fn no_blank_line_after_verbatim_but_one_before_it() {
3939 let mut program = TsProgram::new();
3940 program.push(TsStmt::verbatim(
3941 VerbatimOrigin::Contracts,
3942 "const legacy = 1;\n",
3943 None,
3944 ));
3945 program.push(TsStmt::decl(
3946 TsDecl::Interface {
3947 name: "A".to_string(),
3948 type_params: Vec::new(),
3949 members: vec![],
3950 },
3951 None,
3952 ));
3953 program.push(TsStmt::verbatim(
3954 VerbatimOrigin::Secrets,
3955 "const trailing = 2;\n",
3956 None,
3957 ));
3958 let printed = print(&program, "x.bynk", "", "x.ts");
3959 assert_eq!(
3960 printed.text,
3961 "const legacy = 1;\ninterface A {\n}\n\nconst trailing = 2;\n"
3962 );
3963 }
3964
3965 /// Coverage gap named in review of #1314, finding 4: nothing exercised
3966 /// `render_block_body`'s own graceful-degradation path — its doc
3967 /// comment promises a single non-`Block` statement still renders
3968 /// sensibly as a one-statement body rather than panicking, but no test
3969 /// ever passed one. `TryCatch` is the real caller that could hit this
3970 /// (its `try_block`/`catch_block` are typed as a bare `TsStmt`, not
3971 /// required to be a `Block`).
3972 #[test]
3973 fn a_try_block_that_is_not_a_block_statement_still_prints_as_a_braced_body() {
3974 let mut program = TsProgram::new();
3975 program.push(TsStmt::try_catch(
3976 TsStmt::expr_stmt(
3977 TsExpr::Call {
3978 callee: Box::new(TsExpr::Ident("risky".to_string())),
3979 args: vec![],
3980 },
3981 None,
3982 ),
3983 Some("e"),
3984 TsStmt::expr_stmt(
3985 TsExpr::Call {
3986 callee: Box::new(TsExpr::Ident("handle".to_string())),
3987 args: vec![TsExpr::Ident("e".to_string())],
3988 },
3989 None,
3990 ),
3991 None,
3992 ));
3993 let printed = print(&program, "x.bynk", "", "x.ts");
3994 assert_eq!(
3995 printed.text,
3996 "try {\n risky();\n} catch (e) {\n handle(e);\n}\n"
3997 );
3998 }
3999
4000 /// Coverage gap named in review of #1314, finding 4: `render_inline_stmt`'s
4001 /// shared fallback arm (finding 3's fix — explicit variants, not a
4002 /// wildcard) was never actually exercised by a test; every existing
4003 /// brace-free `if`/`for...of` body used `Continue`/`Return`/`ExprStmt`,
4004 /// which each have their own dedicated inline arm.
4005 #[test]
4006 fn a_brace_free_if_body_that_is_not_one_of_the_dedicated_inline_kinds_still_prints() {
4007 let mut program = TsProgram::new();
4008 program.push(TsStmt::if_stmt(
4009 TsExpr::Ident("cond".to_string()),
4010 TsStmt::const_stmt(
4011 TsBindingName::Ident("x".to_string()),
4012 None,
4013 TsExpr::Lit(TsLit::Num("1".to_string())),
4014 None,
4015 ),
4016 None,
4017 ));
4018 let printed = print(&program, "x.bynk", "", "x.ts");
4019 assert_eq!(printed.text, "if (cond) const x = 1;\n");
4020 }
4021
4022 /// Review of #1314, finding 2: nothing checked operator-precedence
4023 /// parenthesisation outside `As` — a `Binary` nested as the operand of
4024 /// `Unary`/`Member`/`Await` printed with no parens at all, silently
4025 /// changing what the text parses back to. Three of the review's own
4026 /// examples, pinned directly.
4027 #[test]
4028 fn parenthesises_a_binary_operand_of_unary_member_and_await() {
4029 let bin = || TsExpr::Binary {
4030 op: TsBinaryOp::NullishCoalescing,
4031 left: Box::new(TsExpr::Ident("a".to_string())),
4032 right: Box::new(TsExpr::Ident("b".to_string())),
4033 };
4034
4035 let mut not_program = TsProgram::new();
4036 not_program.push(TsStmt::expr_stmt(
4037 TsExpr::Unary {
4038 op: TsUnaryOp::Not,
4039 expr: Box::new(bin()),
4040 },
4041 None,
4042 ));
4043 assert_eq!(
4044 print(¬_program, "x.bynk", "", "x.ts").text,
4045 "!(a ?? b);\n"
4046 );
4047
4048 let mut member_program = TsProgram::new();
4049 member_program.push(TsStmt::expr_stmt(
4050 TsExpr::Member {
4051 object: Box::new(bin()),
4052 property: "c".to_string(),
4053 },
4054 None,
4055 ));
4056 assert_eq!(
4057 print(&member_program, "x.bynk", "", "x.ts").text,
4058 "(a ?? b).c;\n"
4059 );
4060
4061 let mut await_program = TsProgram::new();
4062 await_program.push(TsStmt::expr_stmt(TsExpr::Await(Box::new(bin())), None));
4063 assert_eq!(
4064 print(&await_program, "x.bynk", "", "x.ts").text,
4065 "await (a ?? b);\n"
4066 );
4067 }
4068
4069 /// Review of #1314, finding 2's fourth example: a right-nested `Binary`
4070 /// (`a ?? (b ?? c)`, as built) must not print as the bare `a ?? b ?? c`
4071 /// that a naive left-to-right walk would produce — that text parses
4072 /// back as `(a ?? b) ?? c`, a different tree than what was built.
4073 #[test]
4074 fn parenthesises_a_nested_binary_operand_of_another_binary() {
4075 let mut program = TsProgram::new();
4076 program.push(TsStmt::expr_stmt(
4077 TsExpr::Binary {
4078 op: TsBinaryOp::NullishCoalescing,
4079 left: Box::new(TsExpr::Ident("a".to_string())),
4080 right: Box::new(TsExpr::Binary {
4081 op: TsBinaryOp::NullishCoalescing,
4082 left: Box::new(TsExpr::Ident("b".to_string())),
4083 right: Box::new(TsExpr::Ident("c".to_string())),
4084 }),
4085 },
4086 None,
4087 ));
4088 let printed = print(&program, "x.bynk", "", "x.ts");
4089 assert_eq!(printed.text, "a ?? (b ?? c);\n");
4090 }
4091
4092 /// Review of #1314, finding 1: `TsLit::Str` escaped only `"`/`\`, so a
4093 /// literal containing a newline, tab, or carriage return printed raw —
4094 /// unterminated-string-literal TypeScript `tsc` can't parse. Pins the
4095 /// fix against every character `bynk_check::wire_default::
4096 /// escape_ts_literal` escapes.
4097 #[test]
4098 fn escapes_newline_tab_and_carriage_return_in_a_string_literal() {
4099 let mut program = TsProgram::new();
4100 program.push(TsStmt::expr_stmt(
4101 TsExpr::Lit(TsLit::Str("line one\nline two\ttabbed\r".to_string())),
4102 None,
4103 ));
4104 let printed = print(&program, "x.bynk", "", "x.ts");
4105 assert_eq!(printed.text, "\"line one\\nline two\\ttabbed\\r\";\n");
4106 }
4107
4108 // -- Arc C slice 3 (#1321, `workers.rs`): the five new shapes Decision A
4109 // names, plus the real gaps found beyond it (`TsDecl::Function`/
4110 // `TsDecl::TypeAlias`/`TsDecl::ImportNamespace`, the parameterless
4111 // `catch {}` form, and the two new `TsUnaryOp`/four new `TsBinaryOp`
4112 // operators). --
4113
4114 #[test]
4115 fn prints_a_shorthand_async_method_entry_in_a_multiline_object() {
4116 let mut program = TsProgram::new();
4117 program.push(TsStmt::return_stmt(
4118 Some(TsExpr::multiline_object_entries(vec![
4119 TsObjectEntry::Method {
4120 name: "foo".to_string(),
4121 is_async: true,
4122 generics: Vec::new(),
4123 params: vec![TsParam {
4124 name: "a".to_string(),
4125 ty: Some(TsType::named("string")),
4126 optional: false,
4127 }],
4128 return_type: None,
4129 doc: None,
4130 inline: false,
4131 body: vec![TsStmt::return_stmt(
4132 Some(TsExpr::Ident("a".to_string())),
4133 None,
4134 )],
4135 },
4136 ])),
4137 None,
4138 ));
4139 let printed = print(&program, "x.bynk", "", "x.ts");
4140 assert_eq!(
4141 printed.text,
4142 "return {\n async foo(a: string) {\n return a;\n },\n};\n"
4143 );
4144 }
4145
4146 #[test]
4147 fn prints_shorthand_and_spread_entries_inline() {
4148 let mut program = TsProgram::new();
4149 program.push(TsStmt::expr_stmt(
4150 TsExpr::object_entries(vec![
4151 TsObjectEntry::Spread(TsExpr::Ident("deps".to_string())),
4152 TsObjectEntry::Shorthand("cap1".to_string()),
4153 TsObjectEntry::Prop(
4154 "identity".to_string(),
4155 TsExpr::Ident("__caller".to_string()),
4156 ),
4157 ]),
4158 None,
4159 ));
4160 let printed = print(&program, "x.bynk", "", "x.ts");
4161 assert_eq!(printed.text, "{ ...deps, cap1, identity: __caller };\n");
4162 }
4163
4164 #[test]
4165 fn prints_an_expression_bodied_arrow() {
4166 let mut program = TsProgram::new();
4167 program.push(TsStmt::expr_stmt(
4168 TsExpr::Arrow {
4169 params: vec![TsParam {
4170 name: "events".to_string(),
4171 ty: Some(TsType::named_with_args(
4172 "Array",
4173 vec![TsType::named("Wire")],
4174 )),
4175 optional: false,
4176 }],
4177 is_async: false,
4178 generics: Vec::new(),
4179 return_type: None,
4180 body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Call {
4181 callee: Box::new(TsExpr::Ident("dispatch".to_string())),
4182 args: vec![TsExpr::Ident("events".to_string())],
4183 }))),
4184 },
4185 None,
4186 ));
4187 let printed = print(&program, "x.bynk", "", "x.ts");
4188 assert_eq!(printed.text, "(events: Array<Wire>) => dispatch(events);\n");
4189 }
4190
4191 /// #1327's own real gap: `emit_composition_root`'s `__eventsDispatch`
4192 /// closure is the first real `Arrow` site that's async.
4193 #[test]
4194 fn prints_an_async_arrow() {
4195 let mut program = TsProgram::new();
4196 program.push(TsStmt::expr_stmt(
4197 TsExpr::Arrow {
4198 params: vec![TsParam {
4199 name: "events".to_string(),
4200 ty: Some(TsType::named_with_args(
4201 "Array",
4202 vec![TsType::named("Wire")],
4203 )),
4204 optional: false,
4205 }],
4206 is_async: true,
4207 generics: Vec::new(),
4208 return_type: None,
4209 body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Ident(
4210 "{ dispatch(events); }".to_string(),
4211 )))),
4212 },
4213 None,
4214 ));
4215 let printed = print(&program, "x.bynk", "", "x.ts");
4216 assert_eq!(
4217 printed.text,
4218 "async (events: Array<Wire>) => { dispatch(events); };\n"
4219 );
4220 }
4221
4222 /// #1435 (Arc E slice 1): [`TsArrowBody::Block`], the first real
4223 /// statement-bodied arrow — pins the exact `serialisation.rs` `Float`
4224 /// non-finite guard shape (`serialise_field_expr_wire`'s own
4225 /// `WireRef::Base { base: Float, .. }` arm) directly against a hand-built
4226 /// tree, the same way #1322's own three parenthesisation tests below pin
4227 /// their own gap. Proves `render_arrow_body`'s `Block` arm reuses
4228 /// `render_compact_stmts` (an `If` with a brace-free `Throw` then-branch,
4229 /// followed by a `Return`) correctly: both statements land on the SAME
4230 /// physical line as the arrow's own `{ ... }`, with no trailing newline
4231 /// leaking out of the block into the enclosing call's own `;`.
4232 #[test]
4233 fn prints_a_block_bodied_arrow_iife() {
4234 let mut program = TsProgram::new();
4235 let guard = TsExpr::Call {
4236 callee: Box::new(TsExpr::Arrow {
4237 params: vec![TsParam {
4238 name: "v".to_string(),
4239 ty: Some(TsType::named("number")),
4240 optional: false,
4241 }],
4242 is_async: false,
4243 generics: Vec::new(),
4244 return_type: None,
4245 body: Box::new(TsArrowBody::Block(vec![
4246 TsStmt::if_stmt(
4247 TsExpr::Unary {
4248 op: TsUnaryOp::Not,
4249 expr: Box::new(TsExpr::Call {
4250 callee: Box::new(TsExpr::Member {
4251 object: Box::new(TsExpr::Ident("Number".to_string())),
4252 property: "isFinite".to_string(),
4253 }),
4254 args: vec![TsExpr::Ident("v".to_string())],
4255 }),
4256 },
4257 TsStmt::throw_stmt(
4258 TsExpr::New {
4259 callee: Box::new(TsExpr::Ident("Error".to_string())),
4260 args: vec![TsExpr::Lit(TsLit::Str(
4261 "non-finite Float at boundary".to_string(),
4262 ))],
4263 },
4264 None,
4265 ),
4266 None,
4267 ),
4268 TsStmt::return_stmt(
4269 Some(TsExpr::As {
4270 expr: Box::new(TsExpr::Ident("v".to_string())),
4271 ty: TsType::named("JsonValue"),
4272 }),
4273 None,
4274 ),
4275 ])),
4276 }),
4277 args: vec![TsExpr::Ident("value".to_string())],
4278 };
4279 program.push(TsStmt::expr_stmt(guard, None));
4280 let printed = print(&program, "x.bynk", "", "x.ts");
4281 assert_eq!(
4282 printed.text,
4283 "((v: number) => { if (!Number.isFinite(v)) throw new Error(\"non-finite Float at boundary\"); return v as JsonValue; })(value);\n"
4284 );
4285 }
4286
4287 /// Review of #1322, finding 1: `Arrow` was missing from every
4288 /// parenthesisation rule. Not reachable through any real `bynk-emit`
4289 /// content today (the one grounded `Arrow` site is an object-property
4290 /// value, rendered through the depth-unaware `render_expr` with no
4291 /// operand context) — these three tests pin the fix directly against
4292 /// hand-built trees, the same way #1314's own precedence tests did
4293 /// before any real content exercised them.
4294 #[test]
4295 fn parenthesises_an_arrow_used_as_a_call_callee() {
4296 let mut program = TsProgram::new();
4297 program.push(TsStmt::expr_stmt(
4298 TsExpr::Call {
4299 callee: Box::new(TsExpr::Arrow {
4300 params: vec![TsParam {
4301 name: "x".to_string(),
4302 ty: None,
4303 optional: false,
4304 }],
4305 is_async: false,
4306 generics: Vec::new(),
4307 return_type: None,
4308 body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Ident("x".to_string())))),
4309 }),
4310 args: vec![TsExpr::Lit(TsLit::Num("1".to_string()))],
4311 },
4312 None,
4313 ));
4314 let printed = print(&program, "x.bynk", "", "x.ts");
4315 assert_eq!(printed.text, "((x) => x)(1);\n");
4316 }
4317
4318 #[test]
4319 fn parenthesises_an_arrow_used_as_a_binary_operand() {
4320 let mut program = TsProgram::new();
4321 program.push(TsStmt::expr_stmt(
4322 TsExpr::Binary {
4323 op: TsBinaryOp::NullishCoalescing,
4324 left: Box::new(TsExpr::Ident("a".to_string())),
4325 right: Box::new(TsExpr::Arrow {
4326 params: vec![TsParam {
4327 name: "x".to_string(),
4328 ty: None,
4329 optional: false,
4330 }],
4331 is_async: false,
4332 generics: Vec::new(),
4333 return_type: None,
4334 body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Ident("y".to_string())))),
4335 }),
4336 },
4337 None,
4338 ));
4339 let printed = print(&program, "x.bynk", "", "x.ts");
4340 assert_eq!(printed.text, "a ?? ((x) => y);\n");
4341 }
4342
4343 #[test]
4344 fn parenthesises_an_arrow_used_as_an_as_operand() {
4345 let mut program = TsProgram::new();
4346 program.push(TsStmt::expr_stmt(
4347 TsExpr::As {
4348 expr: Box::new(TsExpr::Arrow {
4349 params: vec![TsParam {
4350 name: "x".to_string(),
4351 ty: None,
4352 optional: false,
4353 }],
4354 is_async: false,
4355 generics: Vec::new(),
4356 return_type: None,
4357 body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Ident("y".to_string())))),
4358 }),
4359 ty: TsType::named("Handler"),
4360 },
4361 None,
4362 ));
4363 let printed = print(&program, "x.bynk", "", "x.ts");
4364 assert_eq!(printed.text, "((x) => y) as Handler;\n");
4365 }
4366
4367 #[test]
4368 fn prints_optional_member_and_optional_index_chained() {
4369 let mut program = TsProgram::new();
4370 program.push(TsStmt::expr_stmt(
4371 TsExpr::OptionalIndex {
4372 object: Box::new(TsExpr::OptionalMember {
4373 object: Box::new(TsExpr::Ident("x".to_string())),
4374 property: "process".to_string(),
4375 }),
4376 index: Box::new(TsExpr::Lit(TsLit::Str("secret".to_string()))),
4377 },
4378 None,
4379 ));
4380 let printed = print(&program, "x.bynk", "", "x.ts");
4381 assert_eq!(printed.text, "x?.process?.[\"secret\"];\n");
4382 }
4383
4384 #[test]
4385 fn print_type_renders_an_optional_object_field() {
4386 let ty = TsType::Object(vec![
4387 TsTypeMember::optional_prop("env", TsType::named("unknown")),
4388 TsTypeMember::prop("name", TsType::named("string")),
4389 ]);
4390 assert_eq!(print_type(&ty), "{ env?: unknown; name: string }");
4391 }
4392
4393 #[test]
4394 fn prints_a_top_level_function_declaration() {
4395 let mut program = TsProgram::new();
4396 program.push(TsStmt::decl(
4397 TsDecl::Export(Box::new(TsDecl::Function {
4398 name: "compose".to_string(),
4399 generics: Vec::new(),
4400 params: vec![TsParam {
4401 name: "env".to_string(),
4402 ty: Some(TsType::named("Env")),
4403 optional: false,
4404 }],
4405 return_type: None,
4406 body: vec![TsStmt::return_stmt(Some(TsExpr::Lit(TsLit::Null)), None)],
4407 is_async: false,
4408 inline: false,
4409 })),
4410 None,
4411 ));
4412 let printed = print(&program, "x.bynk", "", "x.ts");
4413 assert_eq!(
4414 printed.text,
4415 "export function compose(env: Env) {\n return null;\n}\n"
4416 );
4417 }
4418
4419 /// #1351's own real gap: `TsDecl::Function` had no `generics` field —
4420 /// `emit_free_fn`'s own v0.20a erased generics (`export function
4421 /// foo<A, B>(...)`) needed one. Review of #1352, finding 3: every other
4422 /// existing `TsDecl::Function` test only threads `generics: Vec::new()`
4423 /// through; this pins the non-empty case directly, matching #1339's own
4424 /// precedent of a dedicated test per new field.
4425 #[test]
4426 fn prints_a_generic_top_level_function_declaration() {
4427 let mut program = TsProgram::new();
4428 program.push(TsStmt::decl(
4429 TsDecl::Export(Box::new(TsDecl::Function {
4430 name: "identity".to_string(),
4431 generics: vec!["A".to_string(), "B".to_string()],
4432 params: vec![TsParam {
4433 name: "x".to_string(),
4434 ty: Some(TsType::named("A")),
4435 optional: false,
4436 }],
4437 return_type: Some(TsType::named("A")),
4438 body: vec![TsStmt::return_stmt(
4439 Some(TsExpr::Ident("x".to_string())),
4440 None,
4441 )],
4442 is_async: false,
4443 inline: false,
4444 })),
4445 None,
4446 ));
4447 let printed = print(&program, "x.bynk", "", "x.ts");
4448 assert_eq!(
4449 printed.text,
4450 "export function identity<A, B>(x: A): A {\n return x;\n}\n"
4451 );
4452 }
4453
4454 #[test]
4455 fn prints_a_top_level_type_alias() {
4456 let mut program = TsProgram::new();
4457 program.push(TsStmt::decl(
4458 TsDecl::TypeAlias {
4459 name: "Foo".to_string(),
4460 type_params: Vec::new(),
4461 ty: TsType::named("{ get(id: any): any }"),
4462 },
4463 None,
4464 ));
4465 let printed = print(&program, "x.bynk", "", "x.ts");
4466 assert_eq!(printed.text, "type Foo = { get(id: any): any };\n");
4467 }
4468
4469 #[test]
4470 fn prints_a_namespace_import_adjacent_to_a_named_import_with_no_blank_line() {
4471 let mut program = TsProgram::new();
4472 program.push(TsStmt::decl(
4473 TsDecl::Import {
4474 type_only: false,
4475 names: vec!["a".to_string()],
4476 from: "./x.js".to_string(),
4477 },
4478 None,
4479 ));
4480 program.push(TsStmt::decl(
4481 TsDecl::ImportNamespace {
4482 type_only: false,
4483 alias: "handlers".to_string(),
4484 from: "./handlers.js".to_string(),
4485 },
4486 None,
4487 ));
4488 let printed = print(&program, "x.bynk", "", "x.ts");
4489 assert_eq!(
4490 printed.text,
4491 "import { a } from \"./x.js\";\nimport * as handlers from \"./handlers.js\";\n"
4492 );
4493 }
4494
4495 /// Arc C, step (10) (#1392): `ImportNamespace.type_only`, a parallel gap
4496 /// by omission — `TsDecl::Import` already had this field, but nothing
4497 /// before `emit_cross_context_namespace_imports`'s own conversion built
4498 /// a type-only namespace import (`import type * as ns from "...";`, a
4499 /// Workers-mode consumed-context import reaching the callee's types
4500 /// only, #661).
4501 #[test]
4502 fn prints_a_type_only_namespace_import() {
4503 let mut program = TsProgram::new();
4504 program.push(TsStmt::decl(
4505 TsDecl::ImportNamespace {
4506 type_only: true,
4507 alias: "commerce_payment".to_string(),
4508 from: "../commerce-payment/handlers.js".to_string(),
4509 },
4510 None,
4511 ));
4512 let printed = print(&program, "x.bynk", "", "x.ts");
4513 assert_eq!(
4514 printed.text,
4515 "import type * as commerce_payment from \"../commerce-payment/handlers.js\";\n"
4516 );
4517 }
4518
4519 /// Arc C, slice 37 (#1409, `tests_emit.rs`'s own slice G): `emit_integration_
4520 /// module`'s own per-participant `import worker_{ns} from "../workers/{dir}/
4521 /// index.js";` (the participant's Worker entry module's default export) —
4522 /// the first real default import anywhere in `bynk-emit`'s own converted
4523 /// content.
4524 #[test]
4525 fn prints_a_default_import() {
4526 let mut program = TsProgram::new();
4527 program.push(TsStmt::decl(
4528 TsDecl::ImportDefault {
4529 alias: "worker_shop_api".to_string(),
4530 from: "../workers/shop-api/index.js".to_string(),
4531 },
4532 None,
4533 ));
4534 let printed = print(&program, "x.bynk", "", "x.ts");
4535 assert_eq!(
4536 printed.text,
4537 "import worker_shop_api from \"../workers/shop-api/index.js\";\n"
4538 );
4539 }
4540
4541 /// #1321's own real gap beyond the accepted proposal's five: ES2019's
4542 /// optional catch binding (`emit_http_sum_wrapper`'s own `} catch {`,
4543 /// no `(e)` at all).
4544 #[test]
4545 fn prints_a_parameterless_catch() {
4546 let mut program = TsProgram::new();
4547 program.push(TsStmt::try_catch(
4548 TsStmt::block(vec![], None),
4549 None::<String>,
4550 TsStmt::block(vec![], None),
4551 None,
4552 ));
4553 let printed = print(&program, "x.bynk", "", "x.ts");
4554 assert_eq!(printed.text, "try {\n} catch {\n}\n");
4555 }
4556
4557 #[test]
4558 fn prints_typeof_and_the_new_comparison_and_logical_operators() {
4559 let mut program = TsProgram::new();
4560 program.push(TsStmt::expr_stmt(
4561 TsExpr::Unary {
4562 op: TsUnaryOp::Typeof,
4563 expr: Box::new(TsExpr::Ident("x".to_string())),
4564 },
4565 None,
4566 ));
4567 program.push(TsStmt::expr_stmt(
4568 TsExpr::Binary {
4569 op: TsBinaryOp::StrictEq,
4570 left: Box::new(TsExpr::Ident("a".to_string())),
4571 right: Box::new(TsExpr::Ident("b".to_string())),
4572 },
4573 None,
4574 ));
4575 program.push(TsStmt::expr_stmt(
4576 TsExpr::Binary {
4577 op: TsBinaryOp::StrictNotEq,
4578 left: Box::new(TsExpr::Ident("a".to_string())),
4579 right: Box::new(TsExpr::Ident("b".to_string())),
4580 },
4581 None,
4582 ));
4583 program.push(TsStmt::expr_stmt(
4584 TsExpr::Binary {
4585 op: TsBinaryOp::And,
4586 left: Box::new(TsExpr::Ident("a".to_string())),
4587 right: Box::new(TsExpr::Ident("b".to_string())),
4588 },
4589 None,
4590 ));
4591 program.push(TsStmt::expr_stmt(
4592 TsExpr::Binary {
4593 op: TsBinaryOp::Or,
4594 left: Box::new(TsExpr::Ident("a".to_string())),
4595 right: Box::new(TsExpr::Ident("b".to_string())),
4596 },
4597 None,
4598 ));
4599 let printed = print(&program, "x.bynk", "", "x.ts");
4600 assert_eq!(
4601 printed.text,
4602 "typeof x;\n\na === b;\n\na !== b;\n\na && b;\n\na || b;\n"
4603 );
4604 }
4605
4606 /// #1321's own real gap: with only `??` in the algebra, a nested
4607 /// `Binary` operand of another `Binary` was always parenthesized,
4608 /// regardless of operator (see `parenthesises_a_nested_binary_operand_
4609 /// of_another_binary`, unchanged). `workers.rs`'s own real content
4610 /// nests a *strictly higher precedence* comparison inside `||`/`&&`
4611 /// (`__authz === null || !__authz.startsWith(...)`) and the byte-golden
4612 /// fixtures have no parens around it — this pins that the printer
4613 /// omits them precisely in that case, not more broadly.
4614 #[test]
4615 fn a_strictly_higher_precedence_comparison_needs_no_parens_inside_or_or_and() {
4616 let mut program = TsProgram::new();
4617 program.push(TsStmt::expr_stmt(
4618 TsExpr::Binary {
4619 op: TsBinaryOp::Or,
4620 left: Box::new(TsExpr::Binary {
4621 op: TsBinaryOp::StrictEq,
4622 left: Box::new(TsExpr::Ident("a".to_string())),
4623 right: Box::new(TsExpr::Lit(TsLit::Null)),
4624 }),
4625 right: Box::new(TsExpr::Unary {
4626 op: TsUnaryOp::Not,
4627 expr: Box::new(TsExpr::Ident("b".to_string())),
4628 }),
4629 },
4630 None,
4631 ));
4632 let printed = print(&program, "x.bynk", "", "x.ts");
4633 assert_eq!(printed.text, "a === null || !b;\n");
4634 }
4635
4636 // -- #1323 (workers_entry.rs): Switch, ExportDefault, Conditional,
4637 // TsType::Object readonly/method members, and the smaller mechanical
4638 // gaps found during implementation (ReExport, Blank, TsLit::Bool,
4639 // TsExpr::Paren, same-operator-chain flattening). --
4640
4641 /// #1323's own largest gap: `switch (<discriminant>) { <cases> }`. A
4642 /// non-`default` `case` is `{ }`-blocked; `default` is not — pinned
4643 /// against `workers_entry.rs`'s own real internal-dispatch shape.
4644 #[test]
4645 fn prints_a_switch_with_a_braced_case_and_an_unbraced_default() {
4646 let mut program = TsProgram::new();
4647 program.push(TsStmt::switch_stmt(
4648 TsExpr::Ident("servicePath".to_string()),
4649 vec![
4650 TsSwitchCase {
4651 test: Some(TsExpr::Lit(TsLit::Str("orders".to_string()))),
4652 body: vec![TsStmt::return_stmt(None, None)],
4653 default_braced: false,
4654 case_braced: true,
4655 },
4656 TsSwitchCase {
4657 test: None,
4658 body: vec![TsStmt::expr_stmt(
4659 TsExpr::Call {
4660 callee: Box::new(TsExpr::Member {
4661 object: Box::new(TsExpr::Ident("console".to_string())),
4662 property: "log".to_string(),
4663 }),
4664 args: vec![],
4665 },
4666 None,
4667 )],
4668 default_braced: false,
4669 case_braced: true,
4670 },
4671 ],
4672 None,
4673 ));
4674 let printed = print(&program, "x.bynk", "", "x.ts");
4675 assert_eq!(
4676 printed.text,
4677 "switch (servicePath) {\n case \"orders\": {\n return;\n }\n default:\n console.log();\n}\n"
4678 );
4679 }
4680
4681 /// Arc C, slice 33 (`tests_emit.rs` slice C, #1401): `TsSwitchCase.
4682 /// default_braced` — `emit_stub_class`'s own `ReturnsEach` dispatch
4683 /// braces its `default` case, unlike `workers_entry.rs`'s own unbraced
4684 /// one pinned just above.
4685 #[test]
4686 fn prints_a_switch_with_a_braced_default() {
4687 let mut program = TsProgram::new();
4688 program.push(TsStmt::switch_stmt(
4689 TsExpr::Ident("__k".to_string()),
4690 vec![TsSwitchCase {
4691 test: None,
4692 body: vec![TsStmt::expr_stmt(TsExpr::Ident("x".to_string()), None)],
4693 default_braced: true,
4694 case_braced: true,
4695 }],
4696 None,
4697 ));
4698 let printed = print(&program, "x.bynk", "", "x.ts");
4699 assert_eq!(
4700 printed.text,
4701 "switch (__k) {\n default: {\n x;\n }\n}\n"
4702 );
4703 }
4704
4705 /// Arc E slice 6 (#1445): `TsSwitchCase.case_braced` — the mirror-image
4706 /// gap `default_braced` left open. `emit_sum_codec`'s own payload-free
4707 /// variant case is unbraced right beside a braced payload-carrying
4708 /// sibling in the *same* switch, pinned against `212_json_codec`'s own
4709 /// mixed `Status` fixture (`case "Pending": return { kind: "Pending"
4710 /// };` then `case "Shipped": { ... }`).
4711 #[test]
4712 fn prints_a_switch_with_an_unbraced_case_beside_a_braced_one() {
4713 let mut program = TsProgram::new();
4714 program.push(TsStmt::switch_stmt(
4715 TsExpr::Ident("kind".to_string()),
4716 vec![
4717 TsSwitchCase {
4718 test: Some(TsExpr::Lit(TsLit::Str("Pending".to_string()))),
4719 body: vec![TsStmt::return_stmt(
4720 Some(TsExpr::Ident("a".to_string())),
4721 None,
4722 )],
4723 default_braced: false,
4724 case_braced: false,
4725 },
4726 TsSwitchCase {
4727 test: Some(TsExpr::Lit(TsLit::Str("Shipped".to_string()))),
4728 body: vec![TsStmt::return_stmt(
4729 Some(TsExpr::Ident("b".to_string())),
4730 None,
4731 )],
4732 default_braced: false,
4733 case_braced: true,
4734 },
4735 ],
4736 None,
4737 ));
4738 let printed = print(&program, "x.bynk", "", "x.ts");
4739 assert_eq!(
4740 printed.text,
4741 "switch (kind) {\n case \"Pending\":\n return a;\n case \"Shipped\": {\n return b;\n }\n}\n"
4742 );
4743 }
4744
4745 /// #1323's own second gap: `export default <expr>;` — a default export
4746 /// of a bare expression, not a declaration. Uses the same depth-aware
4747 /// multiline-object handling `const`/`let`/`return` already get.
4748 #[test]
4749 fn prints_an_export_default_object_literal_multiline() {
4750 let mut program = TsProgram::new();
4751 program.push(TsStmt::decl(
4752 TsDecl::ExportDefault(TsExpr::multiline_object(vec![(
4753 "fetch".to_string(),
4754 TsExpr::Ident("handler".to_string()),
4755 )])),
4756 None,
4757 ));
4758 let printed = print(&program, "x.bynk", "", "x.ts");
4759 assert_eq!(printed.text, "export default {\n fetch: handler,\n};\n");
4760 }
4761
4762 /// #1355's own real gap: a `multiline: true` `TsExpr::Object` nested as
4763 /// one `Prop`'s own value inside ANOTHER multiline object —
4764 /// `emit_messages_bundle`'s own real doubly-nested `{ locale: { code:
4765 /// expr, ... }, ... }` table. Before this slice, `render_multiline_
4766 /// object_entry`'s own `Prop` arm rendered its value through the plain,
4767 /// depth-unaware `render_expr`, which ignores `multiline` entirely —
4768 /// this would have silently collapsed the inner object to one line.
4769 #[test]
4770 fn prints_a_nested_multiline_object_as_a_props_own_value() {
4771 let mut program = TsProgram::new();
4772 program.push(TsStmt::const_stmt(
4773 TsBindingName::Ident("byLocale".to_string()),
4774 None,
4775 TsExpr::multiline_object_entries(vec![TsObjectEntry::Prop(
4776 "\"en\"".to_string(),
4777 TsExpr::multiline_object_entries(vec![
4778 TsObjectEntry::Prop(
4779 "\"greeting\"".to_string(),
4780 TsExpr::Ident("renderGreeting".to_string()),
4781 ),
4782 TsObjectEntry::Prop(
4783 "\"farewell\"".to_string(),
4784 TsExpr::Ident("renderFarewell".to_string()),
4785 ),
4786 ]),
4787 )]),
4788 None,
4789 ));
4790 let printed = print(&program, "x.bynk", "", "x.ts");
4791 assert_eq!(
4792 printed.text,
4793 "const byLocale = {\n \"en\": {\n \"greeting\": renderGreeting,\n \"farewell\": renderFarewell,\n },\n};\n"
4794 );
4795 }
4796
4797 /// Review of #1356, finding 2: `render_multiline_object_entry`'s own
4798 /// `Prop` arm routes through `render_stmt_level_expr` for BOTH
4799 /// `TsExpr::Object`/`Array` (see `TsExpr::Array`'s own doc, updated by
4800 /// #1355) — the object case is pinned above, this is the array sibling,
4801 /// making that doc claim self-verifying rather than merely asserted.
4802 #[test]
4803 fn prints_a_nested_multiline_array_as_a_props_own_value() {
4804 let mut program = TsProgram::new();
4805 program.push(TsStmt::const_stmt(
4806 TsBindingName::Ident("byGroup".to_string()),
4807 None,
4808 TsExpr::multiline_object_entries(vec![TsObjectEntry::Prop(
4809 "\"a\"".to_string(),
4810 TsExpr::multiline_array(vec![
4811 TsExpr::Ident("one".to_string()),
4812 TsExpr::Ident("two".to_string()),
4813 ]),
4814 )]),
4815 None,
4816 ));
4817 let printed = print(&program, "x.bynk", "", "x.ts");
4818 assert_eq!(
4819 printed.text,
4820 "const byGroup = {\n \"a\": [\n one,\n two,\n ],\n};\n"
4821 );
4822 }
4823
4824 /// #1323's own third gap: `test ? consequent : alternate`.
4825 #[test]
4826 fn prints_a_conditional_expression() {
4827 let mut program = TsProgram::new();
4828 program.push(TsStmt::expr_stmt(
4829 TsExpr::Conditional {
4830 test: Box::new(TsExpr::Binary {
4831 op: TsBinaryOp::StrictEq,
4832 left: Box::new(TsExpr::Ident("method".to_string())),
4833 right: Box::new(TsExpr::Lit(TsLit::Str("HEAD".to_string()))),
4834 }),
4835 consequent: Box::new(TsExpr::Lit(TsLit::Num("1".to_string()))),
4836 alternate: Box::new(TsExpr::Lit(TsLit::Num("2".to_string()))),
4837 },
4838 None,
4839 ));
4840 let printed = print(&program, "x.bynk", "", "x.ts");
4841 assert_eq!(printed.text, "method === \"HEAD\" ? 1 : 2;\n");
4842 }
4843
4844 /// Review-anticipated gap (matching #1322 finding 1's own class): a
4845 /// `Conditional` used as a binary operand, an `as`-operand, or a call
4846 /// callee all need parens — closed proactively in this same slice
4847 /// rather than left for a review round to re-find.
4848 #[test]
4849 fn parenthesises_a_conditional_used_as_a_binary_operand_and_an_as_operand() {
4850 let cond = || TsExpr::Conditional {
4851 test: Box::new(TsExpr::Ident("a".to_string())),
4852 consequent: Box::new(TsExpr::Ident("b".to_string())),
4853 alternate: Box::new(TsExpr::Ident("c".to_string())),
4854 };
4855 let mut program = TsProgram::new();
4856 program.push(TsStmt::expr_stmt(
4857 TsExpr::Binary {
4858 op: TsBinaryOp::NullishCoalescing,
4859 left: Box::new(TsExpr::Ident("x".to_string())),
4860 right: Box::new(cond()),
4861 },
4862 None,
4863 ));
4864 program.push(TsStmt::expr_stmt(
4865 TsExpr::As {
4866 expr: Box::new(cond()),
4867 ty: TsType::named("string"),
4868 },
4869 None,
4870 ));
4871 let printed = print(&program, "x.bynk", "", "x.ts");
4872 assert_eq!(
4873 printed.text,
4874 "x ?? (a ? b : c);\n\n(a ? b : c) as string;\n"
4875 );
4876 }
4877
4878 /// #1323's own real gap: a `Method` member of a type-position object
4879 /// literal (no body), alongside a `readonly` property.
4880 #[test]
4881 fn print_type_renders_a_readonly_field_and_a_method_signature() {
4882 let ty = TsType::Object(vec![
4883 TsTypeMember::readonly_prop("cron", TsType::named("string")),
4884 TsTypeMember::method("ack", vec![], TsType::named("void")),
4885 TsTypeMember::method(
4886 "waitUntil",
4887 vec![TsParam {
4888 name: "promise".to_string(),
4889 ty: Some(TsType::named_with_args(
4890 "Promise",
4891 vec![TsType::named("unknown")],
4892 )),
4893 optional: false,
4894 }],
4895 TsType::named("void"),
4896 ),
4897 ]);
4898 assert_eq!(
4899 print_type(&ty),
4900 "{ readonly cron: string; ack(): void; waitUntil(promise: Promise<unknown>): void }"
4901 );
4902 }
4903
4904 /// #1323's own real gap: `TsObjectEntry::Method` gained a
4905 /// `return_type` field (mirroring `TsClassMethod`'s own existing one) —
4906 /// `workers_entry.rs`'s own `export default { fetch, scheduled?,
4907 /// queue? }` entries all carry an explicit return-type annotation,
4908 /// unlike `workers.rs`'s own `compose`-returned wrapper methods
4909 /// (`#1321`), none of which do.
4910 #[test]
4911 fn prints_a_multiline_object_methods_own_return_type_annotation() {
4912 let mut program = TsProgram::new();
4913 program.push(TsStmt::decl(
4914 TsDecl::ExportDefault(TsExpr::multiline_object_entries(vec![
4915 TsObjectEntry::Method {
4916 name: "fetch".to_string(),
4917 is_async: true,
4918 generics: Vec::new(),
4919 params: vec![],
4920 return_type: Some(TsType::named_with_args(
4921 "Promise",
4922 vec![TsType::named("Response")],
4923 )),
4924 doc: None,
4925 inline: false,
4926 body: vec![],
4927 },
4928 ])),
4929 None,
4930 ));
4931 let printed = print(&program, "x.bynk", "", "x.ts");
4932 assert_eq!(
4933 printed.text,
4934 "export default {\n async fetch(): Promise<Response> {\n },\n};\n"
4935 );
4936 }
4937
4938 /// #1323's own real correction: `Await` no longer auto-parenthesizes
4939 /// under `As` — `workers_entry.rs`'s own real `await request.json() as
4940 /// JsonValue` has no parens (`as` binds looser than `await`, so none
4941 /// are grammatically needed; P7.8's original "always parenthesize"
4942 /// reasoning conflated one file's own real text with a grammar
4943 /// requirement).
4944 #[test]
4945 fn an_await_used_as_an_as_operand_no_longer_needs_parens() {
4946 let mut program = TsProgram::new();
4947 program.push(TsStmt::const_stmt(
4948 TsBindingName::Ident("args".to_string()),
4949 None,
4950 TsExpr::As {
4951 expr: Box::new(TsExpr::Await(Box::new(TsExpr::Call {
4952 callee: Box::new(TsExpr::Member {
4953 object: Box::new(TsExpr::Ident("request".to_string())),
4954 property: "json".to_string(),
4955 }),
4956 args: vec![],
4957 }))),
4958 ty: TsType::named("JsonValue"),
4959 },
4960 None,
4961 ));
4962 let printed = print(&program, "x.bynk", "", "x.ts");
4963 assert_eq!(
4964 printed.text,
4965 "const args = await request.json() as JsonValue;\n"
4966 );
4967 }
4968
4969 /// #1353's own real gap: `bynk_ts::TsStmtKind::Throw`, added for
4970 /// `emit_contract_guarded_body`'s own real `throw __e;` sites. Review of
4971 /// #1354, finding 1: every real call site today reaches this only
4972 /// through an `InlineBlock` (`render_stmt(out, stmt, 0)`), so the
4973 /// top-level, non-inline `render_stmt` arm's own `indent(depth)` at a
4974 /// nonzero depth was untested — pinned directly here, mirroring
4975 /// `Return`'s own shape exactly.
4976 #[test]
4977 fn prints_a_throw_statement_at_a_nested_depth() {
4978 let stmt = TsStmt::throw_stmt(TsExpr::Ident("__e".to_string()), None);
4979 assert_eq!(print_stmt(&stmt, 2), " throw __e;\n");
4980 }
4981
4982 /// #1353's own real gap, byte-for-byte: the whole contract-guard shape
4983 /// `emit_contract_guarded_body` builds (`if (!(pred)) { const __e = ...;
4984 /// __e.name = ...; throw __e; }`) — an `If` over an `InlineBlock` of a
4985 /// `Const`/`Assign`/`Throw` trio — asserted as one exact string. Review
4986 /// of #1354, finding 1: no existing test (fixture, `contract_behaviour.
4987 /// rs`, `source_map.rs`) asserts this shape's own bytes; they only
4988 /// `contains`-check a runtime message or a generated line number, none
4989 /// would catch a missing space, a reordered statement, or a lost
4990 /// indent.
4991 #[test]
4992 fn prints_a_contract_guard_if_stmt() {
4993 let cond = TsExpr::Unary {
4994 op: TsUnaryOp::Not,
4995 expr: Box::new(TsExpr::Paren(Box::new(TsExpr::Ident("n > 0".to_string())))),
4996 };
4997 let new_error = TsExpr::New {
4998 callee: Box::new(TsExpr::Ident("Error".to_string())),
4999 args: vec![TsExpr::template_lit(
5000 vec![
5001 "contract violated: precondition \\`positive\\` of scale (n=${n})".to_string(),
5002 ],
5003 Vec::new(),
5004 )],
5005 };
5006 let const_e = TsStmt::const_stmt(
5007 TsBindingName::Ident("__e".to_string()),
5008 None,
5009 new_error,
5010 None,
5011 );
5012 let assign_name = TsStmt::assign(
5013 TsExpr::Member {
5014 object: Box::new(TsExpr::Ident("__e".to_string())),
5015 property: "name".to_string(),
5016 },
5017 TsExpr::Lit(TsLit::Str("BynkContractError".to_string())),
5018 None,
5019 );
5020 let throw_e = TsStmt::throw_stmt(TsExpr::Ident("__e".to_string()), None);
5021 let then_branch = TsStmt::inline_block(vec![const_e, assign_name, throw_e], None);
5022 let if_stmt = TsStmt::if_stmt(cond, then_branch, None);
5023 assert_eq!(
5024 print_stmt(&if_stmt, 1),
5025 " if (!(n > 0)) { const __e = new Error(`contract violated: precondition \\`positive\\` of scale (n=${n})`); __e.name = \"BynkContractError\"; throw __e; }\n"
5026 );
5027 }
5028
5029 /// #1323's own real gap: `if`'s own `else` branch, printed on a fresh
5030 /// line at the `if`'s own indent, then following the same block-vs-
5031 /// inline rule the `if` itself does — `workers_entry.rs`'s own real
5032 /// queue-consumer ack/retry dispatch.
5033 #[test]
5034 fn prints_an_if_else_with_an_inline_then_and_an_inline_block_else() {
5035 let mut program = TsProgram::new();
5036 program.push(TsStmt::if_else_stmt(
5037 TsExpr::Binary {
5038 op: TsBinaryOp::StrictEq,
5039 left: Box::new(TsExpr::Member {
5040 object: Box::new(TsExpr::Ident("result".to_string())),
5041 property: "tag".to_string(),
5042 }),
5043 right: Box::new(TsExpr::Lit(TsLit::Str("Ack".to_string()))),
5044 },
5045 TsStmt::expr_stmt(
5046 TsExpr::Call {
5047 callee: Box::new(TsExpr::Member {
5048 object: Box::new(TsExpr::Ident("msg".to_string())),
5049 property: "ack".to_string(),
5050 }),
5051 args: vec![],
5052 },
5053 None,
5054 ),
5055 TsStmt::inline_block(
5056 vec![TsStmt::expr_stmt(
5057 TsExpr::Call {
5058 callee: Box::new(TsExpr::Member {
5059 object: Box::new(TsExpr::Ident("msg".to_string())),
5060 property: "retry".to_string(),
5061 }),
5062 args: vec![],
5063 },
5064 None,
5065 )],
5066 None,
5067 ),
5068 None,
5069 ));
5070 let printed = print(&program, "x.bynk", "", "x.ts");
5071 assert_eq!(
5072 printed.text,
5073 "if (result.tag === \"Ack\") msg.ack();\nelse { msg.retry(); }\n"
5074 );
5075 }
5076
5077 /// #1323's own real gap: a `TryCatch`'s own `catch` body can be an
5078 /// `InlineBlock` too — the braces stay on their own lines (this shape's
5079 /// usual convention), but the content packs onto one compact line.
5080 /// `workers_entry.rs`'s own real queue-consumer catch clause.
5081 #[test]
5082 fn a_try_catchs_own_catch_block_can_be_an_inline_block() {
5083 let mut program = TsProgram::new();
5084 program.push(TsStmt::try_catch(
5085 TsStmt::block(
5086 vec![TsStmt::expr_stmt(TsExpr::Ident("work".to_string()), None)],
5087 None,
5088 ),
5089 Some("e"),
5090 TsStmt::inline_block(
5091 vec![
5092 TsStmt::expr_stmt(
5093 TsExpr::Call {
5094 callee: Box::new(TsExpr::Member {
5095 object: Box::new(TsExpr::Ident("console".to_string())),
5096 property: "error".to_string(),
5097 }),
5098 args: vec![TsExpr::Ident("e".to_string())],
5099 },
5100 None,
5101 ),
5102 TsStmt::expr_stmt(
5103 TsExpr::Call {
5104 callee: Box::new(TsExpr::Member {
5105 object: Box::new(TsExpr::Ident("msg".to_string())),
5106 property: "retry".to_string(),
5107 }),
5108 args: vec![],
5109 },
5110 None,
5111 ),
5112 ],
5113 None,
5114 ),
5115 None,
5116 ));
5117 let printed = print(&program, "x.bynk", "", "x.ts");
5118 assert_eq!(
5119 printed.text,
5120 "try {\n work;\n} catch (e) {\n console.error(e); msg.retry();\n}\n"
5121 );
5122 }
5123
5124 /// #1323's own real gap: `TsBinaryOp::GreaterThan` — the request-body
5125 /// ceiling guard's own `Number(__contentLength) > <cap>`, the one real
5126 /// site needing a relational (not equality) comparison.
5127 #[test]
5128 fn prints_a_greater_than_comparison() {
5129 let mut program = TsProgram::new();
5130 program.push(TsStmt::expr_stmt(
5131 TsExpr::Binary {
5132 op: TsBinaryOp::GreaterThan,
5133 left: Box::new(TsExpr::Call {
5134 callee: Box::new(TsExpr::Ident("Number".to_string())),
5135 args: vec![TsExpr::Ident("__contentLength".to_string())],
5136 }),
5137 right: Box::new(TsExpr::Lit(TsLit::Num("100".to_string()))),
5138 },
5139 None,
5140 ));
5141 let printed = print(&program, "x.bynk", "", "x.ts");
5142 assert_eq!(printed.text, "Number(__contentLength) > 100;\n");
5143 }
5144
5145 /// #1323's own real gap: `TsTypeMember::Index` — `workers_entry.rs`'s
5146 /// own `args as { [k: string]: JsonValue }`, textually distinct from
5147 /// the semantically-equivalent `Record<string, JsonValue>`.
5148 #[test]
5149 fn print_type_renders_an_index_signature() {
5150 let ty = TsType::Object(vec![TsTypeMember::index(
5151 "k",
5152 TsType::named("string"),
5153 TsType::named("JsonValue"),
5154 )]);
5155 assert_eq!(print_type(&ty), "{ [k: string]: JsonValue }");
5156 }
5157
5158 /// #1323's own real gap, found implementing `workers_entry.rs`:
5159 /// `export { a, b } from "spec";` — a re-export, distinct from both
5160 /// `Import` and `Export`. Not classified alongside imports for the
5161 /// "no blank line between adjacent imports" exception.
5162 #[test]
5163 fn prints_a_re_export_with_the_ordinary_blank_line_rule() {
5164 let mut program = TsProgram::new();
5165 program.push(TsStmt::decl(
5166 TsDecl::ImportNamespace {
5167 type_only: false,
5168 alias: "handlers".to_string(),
5169 from: "./handlers.js".to_string(),
5170 },
5171 None,
5172 ));
5173 program.push(TsStmt::decl(
5174 TsDecl::ReExport {
5175 names: vec!["Orders".to_string()],
5176 from: "./handlers.js".to_string(),
5177 },
5178 None,
5179 ));
5180 let printed = print(&program, "x.bynk", "", "x.ts");
5181 assert_eq!(
5182 printed.text,
5183 "import * as handlers from \"./handlers.js\";\n\nexport { Orders } from \"./handlers.js\";\n"
5184 );
5185 }
5186
5187 /// #1323's own real gap, found implementing `workers_entry.rs`: a bare
5188 /// blank line usable at any nesting depth, distinct from `print()`'s
5189 /// own top-level-only policy.
5190 #[test]
5191 fn a_blank_statement_prints_one_empty_line_inside_a_nested_block() {
5192 let mut program = TsProgram::new();
5193 program.push(TsStmt::if_stmt(
5194 TsExpr::Ident("cond".to_string()),
5195 TsStmt::block(
5196 vec![
5197 TsStmt::expr_stmt(TsExpr::Ident("a".to_string()), None),
5198 TsStmt::blank(None),
5199 TsStmt::expr_stmt(TsExpr::Ident("b".to_string()), None),
5200 ],
5201 None,
5202 ),
5203 None,
5204 ));
5205 let printed = print(&program, "x.bynk", "", "x.ts");
5206 assert_eq!(printed.text, "if (cond) {\n a;\n\n b;\n}\n");
5207 }
5208
5209 /// #1323's own real gap, found implementing `workers_entry.rs`:
5210 /// `CorsPolicy.credentials`/`SecurityPolicy.nosniff` are real booleans.
5211 #[test]
5212 fn prints_a_boolean_literal() {
5213 let mut program = TsProgram::new();
5214 program.push(TsStmt::expr_stmt(
5215 TsExpr::object(vec![
5216 ("credentials".to_string(), TsExpr::Lit(TsLit::Bool(true))),
5217 ("nosniff".to_string(), TsExpr::Lit(TsLit::Bool(false))),
5218 ]),
5219 None,
5220 ));
5221 let printed = print(&program, "x.bynk", "", "x.ts");
5222 assert_eq!(printed.text, "{ credentials: true, nosniff: false };\n");
5223 }
5224
5225 /// #1323's own real gap: an explicit `Paren` always prints its own
5226 /// literal parens, even when the wrapped expression's own precedence
5227 /// would not otherwise need any — `workers_entry.rs`'s CORS-preflight
5228 /// guard wraps its path-match condition in `(...)` unconditionally,
5229 /// even for a single equality check with nothing lower-precedence than
5230 /// the outer `&&` inside it (which the ordinary precedence-derived
5231 /// rules correctly do NOT parenthesize on their own).
5232 #[test]
5233 fn an_explicit_paren_always_prints_regardless_of_the_inner_expressions_own_precedence() {
5234 let mut program = TsProgram::new();
5235 program.push(TsStmt::expr_stmt(
5236 TsExpr::Binary {
5237 op: TsBinaryOp::And,
5238 left: Box::new(TsExpr::Ident("method".to_string())),
5239 right: Box::new(TsExpr::Paren(Box::new(TsExpr::Binary {
5240 op: TsBinaryOp::StrictEq,
5241 left: Box::new(TsExpr::Ident("path".to_string())),
5242 right: Box::new(TsExpr::Lit(TsLit::Str("/foo".to_string()))),
5243 }))),
5244 },
5245 None,
5246 ));
5247 let printed = print(&program, "x.bynk", "", "x.ts");
5248 assert_eq!(printed.text, "method && (path === \"/foo\");\n");
5249 }
5250
5251 /// #1323's own real gap: a 3-term `||` chain of the *same* operator
5252 /// prints flat, matching `emit_call_handler_dispatch`'s own real
5253 /// `typeof args !== "object" || args === null || Array.isArray(args)`
5254 /// — the pre-#1323 "always parenthesize equal precedence" rule would
5255 /// have wrongly added parens around the first two terms.
5256 #[test]
5257 fn a_three_term_or_chain_of_the_same_operator_prints_flat() {
5258 let mut program = TsProgram::new();
5259 program.push(TsStmt::expr_stmt(
5260 TsExpr::Binary {
5261 op: TsBinaryOp::Or,
5262 left: Box::new(TsExpr::Binary {
5263 op: TsBinaryOp::Or,
5264 left: Box::new(TsExpr::Binary {
5265 op: TsBinaryOp::StrictNotEq,
5266 left: Box::new(TsExpr::Unary {
5267 op: TsUnaryOp::Typeof,
5268 expr: Box::new(TsExpr::Ident("args".to_string())),
5269 }),
5270 right: Box::new(TsExpr::Lit(TsLit::Str("object".to_string()))),
5271 }),
5272 right: Box::new(TsExpr::Binary {
5273 op: TsBinaryOp::StrictEq,
5274 left: Box::new(TsExpr::Ident("args".to_string())),
5275 right: Box::new(TsExpr::Lit(TsLit::Null)),
5276 }),
5277 }),
5278 right: Box::new(TsExpr::Call {
5279 callee: Box::new(TsExpr::Member {
5280 object: Box::new(TsExpr::Ident("Array".to_string())),
5281 property: "isArray".to_string(),
5282 }),
5283 args: vec![TsExpr::Ident("args".to_string())],
5284 }),
5285 },
5286 None,
5287 ));
5288 let printed = print(&program, "x.bynk", "", "x.ts");
5289 assert_eq!(
5290 printed.text,
5291 "typeof args !== \"object\" || args === null || Array.isArray(args);\n"
5292 );
5293 }
5294
5295 /// Arc C, step (11) (#1388): `TsBinaryOp::Add`'s own real, dominant real
5296 /// site — a message template's own literal/placeholder segments,
5297 /// left-folded via `.join(" + ")` — needs the identical flat-chain
5298 /// treatment `||`/`&&` already established, joining the same
5299 /// associativity exemption in `render_binary_operand`, not a
5300 /// bespoke third rule.
5301 #[test]
5302 fn a_three_term_add_chain_of_the_same_operator_prints_flat() {
5303 let mut program = TsProgram::new();
5304 program.push(TsStmt::expr_stmt(
5305 TsExpr::Binary {
5306 op: TsBinaryOp::Add,
5307 left: Box::new(TsExpr::Binary {
5308 op: TsBinaryOp::Add,
5309 left: Box::new(TsExpr::Lit(TsLit::Str("a".to_string()))),
5310 right: Box::new(TsExpr::Lit(TsLit::Str("b".to_string()))),
5311 }),
5312 right: Box::new(TsExpr::Lit(TsLit::Str("c".to_string()))),
5313 },
5314 None,
5315 ));
5316 let printed = print(&program, "x.bynk", "", "x.ts");
5317 assert_eq!(printed.text, "\"a\" + \"b\" + \"c\";\n");
5318 }
5319
5320 /// Review of #1389, finding 1: unlike `||`/`&&` (semantically
5321 /// associative — flattening either side is safe), `+` is only
5322 /// grammatically left-associative — `1 + (2 + "3")` and `(1 + 2) + "3"`
5323 /// disagree once a number joins the chain. A right-nested `Add` must
5324 /// keep its parens, matching every other non-`Or`/`And` same-operator
5325 /// chain's own treatment (`a_right_nested_strict_eq_chain_keeps_its_
5326 /// parens`/`a_right_nested_greater_than_chain_keeps_its_parens`).
5327 #[test]
5328 fn a_right_nested_add_chain_keeps_its_parens() {
5329 let mut program = TsProgram::new();
5330 program.push(TsStmt::expr_stmt(
5331 TsExpr::Binary {
5332 op: TsBinaryOp::Add,
5333 left: Box::new(TsExpr::Lit(TsLit::Num("1".to_string()))),
5334 right: Box::new(TsExpr::Binary {
5335 op: TsBinaryOp::Add,
5336 left: Box::new(TsExpr::Lit(TsLit::Num("2".to_string()))),
5337 right: Box::new(TsExpr::Lit(TsLit::Str("3".to_string()))),
5338 }),
5339 },
5340 None,
5341 ));
5342 let printed = print(&program, "x.bynk", "", "x.ts");
5343 assert_eq!(printed.text, "1 + (2 + \"3\");\n");
5344 }
5345
5346 /// `Add`'s own new `binary_precedence` entry (higher than
5347 /// `GreaterThan`, matching real JS/TS) — a nested `Add` under a
5348 /// `GreaterThan` needs no parens (`+` binds tighter), the reverse
5349 /// direction does.
5350 #[test]
5351 fn add_binds_tighter_than_greater_than_on_both_sides() {
5352 let mut program = TsProgram::new();
5353 program.push(TsStmt::expr_stmt(
5354 TsExpr::Binary {
5355 op: TsBinaryOp::GreaterThan,
5356 left: Box::new(TsExpr::Binary {
5357 op: TsBinaryOp::Add,
5358 left: Box::new(TsExpr::Ident("a".to_string())),
5359 right: Box::new(TsExpr::Ident("b".to_string())),
5360 }),
5361 right: Box::new(TsExpr::Ident("c".to_string())),
5362 },
5363 None,
5364 ));
5365 let printed = print(&program, "x.bynk", "", "x.ts");
5366 assert_eq!(printed.text, "a + b > c;\n");
5367 }
5368
5369 /// The reverse nesting: a `GreaterThan` operand under an outer `Add`
5370 /// needs its parens (`+` binds tighter, so `Add`'s own precedence check
5371 /// against a lower-precedence `GreaterThan` operand must still fire).
5372 #[test]
5373 fn greater_than_nested_under_add_keeps_its_parens() {
5374 let mut program = TsProgram::new();
5375 program.push(TsStmt::expr_stmt(
5376 TsExpr::Binary {
5377 op: TsBinaryOp::Add,
5378 left: Box::new(TsExpr::Ident("a".to_string())),
5379 right: Box::new(TsExpr::Binary {
5380 op: TsBinaryOp::GreaterThan,
5381 left: Box::new(TsExpr::Ident("b".to_string())),
5382 right: Box::new(TsExpr::Ident("c".to_string())),
5383 }),
5384 },
5385 None,
5386 ));
5387 let printed = print(&program, "x.bynk", "", "x.ts");
5388 assert_eq!(printed.text, "a + (b > c);\n");
5389 }
5390
5391 /// Review of #1324, finding 1: the same-operator flattening #1323 added
5392 /// for `||`/`&&` was wrongly applied to every operator, including
5393 /// non-associative ones — a right-nested `StrictEq` inside `StrictEq`
5394 /// must keep its parens (`a === b === c` parses as `(a === b) === c`,
5395 /// not the tree's real `a === (b === c)`).
5396 #[test]
5397 fn a_right_nested_strict_eq_chain_keeps_its_parens() {
5398 let mut program = TsProgram::new();
5399 program.push(TsStmt::expr_stmt(
5400 TsExpr::Binary {
5401 op: TsBinaryOp::StrictEq,
5402 left: Box::new(TsExpr::Ident("a".to_string())),
5403 right: Box::new(TsExpr::Binary {
5404 op: TsBinaryOp::StrictEq,
5405 left: Box::new(TsExpr::Ident("b".to_string())),
5406 right: Box::new(TsExpr::Ident("c".to_string())),
5407 }),
5408 },
5409 None,
5410 ));
5411 let printed = print(&program, "x.bynk", "", "x.ts");
5412 assert_eq!(printed.text, "a === (b === c);\n");
5413 }
5414
5415 /// Same finding, for the new `GreaterThan` operator: `a > b > c` parses
5416 /// as `(a > b) > c` (a boolean compared against `c`), not the tree's
5417 /// real `a > (b > c)` — a right-nested chain must keep its parens.
5418 #[test]
5419 fn a_right_nested_greater_than_chain_keeps_its_parens() {
5420 let mut program = TsProgram::new();
5421 program.push(TsStmt::expr_stmt(
5422 TsExpr::Binary {
5423 op: TsBinaryOp::GreaterThan,
5424 left: Box::new(TsExpr::Ident("a".to_string())),
5425 right: Box::new(TsExpr::Binary {
5426 op: TsBinaryOp::GreaterThan,
5427 left: Box::new(TsExpr::Ident("b".to_string())),
5428 right: Box::new(TsExpr::Ident("c".to_string())),
5429 }),
5430 },
5431 None,
5432 ));
5433 let printed = print(&program, "x.bynk", "", "x.ts");
5434 assert_eq!(printed.text, "a > (b > c);\n");
5435 }
5436
5437 /// Review of #1324, finding 2: a `Blank` used as an `if`'s brace-free
5438 /// body used to render as a bare newline, silently letting the very
5439 /// next statement in the enclosing block become the `if`'s own body —
5440 /// now it prints an honest empty statement (`;`) that can't swallow
5441 /// anything after it.
5442 #[test]
5443 fn a_blank_if_body_prints_an_empty_statement_not_a_swallowing_newline() {
5444 // Nested inside a `Block`, not two top-level program statements —
5445 // `print`'s own blank-line-between-statements policy only applies
5446 // between top-level statements (see its own doc comment), so a
5447 // top-level pair would mask the swallowing bug this test exists to
5448 // catch: the real hazard is two statements sharing one physical
5449 // line with nothing separating them, exactly what `render_stmt`'s
5450 // `Block` arm produces with no blank-line insertion of its own.
5451 let mut program = TsProgram::new();
5452 program.push(TsStmt::block(
5453 vec![
5454 TsStmt::if_stmt(TsExpr::Ident("cond".to_string()), TsStmt::blank(None), None),
5455 TsStmt::expr_stmt(
5456 TsExpr::Call {
5457 callee: Box::new(TsExpr::Ident("nextStatement".to_string())),
5458 args: vec![],
5459 },
5460 None,
5461 ),
5462 ],
5463 None,
5464 ));
5465 let printed = print(&program, "x.bynk", "", "x.ts");
5466 assert_eq!(printed.text, "{\n if (cond) ;\n nextStatement();\n}\n");
5467 }
5468
5469 /// A left-nested same-operator chain also keeps its parens for a
5470 /// non-associative operator (unlike the `||`/`&&` case above) —
5471 /// `(a === b) === c` reads the same as `a === b === c` would parse, so
5472 /// this is really about `render_binary_operand` not silently dropping
5473 /// parens it should keep, not about a left/right asymmetry.
5474 #[test]
5475 fn a_left_nested_strict_eq_chain_keeps_its_parens() {
5476 let mut program = TsProgram::new();
5477 program.push(TsStmt::expr_stmt(
5478 TsExpr::Binary {
5479 op: TsBinaryOp::StrictEq,
5480 left: Box::new(TsExpr::Binary {
5481 op: TsBinaryOp::StrictEq,
5482 left: Box::new(TsExpr::Ident("a".to_string())),
5483 right: Box::new(TsExpr::Ident("b".to_string())),
5484 }),
5485 right: Box::new(TsExpr::Ident("c".to_string())),
5486 },
5487 None,
5488 ));
5489 let printed = print(&program, "x.bynk", "", "x.ts");
5490 assert_eq!(printed.text, "(a === b) === c;\n");
5491 }
5492
5493 // -- #1325: emit_test_main's own new shapes ------------------------------
5494
5495 #[test]
5496 fn prints_a_template_literal_with_substitutions() {
5497 let mut program = TsProgram::new();
5498 program.push(TsStmt::expr_stmt(
5499 TsExpr::template_lit(
5500 vec![
5501 String::new(),
5502 " passed, ".to_string(),
5503 " failed.".to_string(),
5504 ],
5505 vec![
5506 TsExpr::Ident("passed".to_string()),
5507 TsExpr::Ident("failed".to_string()),
5508 ],
5509 ),
5510 None,
5511 ));
5512 let printed = print(&program, "x.bynk", "", "x.ts");
5513 assert_eq!(printed.text, "`${passed} passed, ${failed} failed.`;\n");
5514 }
5515
5516 /// A template literal's own static parts print with no escaping applied
5517 /// by the printer — see `TsExpr::TemplateLit`'s own doc for why: a
5518 /// generic escaper would double the literal backslash of an
5519 /// already-pre-formed JS unicode escape (`✓`), corrupting it. This
5520 /// pins the real, grounded shape directly: the six ASCII characters
5521 /// `✓` pass through unchanged, not doubled into `\\u2713`.
5522 #[test]
5523 fn a_template_literal_part_carrying_a_preformed_unicode_escape_is_not_reescaped() {
5524 let mut program = TsProgram::new();
5525 program.push(TsStmt::expr_stmt(
5526 TsExpr::template_lit(
5527 vec![" \\u2713 ".to_string(), String::new()],
5528 vec![TsExpr::Ident("r".to_string())],
5529 ),
5530 None,
5531 ));
5532 let printed = print(&program, "x.bynk", "", "x.ts");
5533 assert_eq!(printed.text, "` \\u2713 ${r}`;\n");
5534 }
5535
5536 #[test]
5537 fn a_raw_literal_prints_exactly_as_given_with_no_escaping() {
5538 let mut program = TsProgram::new();
5539 program.push(TsStmt::expr_stmt(
5540 TsExpr::Lit(TsLit::Raw("\"integration \\u00b7 \"".to_string())),
5541 None,
5542 ));
5543 let printed = print(&program, "x.bynk", "", "x.ts");
5544 assert_eq!(printed.text, "\"integration \\u00b7 \";\n");
5545 }
5546
5547 #[test]
5548 fn prints_a_multiline_array_literal() {
5549 let mut program = TsProgram::new();
5550 program.push(TsStmt::const_stmt(
5551 TsBindingName::Ident("modules".to_string()),
5552 None,
5553 TsExpr::multiline_array(vec![
5554 TsExpr::object(vec![(
5555 "name".to_string(),
5556 TsExpr::Lit(TsLit::Str("a".to_string())),
5557 )]),
5558 TsExpr::object(vec![(
5559 "name".to_string(),
5560 TsExpr::Lit(TsLit::Str("b".to_string())),
5561 )]),
5562 ]),
5563 None,
5564 ));
5565 let printed = print(&program, "x.bynk", "", "x.ts");
5566 assert_eq!(
5567 printed.text,
5568 "const modules = [\n { name: \"a\" },\n { name: \"b\" },\n];\n"
5569 );
5570 }
5571
5572 /// Same reachability boundary as `TsExpr::Object`'s own `multiline`
5573 /// field (see its doc) — a `multiline: true` array nested inside another
5574 /// expression falls back to single-line via the depth-unaware
5575 /// `render_expr` recursion.
5576 #[test]
5577 fn a_nested_multiline_array_falls_back_to_single_line() {
5578 let mut program = TsProgram::new();
5579 program.push(TsStmt::expr_stmt(
5580 TsExpr::array(vec![TsExpr::multiline_array(vec![TsExpr::Lit(
5581 TsLit::Num("1".to_string()),
5582 )])]),
5583 None,
5584 ));
5585 let printed = print(&program, "x.bynk", "", "x.ts");
5586 assert_eq!(printed.text, "[[1]];\n");
5587 }
5588
5589 #[test]
5590 fn prints_a_declare_const_ambient_binding() {
5591 let mut program = TsProgram::new();
5592 program.push(TsStmt::decl(
5593 TsDecl::DeclareConst {
5594 name: "process".to_string(),
5595 ty: TsType::Object(vec![TsTypeMember::prop("env", TsType::named("unknown"))]),
5596 },
5597 None,
5598 ));
5599 let printed = print(&program, "x.bynk", "", "x.ts");
5600 assert_eq!(printed.text, "declare const process: { env: unknown };\n");
5601 }
5602
5603 #[test]
5604 fn prints_an_async_top_level_function() {
5605 let mut program = TsProgram::new();
5606 program.push(TsStmt::decl(
5607 TsDecl::Function {
5608 name: "main".to_string(),
5609 generics: Vec::new(),
5610 params: vec![],
5611 return_type: None,
5612 body: vec![TsStmt::return_stmt(None, None)],
5613 is_async: true,
5614 inline: false,
5615 },
5616 None,
5617 ));
5618 let printed = print(&program, "x.bynk", "", "x.ts");
5619 assert_eq!(printed.text, "async function main() {\n return;\n}\n");
5620 }
5621
5622 /// #1369 (Arc C, slice 20): `TsDecl::Function.inline` — the zero-factory
5623 /// shape (`function name(): Ret { return <expr>; }` all on one line)
5624 /// `emit_agent` needs, mirroring `TsObjectEntry::Method.inline`'s own
5625 /// single-line-vs-multi-line precedent (#1337) at a different node kind.
5626 #[test]
5627 fn prints_an_inline_top_level_function() {
5628 let mut program = TsProgram::new();
5629 program.push(TsStmt::decl(
5630 TsDecl::Function {
5631 name: "zero".to_string(),
5632 generics: Vec::new(),
5633 params: vec![],
5634 return_type: Some(TsType::named("Foo")),
5635 body: vec![TsStmt::return_stmt(
5636 Some(TsExpr::object(vec![(
5637 "n".to_string(),
5638 TsExpr::Lit(TsLit::Num("0".to_string())),
5639 )])),
5640 None,
5641 )],
5642 is_async: false,
5643 inline: true,
5644 },
5645 None,
5646 ));
5647 let printed = print(&program, "x.bynk", "", "x.ts");
5648 assert_eq!(printed.text, "function zero(): Foo { return { n: 0 }; }\n");
5649 }
5650
5651 #[test]
5652 fn prints_a_postfix_increment_statement() {
5653 let mut program = TsProgram::new();
5654 program.push(TsStmt::increment(TsExpr::Ident("passed".to_string()), None));
5655 let printed = print(&program, "x.bynk", "", "x.ts");
5656 assert_eq!(printed.text, "passed++;\n");
5657 }
5658
5659 /// An `Increment` inside an `InlineBlock` renders correctly through
5660 /// `render_inline_stmt`'s fallback — `emit_test_main`'s own real
5661 /// `{ passed++; console.log(...); }` shape.
5662 #[test]
5663 fn a_postfix_increment_renders_correctly_inside_an_inline_block() {
5664 let mut program = TsProgram::new();
5665 program.push(TsStmt::if_stmt(
5666 TsExpr::Ident("cond".to_string()),
5667 TsStmt::inline_block(
5668 vec![
5669 TsStmt::increment(TsExpr::Ident("passed".to_string()), None),
5670 TsStmt::expr_stmt(TsExpr::Ident("next".to_string()), None),
5671 ],
5672 None,
5673 ),
5674 None,
5675 ));
5676 let printed = print(&program, "x.bynk", "", "x.ts");
5677 assert_eq!(printed.text, "if (cond) { passed++; next; }\n");
5678 }
5679
5680 /// `same_line_else` with a `Block` then-branch — `} else {` on one
5681 /// physical line, distinct from the fresh-line default pinned elsewhere
5682 /// in this module.
5683 #[test]
5684 fn same_line_else_puts_the_else_keyword_after_the_closing_brace() {
5685 let mut program = TsProgram::new();
5686 program.push(TsStmt::if_else_same_line_stmt(
5687 TsExpr::Ident("cond".to_string()),
5688 TsStmt::block(
5689 vec![TsStmt::expr_stmt(TsExpr::Ident("a".to_string()), None)],
5690 None,
5691 ),
5692 TsStmt::block(
5693 vec![TsStmt::expr_stmt(TsExpr::Ident("b".to_string()), None)],
5694 None,
5695 ),
5696 None,
5697 ));
5698 let printed = print(&program, "x.bynk", "", "x.ts");
5699 assert_eq!(printed.text, "if (cond) {\n a;\n} else {\n b;\n}\n");
5700 }
5701
5702 /// `same_line_else` with `InlineBlock` branches on both sides —
5703 /// `emit_test_main`'s own real `if (r.pass) { ... } else { ... }` shape,
5704 /// entirely on one generated line.
5705 #[test]
5706 fn same_line_else_with_inline_block_branches_stays_on_one_line() {
5707 let mut program = TsProgram::new();
5708 program.push(TsStmt::if_else_same_line_stmt(
5709 TsExpr::Ident("cond".to_string()),
5710 TsStmt::inline_block(
5711 vec![TsStmt::expr_stmt(TsExpr::Ident("a".to_string()), None)],
5712 None,
5713 ),
5714 TsStmt::inline_block(
5715 vec![TsStmt::expr_stmt(TsExpr::Ident("b".to_string()), None)],
5716 None,
5717 ),
5718 None,
5719 ));
5720 let printed = print(&program, "x.bynk", "", "x.ts");
5721 assert_eq!(printed.text, "if (cond) { a; } else { b; }\n");
5722 }
5723
5724 /// `same_line_else` with a brace-free `then_branch` falls back to the
5725 /// ordinary fresh-line rendering — nothing real needs `<inline-stmt>
5726 /// else {`, and the printer doesn't claim to support it (see
5727 /// `TsStmtKind::If`'s own doc).
5728 #[test]
5729 fn same_line_else_with_a_brace_free_then_branch_falls_back_to_fresh_line() {
5730 let mut program = TsProgram::new();
5731 program.push(TsStmt::if_else_same_line_stmt(
5732 TsExpr::Ident("cond".to_string()),
5733 TsStmt::continue_stmt(None),
5734 TsStmt::block(
5735 vec![TsStmt::expr_stmt(TsExpr::Ident("b".to_string()), None)],
5736 None,
5737 ),
5738 None,
5739 ));
5740 let printed = print(&program, "x.bynk", "", "x.ts");
5741 assert_eq!(printed.text, "if (cond) continue;\nelse {\n b;\n}\n");
5742 }
5743
5744 /// `TsDecl::ReExportAll` prints `export * from "spec";` — no braces, no
5745 /// name list, matching `emit_commons_barrel`'s own real per-file line.
5746 #[test]
5747 fn re_export_all_prints_a_wildcard_re_export() {
5748 let mut program = TsProgram::new();
5749 program.push(TsStmt::decl(
5750 TsDecl::ReExportAll {
5751 from: "./thing/make.js".to_string(),
5752 },
5753 None,
5754 ));
5755 let printed = print(&program, "x.bynk", "", "x.ts");
5756 assert_eq!(printed.text, "export * from \"./thing/make.js\";\n");
5757 }
5758
5759 /// Review of #1329's own grounding: `emit_commons_barrel`'s real barrel
5760 /// module is one header `Comment` immediately followed by one `export
5761 /// *` line per constituent source file, every one of those lines
5762 /// adjacent with no blank line anywhere — the exact byte shape pinned
5763 /// here, matching `251_multi_file_commons_test`'s own real
5764 /// `expected/thing.ts`.
5765 #[test]
5766 fn a_header_comment_and_consecutive_re_export_alls_have_no_blank_lines() {
5767 let mut program = TsProgram::new();
5768 program.push(TsStmt::comment(
5769 "Generated by bynkc — do not edit by hand.",
5770 None,
5771 ));
5772 program.push(TsStmt::decl(
5773 TsDecl::ReExportAll {
5774 from: "./thing/make.js".to_string(),
5775 },
5776 None,
5777 ));
5778 program.push(TsStmt::decl(
5779 TsDecl::ReExportAll {
5780 from: "./thing/widget.js".to_string(),
5781 },
5782 None,
5783 ));
5784 let printed = print(&program, "x.bynk", "", "x.ts");
5785 assert_eq!(
5786 printed.text,
5787 "// Generated by bynkc — do not edit by hand.\n\
5788 export * from \"./thing/make.js\";\n\
5789 export * from \"./thing/widget.js\";\n"
5790 );
5791 }
5792
5793 /// Review of #1330: the grouping rule added for #1329 is scoped to
5794 /// `ReExportAll`-adjacent-to-`ReExportAll` (and a `Comment` immediately
5795 /// before one) — a `ReExportAll` next to anything else still gets the
5796 /// ordinary blank line. `emit_commons_barrel` is `ReExportAll`'s only
5797 /// producer today, so this is the invariant keeping the rule scoped if
5798 /// a future slice ever gives it a second one.
5799 #[test]
5800 fn a_re_export_all_next_to_a_non_re_export_all_gets_the_ordinary_blank_line() {
5801 let mut program = TsProgram::new();
5802 program.push(TsStmt::decl(
5803 TsDecl::ImportNamespace {
5804 type_only: false,
5805 alias: "handlers".to_string(),
5806 from: "./handlers.js".to_string(),
5807 },
5808 None,
5809 ));
5810 program.push(TsStmt::decl(
5811 TsDecl::ReExportAll {
5812 from: "./thing/make.js".to_string(),
5813 },
5814 None,
5815 ));
5816 program.push(TsStmt::comment("trailing note", None));
5817 let printed = print(&program, "x.bynk", "", "x.ts");
5818 assert_eq!(
5819 printed.text,
5820 "import * as handlers from \"./handlers.js\";\n\n\
5821 export * from \"./thing/make.js\";\n\n\
5822 // trailing note\n"
5823 );
5824 }
5825
5826 /// #1333: `print_stmt` prints a `TsStmtKind::DocComment` as a real JSDoc
5827 /// block, matching `emit_doc_block`'s own real multi-line shape
5828 /// (`137_agent_instantiation_workers/expected/workers/demo-counter/
5829 /// handlers.ts`'s own real header comment).
5830 #[test]
5831 fn print_stmt_renders_a_multi_line_doc_comment() {
5832 let stmt = TsStmt::doc_comment(
5833 "A minimal stateful agent in the bundle target: instantiation lowers through the\ngenerated factory, the method call is a direct call, and state persists per key\nacross calls within a session.",
5834 None,
5835 );
5836 assert_eq!(
5837 print_stmt(&stmt, 0),
5838 "/**\n \
5839 * A minimal stateful agent in the bundle target: instantiation lowers through the\n \
5840 * generated factory, the method call is a direct call, and state persists per key\n \
5841 * across calls within a session.\n \
5842 */\n"
5843 );
5844 }
5845
5846 /// #1333: a literal `*/` inside doc text escapes to `*\/`, matching
5847 /// `emit_doc_block`'s own pre-conversion behaviour exactly (issue
5848 /// #720 — an unescaped `*/` would otherwise close the comment early
5849 /// and let trailing text land as executable top-level TypeScript).
5850 #[test]
5851 fn print_stmt_escapes_a_literal_comment_terminator_inside_doc_text() {
5852 let stmt = TsStmt::doc_comment("docs */ ; (globalThis as any).PWNED = true; /*", None);
5853 let printed = print_stmt(&stmt, 0);
5854 assert!(!printed.contains("*/ ;"), "unescaped terminator: {printed}");
5855 assert_eq!(
5856 printed,
5857 "/**\n * docs *\\/ ; (globalThis as any).PWNED = true; /*\n */\n"
5858 );
5859 }
5860
5861 /// #1333: a blank line inside doc text prints as a bare ` *`, no
5862 /// trailing space — distinct from a non-blank line's own ` * <line>`.
5863 #[test]
5864 fn print_stmt_renders_a_blank_doc_line_as_a_bare_star() {
5865 let stmt = TsStmt::doc_comment("first paragraph\n\nsecond paragraph", None);
5866 assert_eq!(
5867 print_stmt(&stmt, 0),
5868 "/**\n * first paragraph\n *\n * second paragraph\n */\n"
5869 );
5870 }
5871
5872 /// #1333: `print_stmt`'s own `depth` parameter indents every line of
5873 /// the JSDoc block, matching `render_stmt`'s own 2-space-per-level
5874 /// convention — `emit_doc_block`'s real callers pass `INDENT_STEP`
5875 /// (2 raw spaces = depth 1) for a nested doc comment.
5876 #[test]
5877 fn print_stmt_indents_a_doc_comment_at_depth() {
5878 let stmt = TsStmt::doc_comment("a method", None);
5879 assert_eq!(print_stmt(&stmt, 1), " /**\n * a method\n */\n");
5880 }
5881
5882 /// #1337: `TsStmtKind::Raw` prints its own text exactly as given — no
5883 /// leading indent (unlike every other statement kind, whose own
5884 /// `render_stmt` arm prefixes `indent(depth)`), no added semicolon or
5885 /// braces. `emit_method`'s own opaque `lower.rs`-sourced body is
5886 /// already fully, absolutely indented by the time it's captured into
5887 /// one `Raw` node, so the printer must contribute nothing further —
5888 /// the same reasoning `Verbatim` already established for pre-rendered
5889 /// content, `Raw`'s own doc explains why it's a distinct kind.
5890 #[test]
5891 fn print_stmt_renders_raw_text_verbatim_with_no_added_indent_or_punctuation() {
5892 let stmt = TsStmt::raw(" return x + 1;\n", None);
5893 // depth is passed but deliberately has no effect on Raw's own output.
5894 assert_eq!(print_stmt(&stmt, 3), " return x + 1;\n");
5895 }
5896
5897 /// #1337: multi-line `Raw` text (the real shape — a whole function
5898 /// body, not one line) passes through with every embedded line intact,
5899 /// confirming the printer doesn't split, re-indent, or otherwise
5900 /// interpret it.
5901 #[test]
5902 fn print_stmt_renders_multi_line_raw_text_unchanged() {
5903 let stmt = TsStmt::raw(
5904 " const r = Uuid.of(crypto.randomUUID());\n return r.value;\n",
5905 None,
5906 );
5907 assert_eq!(
5908 print_stmt(&stmt, 0),
5909 " const r = Uuid.of(crypto.randomUUID());\n return r.value;\n"
5910 );
5911 }
5912
5913 /// #1337: `TsObjectEntry::Method` with a `Raw` body — `emit_method`'s
5914 /// own real shape (`{method}{generics}({params}): {ret} { <raw body>
5915 /// },`), pinned at the actual depth its own object literal renders at
5916 /// (a top-level `export const {...}`, depth 0 → entries at depth 1,
5917 /// two-space indent, matching `emit_method`'s own pre-conversion
5918 /// hand-written `" {method}..."` line).
5919 #[test]
5920 fn multiline_object_renders_a_method_entry_with_a_raw_body() {
5921 let mut out = String::new();
5922 render_multiline_object(
5923 &mut out,
5924 &[TsObjectEntry::Method {
5925 name: "of".to_string(),
5926 is_async: false,
5927 generics: Vec::new(),
5928 params: vec![TsParam {
5929 name: "value".to_string(),
5930 ty: Some(TsType::named("string")),
5931 optional: false,
5932 }],
5933 return_type: Some(TsType::named("Uuid")),
5934 doc: None,
5935 inline: false,
5936 body: vec![TsStmt::raw(" return value as Uuid;\n", None)],
5937 }],
5938 0,
5939 );
5940 assert_eq!(
5941 out,
5942 "{\n of(value: string): Uuid {\n return value as Uuid;\n },\n}"
5943 );
5944 }
5945
5946 /// #1337: `TsObjectEntry::Method`'s own `generics` field prints
5947 /// `<A, U>` between the method name and its parameter list —
5948 /// `Box.map`'s own real shape (`402_generic_instance_method`, a
5949 /// single-file-form fixture the accepted proposal's own project-form
5950 /// search missed).
5951 #[test]
5952 fn multiline_object_renders_a_generic_method_entry() {
5953 let mut out = String::new();
5954 render_multiline_object(
5955 &mut out,
5956 &[TsObjectEntry::Method {
5957 name: "map".to_string(),
5958 is_async: false,
5959 generics: vec!["A".to_string(), "U".to_string()],
5960 params: vec![
5961 TsParam {
5962 name: "self".to_string(),
5963 ty: Some(TsType::named("Box<A>")),
5964 optional: false,
5965 },
5966 TsParam {
5967 name: "f".to_string(),
5968 ty: Some(TsType::named("(a0: A) => U")),
5969 optional: false,
5970 },
5971 ],
5972 return_type: Some(TsType::named("Box<U>")),
5973 doc: None,
5974 inline: false,
5975 body: vec![TsStmt::return_stmt(
5976 Some(TsExpr::Ident("{ value: f(self.value) }".to_string())),
5977 None,
5978 )],
5979 }],
5980 0,
5981 );
5982 assert_eq!(
5983 out,
5984 "{\n map<A, U>(self: Box<A>, f: (a0: A) => U): Box<U> {\n return { value: f(self.value) };\n },\n}"
5985 );
5986 }
5987
5988 /// #1337: a method with no generics prints no `<>` at all — the
5989 /// ordinary, dominant case (every prior slice's own real method
5990 /// entries), confirming `generics: Vec::new()` is a true no-op, not
5991 /// an empty `<>`.
5992 #[test]
5993 fn multiline_object_renders_a_non_generic_method_entry_with_no_angle_brackets() {
5994 let mut out = String::new();
5995 render_multiline_object(
5996 &mut out,
5997 &[TsObjectEntry::Method {
5998 name: "get".to_string(),
5999 is_async: false,
6000 generics: Vec::new(),
6001 params: vec![],
6002 return_type: Some(TsType::named("void")),
6003 doc: None,
6004 inline: false,
6005 body: vec![],
6006 }],
6007 0,
6008 );
6009 assert_eq!(out, "{\n get(): void {\n },\n}");
6010 }
6011
6012 /// #1337: `TsObjectEntry::Method`'s own `doc` field prints a JSDoc
6013 /// block immediately before the method entry, same indent, no blank
6014 /// line between — `Timestamp.diff`'s own real shape
6015 /// (`65_money_uses_time`), reusing `render_doc_comment` (the same
6016 /// renderer `TsStmtKind::DocComment` already uses) rather than a
6017 /// second copy.
6018 #[test]
6019 fn multiline_object_renders_a_method_entry_with_a_preceding_doc_comment() {
6020 let mut out = String::new();
6021 render_multiline_object(
6022 &mut out,
6023 &[TsObjectEntry::Method {
6024 name: "diff".to_string(),
6025 is_async: false,
6026 generics: Vec::new(),
6027 params: vec![TsParam {
6028 name: "self".to_string(),
6029 ty: Some(TsType::named("Timestamp")),
6030 optional: false,
6031 }],
6032 return_type: Some(TsType::named("Span")),
6033 doc: Some("Compute the duration between two timestamps.".to_string()),
6034 inline: false,
6035 body: vec![TsStmt::raw(" return 0;\n", None)],
6036 }],
6037 0,
6038 );
6039 assert_eq!(
6040 out,
6041 "{\n /**\n * Compute the duration between two timestamps.\n */\n diff(self: Timestamp): Span {\n return 0;\n },\n}"
6042 );
6043 }
6044
6045 /// #1337: `TsObjectEntry::Method`'s own `inline: true` renders the
6046 /// whole entry — signature and one-statement body alike — on ONE
6047 /// physical line, `emit_forwarded_methods`'s own real shape
6048 /// (`255_context_uses_commons_static_method`'s own real `equals`
6049 /// entry: `equals(self: Cents, other: Cents): boolean { return
6050 /// __CommonsCents.equals(self, other) as unknown as boolean; },`),
6051 /// distinct from every other real `Method` entry in this tree
6052 /// (always multi-line, `inline: false`).
6053 #[test]
6054 fn multiline_object_renders_an_inline_method_entry_on_one_line() {
6055 let mut out = String::new();
6056 render_multiline_object(
6057 &mut out,
6058 &[TsObjectEntry::Method {
6059 name: "equals".to_string(),
6060 is_async: false,
6061 generics: Vec::new(),
6062 params: vec![
6063 TsParam {
6064 name: "self".to_string(),
6065 ty: Some(TsType::named("Cents")),
6066 optional: false,
6067 },
6068 TsParam {
6069 name: "other".to_string(),
6070 ty: Some(TsType::named("Cents")),
6071 optional: false,
6072 },
6073 ],
6074 return_type: Some(TsType::named("boolean")),
6075 doc: None,
6076 inline: true,
6077 body: vec![TsStmt::return_stmt(
6078 Some(TsExpr::As {
6079 expr: Box::new(TsExpr::As {
6080 expr: Box::new(TsExpr::Call {
6081 callee: Box::new(TsExpr::Member {
6082 object: Box::new(TsExpr::Ident("__CommonsCents".to_string())),
6083 property: "equals".to_string(),
6084 }),
6085 args: vec![
6086 TsExpr::Ident("self".to_string()),
6087 TsExpr::Ident("other".to_string()),
6088 ],
6089 }),
6090 ty: TsType::named("unknown"),
6091 }),
6092 ty: TsType::named("boolean"),
6093 }),
6094 None,
6095 )],
6096 }],
6097 0,
6098 );
6099 assert_eq!(
6100 out,
6101 "{\n equals(self: Cents, other: Cents): boolean { return __CommonsCents.equals(self, other) as unknown as boolean; },\n}"
6102 );
6103 }
6104
6105 /// Review of #1338, finding 4: `print_object_entry` — the exact public
6106 /// API `emit_refined_type`/`emit_record_type`/`emit_sum_type`'s own
6107 /// real call sites depend on — had no direct test; every #1337 test
6108 /// above drives `render_multiline_object` instead. Pins the contract
6109 /// those call sites rely on: `print_object_entry(&entry, 0)` produces
6110 /// the same text as the entry-slice portion of what
6111 /// `render_multiline_object`'s own depth-0 output produces for the
6112 /// same entry (its own depth convention — the entry lands one level
6113 /// deeper than the object, i.e. `depth + 1` — matches exactly, not
6114 /// coincidentally, since both paths route through the same
6115 /// `render_multiline_object_entry`).
6116 #[test]
6117 fn print_object_entry_matches_the_multiline_objects_own_entry_slice() {
6118 // A real `TsStmt::return_stmt` body, not `Raw` — `Raw`'s own baked-in
6119 // indent only matches depth 0 (finding 3's own new guard), so an
6120 // ordinary real-node body is what this depth-convention contract
6121 // needs to prove at a non-zero depth.
6122 let entry = TsObjectEntry::Method {
6123 name: "of".to_string(),
6124 is_async: false,
6125 generics: Vec::new(),
6126 params: vec![TsParam {
6127 name: "value".to_string(),
6128 ty: Some(TsType::named("string")),
6129 optional: false,
6130 }],
6131 return_type: Some(TsType::named("Uuid")),
6132 doc: None,
6133 inline: false,
6134 body: vec![TsStmt::return_stmt(
6135 Some(TsExpr::As {
6136 expr: Box::new(TsExpr::Ident("value".to_string())),
6137 ty: TsType::named("Uuid"),
6138 }),
6139 None,
6140 )],
6141 };
6142
6143 let mut whole_object = String::new();
6144 render_multiline_object(&mut whole_object, std::slice::from_ref(&entry), 0);
6145 // `render_multiline_object`'s own entry line is followed by `\n`
6146 // then the closing `}` (no blank line between them for a single
6147 // entry) — `print_object_entry`'s own output keeps that same
6148 // trailing `\n` (it has no closing brace of its own to attach to),
6149 // so the real equivalence is entry-slice-plus-newline, not a bare
6150 // slice.
6151 let entry_slice_with_trailing_newline = whole_object
6152 .strip_prefix("{\n")
6153 .and_then(|s| s.strip_suffix('}'))
6154 .expect("render_multiline_object's own single-entry output has a { }-wrapper");
6155
6156 assert_eq!(
6157 print_object_entry(&entry, 0),
6158 entry_slice_with_trailing_newline
6159 );
6160 }
6161
6162 /// #1339's own real gap: `emit_refined_type`'s own branded-type alias,
6163 /// `{base} & { readonly __brand: "..." }`, has no representation among
6164 /// `Named`/`Array`/`Object`/`Fn`/`Union` — mirrors `Union`'s own single-
6165 /// line, ` & `-joined shape exactly.
6166 #[test]
6167 fn print_type_renders_an_intersection() {
6168 let ty = TsType::intersection(vec![
6169 TsType::named("string"),
6170 TsType::Object(vec![TsTypeMember::readonly_prop(
6171 "__brand",
6172 TsType::named("\"Order\""),
6173 )]),
6174 ]);
6175 assert_eq!(print_type(&ty), "string & { readonly __brand: \"Order\" }");
6176 }
6177
6178 /// #1339's own real gap: `emit_sum_type`'s own multi-line discriminated
6179 /// union — a leading `|` on every line except the first (which gets
6180 /// equivalent spacing instead, matching the pre-conversion `writeln!`
6181 /// code's own `let pipe = if i == 0 { " " } else { "|" };` exactly), no
6182 /// trailing newline or `;` of its own (the caller, `TsDecl::TypeAlias`'s
6183 /// own render arm, owns both).
6184 #[test]
6185 fn print_type_renders_a_multiline_union() {
6186 let ty = TsType::multiline_union(vec![
6187 TsType::Object(vec![TsTypeMember::readonly_prop(
6188 "tag",
6189 TsType::named("\"a\""),
6190 )]),
6191 TsType::Object(vec![
6192 TsTypeMember::readonly_prop("tag", TsType::named("\"b\"")),
6193 TsTypeMember::readonly_prop("value", TsType::named("number")),
6194 ]),
6195 ]);
6196 assert_eq!(
6197 print_type(&ty),
6198 " { readonly tag: \"a\" }\n | { readonly tag: \"b\"; readonly value: number }"
6199 );
6200 }
6201
6202 /// #1339: `TsDecl::TypeAlias`'s own multiline-union special case — the
6203 /// `=` is followed directly by `\n` (no trailing space, matching the
6204 /// pre-conversion `writeln!(out, "export type {name}{params} =")`
6205 /// line's own exact bytes), each variant on its own line, the closing
6206 /// `;` appended directly to the last variant's own line. Also pins
6207 /// `type_params`' own bare-generics rendering on the alias header.
6208 #[test]
6209 fn prints_a_generic_sum_types_own_multiline_type_alias() {
6210 let mut program = TsProgram::new();
6211 program.push(TsStmt::decl(
6212 TsDecl::Export(Box::new(TsDecl::TypeAlias {
6213 name: "Opt".to_string(),
6214 type_params: vec!["T".to_string()],
6215 ty: TsType::multiline_union(vec![
6216 TsType::Object(vec![TsTypeMember::readonly_prop(
6217 "tag",
6218 TsType::named("\"none\""),
6219 )]),
6220 TsType::Object(vec![
6221 TsTypeMember::readonly_prop("tag", TsType::named("\"some\"")),
6222 TsTypeMember::readonly_prop("value", TsType::named("T")),
6223 ]),
6224 ]),
6225 })),
6226 None,
6227 ));
6228 let printed = print(&program, "x.bynk", "", "x.ts");
6229 assert_eq!(
6230 printed.text,
6231 "export type Opt<T> =\n \
6232 { readonly tag: \"none\" }\n \
6233 | { readonly tag: \"some\"; readonly value: T };\n"
6234 );
6235 }
6236
6237 /// #1339's own real gap: `emit_record_type`'s own `export interface
6238 /// {name}{params} { readonly {field}: {ty}; ... }` — bare generic names
6239 /// on the interface header, `readonly` on every real member here.
6240 #[test]
6241 fn prints_a_generic_interface_with_readonly_members() {
6242 let mut program = TsProgram::new();
6243 program.push(TsStmt::decl(
6244 TsDecl::Export(Box::new(TsDecl::Interface {
6245 name: "Box".to_string(),
6246 type_params: vec!["T".to_string()],
6247 members: vec![TsTypeMember::readonly_prop("value", TsType::named("T"))],
6248 })),
6249 None,
6250 ));
6251 let printed = print(&program, "x.bynk", "", "x.ts");
6252 assert_eq!(
6253 printed.text,
6254 "export interface Box<T> {\n readonly value: T;\n}\n"
6255 );
6256 }
6257
6258 /// #1357's own real gap: `TsTypeMember::Method` had no `generics`/`doc`
6259 /// fields — `emit_capability`'s own interface methods are genuinely
6260 /// generic (no monomorphisation) and doc-commented per op. `doc` renders
6261 /// at `TsDecl::Interface`'s own render arm (not `render_type_member`
6262 /// itself, which has no `depth` to give `render_doc_comment`), mirroring
6263 /// `TsObjectEntry::Method.doc`'s own identical split (#1337).
6264 #[test]
6265 fn prints_a_generic_documented_interface_method() {
6266 let mut program = TsProgram::new();
6267 program.push(TsStmt::decl(
6268 TsDecl::Export(Box::new(TsDecl::Interface {
6269 name: "Clock".to_string(),
6270 type_params: Vec::new(),
6271 members: vec![TsTypeMember::Method {
6272 name: "now".to_string(),
6273 generics: vec!["T".to_string()],
6274 params: vec![TsParam {
6275 name: "unit".to_string(),
6276 ty: Some(TsType::named("T")),
6277 optional: false,
6278 }],
6279 ret: TsType::named("number"),
6280 doc: Some("Returns the current time.".to_string()),
6281 }],
6282 })),
6283 None,
6284 ));
6285 let printed = print(&program, "x.bynk", "", "x.ts");
6286 assert_eq!(
6287 printed.text,
6288 "export interface Clock {\n /**\n * Returns the current time.\n */\n now<T>(unit: T): number;\n}\n"
6289 );
6290 }
6291
6292 /// #1359's own real need: `print_class_method` — a class-method sibling
6293 /// of `print_object_entry`'s own fragment-printing entry point,
6294 /// `emit_provider`'s own hand-written class wrapper prints each real
6295 /// method through this directly. `depth` means the *class's* own
6296 /// depth, so at `depth == 0` the method itself lands at `indent(1)`,
6297 /// its body at `indent(2)`, matching `TsDecl::Class`'s own equivalent
6298 /// per-method indent exactly — but with no automatic blank-line
6299 /// insertion of its own (unlike `TsDecl::Class`'s own render arm),
6300 /// since `emit_provider`'s own real spacing has none between methods.
6301 #[test]
6302 fn prints_a_single_class_method_fragment() {
6303 let method = TsClassMethod {
6304 name: "double".to_string(),
6305 private: false,
6306 is_async: true,
6307 params: vec![TsParam {
6308 name: "n".to_string(),
6309 ty: Some(TsType::named("number")),
6310 optional: false,
6311 }],
6312 return_type: Some(TsType::named("number")),
6313 doc: None,
6314 body: vec![TsStmt::return_stmt(
6315 Some(TsExpr::Binary {
6316 op: TsBinaryOp::NullishCoalescing,
6317 left: Box::new(TsExpr::Ident("n".to_string())),
6318 right: Box::new(TsExpr::Lit(TsLit::Num("0".to_string()))),
6319 }),
6320 None,
6321 )],
6322 };
6323 assert_eq!(
6324 print_class_method(&method, 0),
6325 " async double(n: number): number {\n return n ?? 0;\n }\n"
6326 );
6327 }
6328
6329 /// Arc C, slice 21 (#1371): `TsClassMethod.private` — pins both the
6330 /// keyword itself and its render-*before*-`async` ordering directly,
6331 /// the same precedent `TsClassField.private` already has (its own
6332 /// direct test, not left to transitive `bynkc` fixture coverage alone)
6333 /// — review of #1372 caught this one still missing for the sibling
6334 /// field.
6335 #[test]
6336 fn prints_a_private_class_method_fragment() {
6337 let method = TsClassMethod {
6338 name: "loadState".to_string(),
6339 private: true,
6340 is_async: true,
6341 params: Vec::new(),
6342 return_type: Some(TsType::named_with_args(
6343 "Promise",
6344 vec![TsType::named("OrderState")],
6345 )),
6346 doc: None,
6347 body: vec![TsStmt::return_stmt(
6348 Some(TsExpr::Ident("state".to_string())),
6349 None,
6350 )],
6351 };
6352 assert_eq!(
6353 print_class_method(&method, 0),
6354 " private async loadState(): Promise<OrderState> {\n return state;\n }\n"
6355 );
6356 }
6357
6358 /// Arc C, slice 23 (#1375): `TsClassMethod.doc` — the grounding pass's
6359 /// own second predicted gap (#1366), the same need
6360 /// `TsObjectEntry::Method.doc` (#1337) and `TsTypeMember::Method.doc`
6361 /// (#1357) already solved for their own node kinds, closed here for the
6362 /// third and last real method-shaped node in this crate.
6363 #[test]
6364 fn prints_a_documented_class_method_fragment() {
6365 let method = TsClassMethod {
6366 name: "spend".to_string(),
6367 private: false,
6368 is_async: false,
6369 params: Vec::new(),
6370 return_type: Some(TsType::named("void")),
6371 doc: Some("Debits the account.".to_string()),
6372 body: vec![],
6373 };
6374 assert_eq!(
6375 print_class_method(&method, 0),
6376 " /**\n * Debits the account.\n */\n spend(): void {\n }\n"
6377 );
6378 }
6379
6380 /// #1339's own real gap: `TsExpr::Arrow` had no `generics`/`return_type`
6381 /// field — `emit_sum_type`'s own generic payload-constructor arrows
6382 /// (`<T>(name: T): Sum<T> => (...)`) need both. The object-literal body
6383 /// is wrapped in an explicit `Paren` — `Arrow`'s own renderer does not
6384 /// auto-parenthesise an object body the way real JS/TS syntax requires
6385 /// to disambiguate it from a block.
6386 #[test]
6387 fn prints_a_generic_arrow_with_a_parenthesised_object_body() {
6388 let mut program = TsProgram::new();
6389 program.push(TsStmt::expr_stmt(
6390 TsExpr::Arrow {
6391 params: vec![TsParam {
6392 name: "value".to_string(),
6393 ty: Some(TsType::named("T")),
6394 optional: false,
6395 }],
6396 is_async: false,
6397 generics: vec!["T".to_string()],
6398 return_type: Some(TsType::named_with_args("Sum", vec![TsType::named("T")])),
6399 body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Paren(Box::new(
6400 TsExpr::object_entries(vec![
6401 TsObjectEntry::Prop(
6402 "tag".to_string(),
6403 TsExpr::Lit(TsLit::Str("some".to_string())),
6404 ),
6405 TsObjectEntry::Shorthand("value".to_string()),
6406 ]),
6407 ))))),
6408 },
6409 None,
6410 ));
6411 let printed = print(&program, "x.bynk", "", "x.ts");
6412 assert_eq!(
6413 printed.text,
6414 "<T>(value: T): Sum<T> => ({ tag: \"some\", value });\n"
6415 );
6416 }
6417
6418 /// Arc C, slice 33 (#1401): `TsBinaryOp::LessThan` — pins the operator
6419 /// text itself.
6420 #[test]
6421 fn prints_a_less_than_comparison() {
6422 let mut program = TsProgram::new();
6423 program.push(TsStmt::expr_stmt(
6424 TsExpr::Binary {
6425 op: TsBinaryOp::LessThan,
6426 left: Box::new(TsExpr::Ident("a".to_string())),
6427 right: Box::new(TsExpr::Ident("b".to_string())),
6428 },
6429 None,
6430 ));
6431 let printed = print(&program, "x.bynk", "", "x.ts");
6432 assert_eq!(printed.text, "a < b;\n");
6433 }
6434
6435 /// `LessThan` shares `GreaterThan`'s own precedence tier — a nested
6436 /// `Add` under it needs no parens (`+` binds tighter), mirroring
6437 /// `add_binds_tighter_than_greater_than_on_both_sides` for the other
6438 /// relational operator.
6439 #[test]
6440 fn add_binds_tighter_than_less_than() {
6441 let mut program = TsProgram::new();
6442 program.push(TsStmt::expr_stmt(
6443 TsExpr::Binary {
6444 op: TsBinaryOp::LessThan,
6445 left: Box::new(TsExpr::Binary {
6446 op: TsBinaryOp::Add,
6447 left: Box::new(TsExpr::Ident("a".to_string())),
6448 right: Box::new(TsExpr::Ident("b".to_string())),
6449 }),
6450 right: Box::new(TsExpr::Ident("c".to_string())),
6451 },
6452 None,
6453 ));
6454 let printed = print(&program, "x.bynk", "", "x.ts");
6455 assert_eq!(printed.text, "a + b < c;\n");
6456 }
6457
6458 /// Arc C, slice 34 (`tests_emit.rs` slice D, #1403): `TsBinaryOp::
6459 /// InstanceOf` — pins the keyword operator text itself, sharing
6460 /// `LessThan`/`GreaterThan`'s own precedence tier.
6461 #[test]
6462 fn prints_an_instanceof_check() {
6463 let mut program = TsProgram::new();
6464 program.push(TsStmt::expr_stmt(
6465 TsExpr::Binary {
6466 op: TsBinaryOp::InstanceOf,
6467 left: Box::new(TsExpr::Ident("e".to_string())),
6468 right: Box::new(TsExpr::Ident("ExpectationError".to_string())),
6469 },
6470 None,
6471 ));
6472 let printed = print(&program, "x.bynk", "", "x.ts");
6473 assert_eq!(printed.text, "e instanceof ExpectationError;\n");
6474 }
6475
6476 /// Review of #1404, finding 2: `prints_an_instanceof_check` alone pins
6477 /// the operator text but not the precedence tier the same diff added to
6478 /// `binary_precedence` — it passes identically whether `InstanceOf` sits
6479 /// at tier 5, tier 1, or is missing from that arm entirely. `Add` (tier
6480 /// 6) binds tighter, mirroring `add_binds_tighter_than_less_than`
6481 /// for the other relational operator sharing this tier.
6482 #[test]
6483 fn add_binds_tighter_than_instanceof() {
6484 let mut program = TsProgram::new();
6485 program.push(TsStmt::expr_stmt(
6486 TsExpr::Binary {
6487 op: TsBinaryOp::InstanceOf,
6488 left: Box::new(TsExpr::Binary {
6489 op: TsBinaryOp::Add,
6490 left: Box::new(TsExpr::Ident("a".to_string())),
6491 right: Box::new(TsExpr::Ident("b".to_string())),
6492 }),
6493 right: Box::new(TsExpr::Ident("C".to_string())),
6494 },
6495 None,
6496 ));
6497 let printed = print(&program, "x.bynk", "", "x.ts");
6498 assert_eq!(printed.text, "a + b instanceof C;\n");
6499 }
6500
6501 /// Review of #1404, finding 2 (the more valuable half): a catch clause
6502 /// naturally grows into `e instanceof A || e instanceof B` as a second
6503 /// error type is added. `InstanceOf` (tier 5) binds tighter than `Or`
6504 /// (tier 2), so this must print flat — a wrong tier would silently
6505 /// over-parenthesize it into `(e instanceof A) || (e instanceof B)`.
6506 #[test]
6507 fn instanceof_binds_tighter_than_or_on_both_sides() {
6508 let mut program = TsProgram::new();
6509 program.push(TsStmt::expr_stmt(
6510 TsExpr::Binary {
6511 op: TsBinaryOp::Or,
6512 left: Box::new(TsExpr::Binary {
6513 op: TsBinaryOp::InstanceOf,
6514 left: Box::new(TsExpr::Ident("e".to_string())),
6515 right: Box::new(TsExpr::Ident("A".to_string())),
6516 }),
6517 right: Box::new(TsExpr::Binary {
6518 op: TsBinaryOp::InstanceOf,
6519 left: Box::new(TsExpr::Ident("e".to_string())),
6520 right: Box::new(TsExpr::Ident("B".to_string())),
6521 }),
6522 },
6523 None,
6524 ));
6525 let printed = print(&program, "x.bynk", "", "x.ts");
6526 assert_eq!(printed.text, "e instanceof A || e instanceof B;\n");
6527 }
6528
6529 /// Arc E slice 5 (`serialisation.rs`, #1443): `TsBinaryOp::In` — pins
6530 /// the keyword operator text itself, sharing `LessThan`/`GreaterThan`/
6531 /// `InstanceOf`'s own precedence tier.
6532 #[test]
6533 fn prints_an_in_check() {
6534 let mut program = TsProgram::new();
6535 program.push(TsStmt::expr_stmt(
6536 TsExpr::Binary {
6537 op: TsBinaryOp::In,
6538 left: Box::new(TsExpr::Lit(TsLit::Str("name".to_string()))),
6539 right: Box::new(TsExpr::Ident("obj".to_string())),
6540 },
6541 None,
6542 ));
6543 let printed = print(&program, "x.bynk", "", "x.ts");
6544 assert_eq!(printed.text, "\"name\" in obj;\n");
6545 }
6546
6547 /// Mirrors `add_binds_tighter_than_instanceof`/`add_binds_tighter_than_
6548 /// less_than` for the newest member of this shared precedence tier:
6549 /// `Add` (tier 6) binds tighter than `In` (tier 5).
6550 #[test]
6551 fn add_binds_tighter_than_in() {
6552 let mut program = TsProgram::new();
6553 program.push(TsStmt::expr_stmt(
6554 TsExpr::Binary {
6555 op: TsBinaryOp::In,
6556 left: Box::new(TsExpr::Binary {
6557 op: TsBinaryOp::Add,
6558 left: Box::new(TsExpr::Ident("a".to_string())),
6559 right: Box::new(TsExpr::Ident("b".to_string())),
6560 }),
6561 right: Box::new(TsExpr::Ident("obj".to_string())),
6562 },
6563 None,
6564 ));
6565 let printed = print(&program, "x.bynk", "", "x.ts");
6566 assert_eq!(printed.text, "a + b in obj;\n");
6567 }
6568
6569 /// Mirrors `instanceof_binds_tighter_than_or_on_both_sides`: `In` (tier
6570 /// 5) binds tighter than `Or` (tier 2), so a real
6571 /// `"a" in obj || "b" in obj` reads flat with no parens — a wrong tier
6572 /// would silently over-parenthesize it.
6573 #[test]
6574 fn in_binds_tighter_than_or_on_both_sides() {
6575 let mut program = TsProgram::new();
6576 program.push(TsStmt::expr_stmt(
6577 TsExpr::Binary {
6578 op: TsBinaryOp::Or,
6579 left: Box::new(TsExpr::Binary {
6580 op: TsBinaryOp::In,
6581 left: Box::new(TsExpr::Lit(TsLit::Str("a".to_string()))),
6582 right: Box::new(TsExpr::Ident("obj".to_string())),
6583 }),
6584 right: Box::new(TsExpr::Binary {
6585 op: TsBinaryOp::In,
6586 left: Box::new(TsExpr::Lit(TsLit::Str("b".to_string()))),
6587 right: Box::new(TsExpr::Ident("obj".to_string())),
6588 }),
6589 },
6590 None,
6591 ));
6592 let printed = print(&program, "x.bynk", "", "x.ts");
6593 assert_eq!(printed.text, "\"a\" in obj || \"b\" in obj;\n");
6594 }
6595
6596 /// Review of #1471, finding 1: `TsBinaryOp::GreaterThanEq`/`LessThanEq`
6597 /// landed with no direct `bynk-ts` test — only pinned indirectly through
6598 /// `bynk-emit`'s own fixture goldens. This test and the four below close
6599 /// that locality gap, mirroring the coverage every other operator on
6600 /// this precedence tier already has (`prints_an_in_check`/`add_binds_
6601 /// tighter_than_in`/`in_binds_tighter_than_or_on_both_sides`).
6602 #[test]
6603 fn prints_a_greater_than_or_equal_comparison() {
6604 let mut program = TsProgram::new();
6605 program.push(TsStmt::expr_stmt(
6606 TsExpr::Binary {
6607 op: TsBinaryOp::GreaterThanEq,
6608 left: Box::new(TsExpr::Ident("a".to_string())),
6609 right: Box::new(TsExpr::Ident("b".to_string())),
6610 },
6611 None,
6612 ));
6613 let printed = print(&program, "x.bynk", "", "x.ts");
6614 assert_eq!(printed.text, "a >= b;\n");
6615 }
6616
6617 #[test]
6618 fn prints_a_less_than_or_equal_comparison() {
6619 let mut program = TsProgram::new();
6620 program.push(TsStmt::expr_stmt(
6621 TsExpr::Binary {
6622 op: TsBinaryOp::LessThanEq,
6623 left: Box::new(TsExpr::Ident("a".to_string())),
6624 right: Box::new(TsExpr::Ident("b".to_string())),
6625 },
6626 None,
6627 ));
6628 let printed = print(&program, "x.bynk", "", "x.ts");
6629 assert_eq!(printed.text, "a <= b;\n");
6630 }
6631
6632 /// Pins `GreaterThanEq`/`LessThanEq`'s own tier-5 placement — the real
6633 /// `pred_condition_and_message` `InRange` shape (#1471) nests both under
6634 /// `And` (tier 3) and must print flat, with no parens on either side. A
6635 /// regression that demoted either operator to `And`'s own tier or below
6636 /// would silently wrap this in parens instead.
6637 #[test]
6638 fn greater_than_eq_and_less_than_eq_nest_flat_under_and() {
6639 let mut program = TsProgram::new();
6640 program.push(TsStmt::expr_stmt(
6641 TsExpr::Binary {
6642 op: TsBinaryOp::And,
6643 left: Box::new(TsExpr::Binary {
6644 op: TsBinaryOp::GreaterThanEq,
6645 left: Box::new(TsExpr::Ident("value".to_string())),
6646 right: Box::new(TsExpr::Lit(TsLit::Num("0".to_string()))),
6647 }),
6648 right: Box::new(TsExpr::Binary {
6649 op: TsBinaryOp::LessThanEq,
6650 left: Box::new(TsExpr::Ident("value".to_string())),
6651 right: Box::new(TsExpr::Lit(TsLit::Num("100".to_string()))),
6652 }),
6653 },
6654 None,
6655 ));
6656 let printed = print(&program, "x.bynk", "", "x.ts");
6657 assert_eq!(printed.text, "value >= 0 && value <= 100;\n");
6658 }
6659
6660 /// Same tier, *different* operator: `render_binary_operand`'s own doc
6661 /// says equal precedence still parenthesises unless the two sides are
6662 /// the exact same associative operator — `GreaterThan`/`GreaterThanEq`
6663 /// are equal-tier but distinct, so nesting one under the other must
6664 /// still keep its parens (the `_ => true` fallback of the equal-
6665 /// precedence match, newly reachable now this tier has more than one
6666 /// non-keyword operator).
6667 #[test]
6668 fn a_greater_than_eq_operand_of_a_different_relational_op_still_parenthesises() {
6669 let mut program = TsProgram::new();
6670 program.push(TsStmt::expr_stmt(
6671 TsExpr::Binary {
6672 op: TsBinaryOp::GreaterThan,
6673 left: Box::new(TsExpr::Ident("a".to_string())),
6674 right: Box::new(TsExpr::Binary {
6675 op: TsBinaryOp::GreaterThanEq,
6676 left: Box::new(TsExpr::Ident("b".to_string())),
6677 right: Box::new(TsExpr::Ident("c".to_string())),
6678 }),
6679 },
6680 None,
6681 ));
6682 let printed = print(&program, "x.bynk", "", "x.ts");
6683 assert_eq!(printed.text, "a > (b >= c);\n");
6684 }
6685
6686 /// `GreaterThanEq`/`LessThanEq` nested in themselves are NOT associative
6687 /// (`a >= b >= c` does not parse as `a >= (b >= c)`), unlike `||`/`&&` —
6688 /// so, unlike `in_binds_tighter_than_or_on_both_sides`'s own flat `In`
6689 /// chain, a same-operator nesting here must still parenthesise.
6690 #[test]
6691 fn a_right_nested_greater_than_eq_chain_keeps_its_parens() {
6692 let mut program = TsProgram::new();
6693 program.push(TsStmt::expr_stmt(
6694 TsExpr::Binary {
6695 op: TsBinaryOp::GreaterThanEq,
6696 left: Box::new(TsExpr::Ident("a".to_string())),
6697 right: Box::new(TsExpr::Binary {
6698 op: TsBinaryOp::GreaterThanEq,
6699 left: Box::new(TsExpr::Ident("b".to_string())),
6700 right: Box::new(TsExpr::Ident("c".to_string())),
6701 }),
6702 },
6703 None,
6704 ));
6705 let printed = print(&program, "x.bynk", "", "x.ts");
6706 assert_eq!(printed.text, "a >= (b >= c);\n");
6707 }
6708}