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 if needs_parens_as_array_element(element) {
1735 out.push('(');
1736 render_type(out, element);
1737 out.push(')');
1738 } else {
1739 render_type(out, element);
1740 }
1741 out.push_str("[]");
1742 }
1743 TsType::Object(members) => {
1744 // Review of #1358, finding 1: a `Method` member's own `doc` has
1745 // no single-line form — this inline shape has nowhere to put a
1746 // JSDoc block, unlike `TsDecl::Interface`'s own render arm
1747 // (which calls `render_doc_comment` before a documented
1748 // member's own line, since it has `depth` and a real newline
1749 // budget to work with). The identical case #1338's own review
1750 // already ruled on for `TsObjectEntry::Method.doc` in
1751 // `render_object_entry_inline` — a loud check, not a silent
1752 // drop, since silently dropping a JSDoc block is a strictly
1753 // worse failure mode than a loud one. Not reachable today
1754 // (`workers_entry.rs`'s own one real `TsType::Object` method
1755 // member goes through `TsTypeMember::method`, so `doc` is
1756 // always `None`) — the #1337 case was equally unreachable,
1757 // which is exactly why it got the assert.
1758 debug_assert!(
1759 !members
1760 .iter()
1761 .any(|m| matches!(m, TsTypeMember::Method { doc: Some(_), .. })),
1762 "render_type: a Method member's own doc comment has no single-line form"
1763 );
1764 if members.is_empty() {
1765 out.push_str("{}");
1766 } else {
1767 out.push_str("{ ");
1768 for (i, m) in members.iter().enumerate() {
1769 if i > 0 {
1770 out.push_str("; ");
1771 }
1772 render_type_member(out, m);
1773 }
1774 out.push_str(" }");
1775 }
1776 }
1777 TsType::Fn { params, ret } => {
1778 out.push('(');
1779 for (i, p) in params.iter().enumerate() {
1780 if i > 0 {
1781 out.push_str(", ");
1782 }
1783 out.push('a');
1784 out.push_str(&i.to_string());
1785 out.push_str(": ");
1786 render_type(out, p);
1787 }
1788 out.push_str(") => ");
1789 render_type(out, ret);
1790 }
1791 TsType::Union {
1792 members,
1793 multiline: false,
1794 } => {
1795 for (i, m) in members.iter().enumerate() {
1796 if i > 0 {
1797 out.push_str(" | ");
1798 }
1799 render_type(out, m);
1800 }
1801 }
1802 // #1339: `emit_sum_type`'s own real shape — one variant per line, a
1803 // leading `|` on every line except the first (which gets equivalent
1804 // spacing instead, matching the pre-conversion `writeln!` code's own
1805 // `let pipe = if i == 0 { " " } else { "|" };` exactly), the closing
1806 // `;` appended by `TsDecl::TypeAlias`'s own caller directly after
1807 // the last member's own line — see `TsType::Union`'s own doc for
1808 // why this needs no depth parameter despite the multi-line shape.
1809 TsType::Union {
1810 members,
1811 multiline: true,
1812 } => {
1813 for (i, m) in members.iter().enumerate() {
1814 if i > 0 {
1815 out.push('\n');
1816 }
1817 out.push_str(" ");
1818 out.push_str(if i == 0 { " " } else { "|" });
1819 out.push(' ');
1820 render_type(out, m);
1821 }
1822 }
1823 TsType::Intersection(members) => {
1824 for (i, m) in members.iter().enumerate() {
1825 if i > 0 {
1826 out.push_str(" & ");
1827 }
1828 render_type(out, m);
1829 }
1830 }
1831 }
1832}
1833
1834/// Whether `element` must be parenthesised before a postfix `[]`. `[]` binds
1835/// tighter than every prefix or infix type operator, so a readonly array
1836/// (`readonly T[][]` reads as `readonly (T[][])`, which tsc rejects with
1837/// TS1354), a function type (`() => T[]` returns the array) and a union or
1838/// intersection (`A | B[]`) would each be misread bare. A mutable array, a
1839/// named type and an object literal are postfix-safe as they are.
1840fn needs_parens_as_array_element(element: &TsType) -> bool {
1841 matches!(
1842 element,
1843 TsType::Array { readonly: true, .. }
1844 | TsType::Fn { .. }
1845 | TsType::Union { .. }
1846 | TsType::Intersection(_)
1847 )
1848}
1849
1850/// One [`TsTypeMember`] rendered inline (no leading indent, no trailing
1851/// separator) — [`render_type`]'s own `TsType::Object` arm's per-member
1852/// renderer.
1853fn render_type_member(out: &mut String, member: &TsTypeMember) {
1854 match member {
1855 TsTypeMember::Prop {
1856 name,
1857 ty,
1858 optional,
1859 readonly,
1860 } => {
1861 if *readonly {
1862 out.push_str("readonly ");
1863 }
1864 out.push_str(name);
1865 if *optional {
1866 out.push('?');
1867 }
1868 out.push_str(": ");
1869 render_type(out, ty);
1870 }
1871 TsTypeMember::Method {
1872 name,
1873 generics,
1874 params,
1875 ret,
1876 doc: _,
1877 } => {
1878 out.push_str(name);
1879 render_bare_generics(out, generics);
1880 out.push('(');
1881 render_params(out, params);
1882 out.push_str("): ");
1883 render_type(out, ret);
1884 }
1885 TsTypeMember::Index {
1886 key_name,
1887 key_ty,
1888 value_ty,
1889 } => {
1890 out.push('[');
1891 out.push_str(key_name);
1892 out.push_str(": ");
1893 render_type(out, key_ty);
1894 out.push_str("]: ");
1895 render_type(out, value_ty);
1896 }
1897 }
1898}
1899
1900/// Print a single [`TsType`] on its own — the real callers this closes R7.2
1901/// for (`bynk-emit`'s `ts_type_ref*`/`ts_ty` families, P7.9, #1315) each
1902/// want one type fragment to interpolate into a larger, still-hand-built
1903/// line (a field's own type annotation, a parameter list, …), not a whole
1904/// [`TsProgram`]. No source-map/buffer machinery — [`print()`] owns that
1905/// for a whole document; this is the printer's other, narrower entry
1906/// point, sharing the same internal recursion rather than a second copy.
1907pub fn print_type(ty: &TsType) -> String {
1908 let mut out = String::new();
1909 render_type(&mut out, ty);
1910 out
1911}
1912
1913/// Print a single [`TsExpr`] on its own — the expression-level sibling of
1914/// [`print_type`]'s own "one fragment, not a whole document" entry point.
1915/// Arc C, step (11) (#1388) need: `emit_icu_placeholder`'s own `Select` arm
1916/// stays one opaque, hand-built block-bodied IIFE (at the time, `TsExpr::
1917/// Arrow` had no block-body variant at all, and extending it for that one
1918/// site was rejected as disproportionate) — its own arm VALUES,
1919/// `emit_sub_message`'s now-real `TsExpr` results, still need stringifying
1920/// back into that opaque host text. #1435 (Arc E slice 1) later added
1921/// [`TsArrowBody::Block`] for a genuinely different real site
1922/// (`serialisation.rs`'s `Float` guard); `emit_icu_placeholder`'s own
1923/// `Select` arm was not reconverted along with it — out of that slice's own
1924/// scope — so this call site's opaque text stays exactly as it was. No
1925/// source-map/buffer machinery, matching [`print_type`]/[`print_stmt`]'s
1926/// own scope exactly.
1927pub fn print_expr(expr: &TsExpr) -> String {
1928 let mut out = String::new();
1929 render_expr(&mut out, expr);
1930 out
1931}
1932
1933/// Print a single [`TsStmt`] on its own, at `depth` — the statement-level
1934/// sibling of [`print_type`]'s own "one fragment, not a whole document"
1935/// entry point. `bynk-emit`'s own #1333 need (`emit_doc_block`, a shared
1936/// helper spliced into ~14 still-unconverted callers' own buffers) wants
1937/// one statement's own printed text at a caller-supplied depth, not a
1938/// whole [`TsProgram`] — no source-map/buffer machinery, matching
1939/// [`print_type`]'s own scope exactly, and reusing `render_stmt`'s own
1940/// exhaustive per-kind dispatch (this module's own private renderer)
1941/// rather than a second copy.
1942/// Review of #1402: a `TsStmtKind::Raw` nested inside a `Switch` case's own
1943/// body (`emit_stub_rhs`'s own `ReturnsEach` dispatch, Arc C slice 33,
1944/// `tests_emit.rs` slice C) carries the identical "no indent of its own,
1945/// pre-indented at a fixed absolute depth" hazard `render_class_method`'s
1946/// and `render_multiline_object_entry`'s own `debug_assert!`s already guard
1947/// — `stmt_contains_raw` already recurses into `Switch` cases, so the same
1948/// check applies here, this fragment entry point's own first `Raw`-bearing
1949/// `Switch` caller. A bare `Raw` passed directly as `stmt` itself is exempt
1950/// (not a false negative — `render_stmt`'s own `Raw` arm never reads
1951/// `depth` at all, so calling `print_stmt` on a bare `Raw` is safe at any
1952/// depth, the established `print_stmt_renders_raw_text_verbatim_with_no_
1953/// added_indent_or_punctuation` contract below): only a Raw *nested inside*
1954/// a depth-using wrapper (like this `Switch` case) is the real hazard.
1955pub fn print_stmt(stmt: &TsStmt, depth: usize) -> String {
1956 debug_assert!(
1957 depth == 0 || matches!(stmt.kind, TsStmtKind::Raw(_)) || !stmt_contains_raw(stmt),
1958 "print_stmt: a Raw-bearing statement's own baked-in indent only matches depth 0"
1959 );
1960 let mut out = String::new();
1961 render_stmt(&mut out, stmt, depth, None);
1962 out
1963}
1964
1965/// [`print_stmt`]'s own sibling (#1477), the `TsDecl::Function`-body
1966/// counterpart to [`print_class_method_and_merge`]: identical rendering,
1967/// but merges any `nested_map` `stmt` carries — on `stmt` itself, or on
1968/// anything nested inside it (a function's body, a class's constructor/
1969/// methods, …) — into `map` as it prints, at the real print-time offset —
1970/// the same offset `bynk-emit`'s own `emit_free_fn` (`emitter/emit.rs`) used
1971/// to recover by exact arithmetic over the returned text, guarded by a
1972/// `debug_assert!`, before #1480 converted it to set `nested_map` directly
1973/// instead. Kept separate from `print_stmt` itself so every existing
1974/// caller's own call sites are untouched. `source_id` is `map`'s
1975/// own registered source this statement's content belongs to (see
1976/// this crate's own private `MergeTarget`'s own doc for why this can't just be hardcoded).
1977///
1978/// **Appends to the caller's own `out`, unlike `print_stmt`'s own
1979/// return-a-fresh-`String` shape** — the merge computes each checkpoint's
1980/// absolute offset from `out.len()` *as this statement's own text is
1981/// written*, so it must be the caller's real, already-populated buffer, not
1982/// a throwaway local one starting at 0. Returning a fresh string here (the
1983/// first shape this function took, caught before merge) would silently
1984/// record every checkpoint at the wrong offset the moment the caller
1985/// spliced the returned text anywhere but the very start of its own buffer.
1986pub fn print_stmt_and_merge(
1987 out: &mut String,
1988 stmt: &TsStmt,
1989 depth: usize,
1990 map: &mut SourceMapBuilder,
1991 source_id: usize,
1992) {
1993 debug_assert!(
1994 depth == 0 || matches!(stmt.kind, TsStmtKind::Raw(_)) || !stmt_contains_raw(stmt),
1995 "print_stmt_and_merge: a Raw-bearing statement's own baked-in indent only matches depth 0"
1996 );
1997 render_stmt(out, stmt, depth, Some(MergeTarget { map, source_id }));
1998}
1999
2000/// Print a single [`TsObjectEntry`] on its own, at `depth` — the
2001/// object-entry sibling of [`print_stmt`]'s own "one fragment, not a whole
2002/// document" entry point, `depth` meaning the SAME thing it does for this
2003/// crate's own internal multi-line-object renderer: the *object's* own
2004/// depth, so the entry itself lands one level deeper, matching an object built by that
2005/// renderer exactly. #1337's own real need: `emit_attached_methods` (a
2006/// shared helper spliced into `emit_refined_type`/`emit_record_type`/
2007/// `emit_sum_type`'s own still-unconverted `&mut String` buffers) now
2008/// returns `Vec<TsObjectEntry>` instead of writing text directly — each
2009/// caller renders the returned entries one at a time through this, the
2010/// same P7.9/#1333 "keep the caller's own signature, print just the
2011/// fragment" pattern applied to an object-entry-shaped fragment instead of
2012/// a whole statement or type.
2013///
2014/// Review of #1338, finding 3: a `TsStmtKind::Raw` body statement (e.g.
2015/// `emit_method`'s own opaque `lower.rs`-sourced body, or `emit_refined_
2016/// type`'s own opaque `emit_refined_checks`-sourced guard body, #1339's
2017/// second real use) carries NO indent of its own — its text is captured
2018/// pre-indented at a fixed absolute depth by its own caller — so it only
2019/// renders correctly when `depth` is `0`. The guard for this now lives in
2020/// `render_multiline_object_entry` itself (a private renderer, so named
2021/// here in text rather than linked — moved there by review of #1340,
2022/// finding 1: this function's own copy missed the `TsExpr::
2023/// multiline_object_entries`/`render_expr` call path #1339 added, which
2024/// reaches that renderer directly, never through this one), so it fires
2025/// for every caller, not just this entry point.
2026pub fn print_object_entry(entry: &TsObjectEntry, depth: usize) -> String {
2027 let mut out = String::new();
2028 render_multiline_object_entry(&mut out, entry, depth, None);
2029 out
2030}
2031
2032/// [`print_object_entry`]'s own sibling (#1477), the `TsObjectEntry::Method`
2033/// counterpart to [`print_class_method_and_merge`]/[`print_stmt_and_merge`]:
2034/// identical rendering, but merges a `Method` entry's own body `nested_map`
2035/// into `map` as it prints, at the real print-time offset. Kept separate
2036/// from `print_object_entry` itself so every existing caller's own call
2037/// sites are untouched.
2038///
2039/// Appends to the caller's own `out`, the same "must be the real buffer,
2040/// not a throwaway local one" reasoning [`print_stmt_and_merge`]'s own doc
2041/// explains in full. `source_id`: see this crate's own private `MergeTarget`'s own doc.
2042pub fn print_object_entry_and_merge(
2043 out: &mut String,
2044 entry: &TsObjectEntry,
2045 depth: usize,
2046 map: &mut SourceMapBuilder,
2047 source_id: usize,
2048) {
2049 render_multiline_object_entry(out, entry, depth, Some(MergeTarget { map, source_id }));
2050}
2051
2052/// Whether `stmts`, or any block/branch/case nested inside them at any
2053/// depth, contains a [`TsStmtKind::Raw`] — the recursive check
2054/// [`render_class_method`]'s and [`render_multiline_object_entry`]'s own
2055/// `debug_assert!`s need. Review of #1374: a top-level-only scan
2056/// (`body.iter().any(...)`) is blind to a `Raw` buried inside an `If`'s own
2057/// `Block` — `emit_agent`'s own `commitState` (#1373) is exactly this shape,
2058/// a transition's own hoisted-statement `Raw`s sitting inside
2059/// `if (__prior !== undefined) { ... }` — so the two guards this exists for
2060/// would have silently missed exactly the case they exist to catch. Every
2061/// `TsStmtKind` variant is named explicitly, not a wildcard, matching this
2062/// crate's own exhaustiveness discipline.
2063fn contains_raw(stmts: &[TsStmt]) -> bool {
2064 stmts.iter().any(stmt_contains_raw)
2065}
2066
2067fn stmt_contains_raw(stmt: &TsStmt) -> bool {
2068 match &stmt.kind {
2069 TsStmtKind::Raw(_) => true,
2070 TsStmtKind::Verbatim { .. }
2071 | TsStmtKind::Decl(_)
2072 | TsStmtKind::Const { .. }
2073 | TsStmtKind::Let { .. }
2074 | TsStmtKind::ExprStmt(_)
2075 | TsStmtKind::Return(_)
2076 | TsStmtKind::Throw(_)
2077 | TsStmtKind::Continue
2078 | TsStmtKind::Assign { .. }
2079 | TsStmtKind::Comment(_)
2080 | TsStmtKind::DocComment(_)
2081 | TsStmtKind::Blank
2082 | TsStmtKind::Increment(_) => false,
2083 TsStmtKind::If {
2084 then_branch,
2085 else_branch,
2086 ..
2087 } => {
2088 stmt_contains_raw(then_branch) || else_branch.as_deref().is_some_and(stmt_contains_raw)
2089 }
2090 TsStmtKind::ForOf { body, .. } | TsStmtKind::For { body, .. } => stmt_contains_raw(body),
2091 TsStmtKind::TryCatch {
2092 try_block,
2093 catch_block,
2094 ..
2095 } => stmt_contains_raw(try_block) || stmt_contains_raw(catch_block),
2096 TsStmtKind::Block(stmts) | TsStmtKind::InlineBlock(stmts) => contains_raw(stmts),
2097 TsStmtKind::Switch { cases, .. } => cases.iter().any(|c| contains_raw(&c.body)),
2098 }
2099}
2100
2101/// Print a single [`TsClassMethod`] on its own, at `depth` — the
2102/// class-method sibling of [`print_object_entry`]'s own "one fragment, not
2103/// a whole document" entry point. `depth` means the *class's* own depth
2104/// (matching `print_object_entry`'s own convention exactly), so the method
2105/// itself lands at `depth + 1`, its own body at `depth + 2`. #1359's own
2106/// real need: `emit_provider`'s own class wrapper stays hand-written text
2107/// (each method's own body needs a per-method source-map sub-builder/
2108/// `merge`, and the wrapper's own real spacing — no blank line between
2109/// methods — genuinely differs from [`TsDecl::Class`]'s own "one blank
2110/// line before each method" policy, `events_fanout.rs`'s real convention,
2111/// #1317), so each real method prints through here directly into that
2112/// still-hand-written wrapper. Deliberately no automatic blank-line
2113/// insertion of its own — the same "caller controls spacing" contract
2114/// `print_object_entry` already established.
2115pub fn print_class_method(method: &TsClassMethod, depth: usize) -> String {
2116 let mut out = String::new();
2117 render_class_method(&mut out, method, depth, None);
2118 out
2119}
2120
2121/// [`print_class_method`]'s own sibling (#1477): identical rendering, but
2122/// merges any of `method.body`'s own direct-child `nested_map`s into `map`
2123/// as they print, at the real print-time offset — no reverse-engineering
2124/// the offset from the returned text afterward, the way `bynk-emit`'s own
2125/// `emit_class_method_and_merge_source_map` (`emitter/emit.rs`) has to
2126/// today. Kept as a separate function, not an added parameter on
2127/// `print_class_method` itself, so every existing caller's own call sites
2128/// are untouched — this is a strict addition.
2129///
2130/// Appends to the caller's own `out`, the same "must be the real buffer,
2131/// not a throwaway local one" reasoning [`print_stmt_and_merge`]'s own doc
2132/// explains in full. `source_id`: see this crate's own private `MergeTarget`'s own doc.
2133pub fn print_class_method_and_merge(
2134 out: &mut String,
2135 method: &TsClassMethod,
2136 depth: usize,
2137 map: &mut SourceMapBuilder,
2138 source_id: usize,
2139) {
2140 render_class_method(out, method, depth, Some(MergeTarget { map, source_id }));
2141}
2142
2143/// The one real per-method render, shared by [`print_class_method`] and
2144/// [`TsDecl::Class`]'s own methods-loop arm — review of #1360, finding 4:
2145/// keeping two independent copies of this shape was exactly the drift risk
2146/// this track exists to design against (two printers disagreeing about a
2147/// formatting convention). `depth` means the *class's* own depth, matching
2148/// both callers' own convention.
2149///
2150/// Review of #1360, finding 1: a `TsStmtKind::Raw` method body has no
2151/// indent of its own — its text is captured pre-indented at a fixed
2152/// absolute depth by its own caller — so it only renders correctly when
2153/// `depth` is `0`, the same hazard `render_multiline_object_entry`'s own
2154/// identical `debug_assert!` guards (review of #1338 finding 3, relocated
2155/// by review of #1340 finding 1). `print_class_method`'s own doc frames it
2156/// as a general fragment entry point, so a future caller at a non-zero
2157/// depth is the expected case this guards against, not a hypothetical.
2158///
2159/// `map`: see [`render_block_stmts`]'s own doc (#1477) — forwarded
2160/// unchanged to `method.body`'s own rendering.
2161fn render_class_method(
2162 out: &mut String,
2163 method: &TsClassMethod,
2164 depth: usize,
2165 map: Option<MergeTarget<'_>>,
2166) {
2167 debug_assert!(
2168 depth == 0 || !contains_raw(&method.body),
2169 "render_class_method: a Raw method body's own baked-in indent only matches depth 0"
2170 );
2171 if let Some(text) = &method.doc {
2172 render_doc_comment(out, text, depth + 1);
2173 }
2174 out.push_str(&indent(depth + 1));
2175 if method.private {
2176 out.push_str("private ");
2177 }
2178 if method.is_async {
2179 out.push_str("async ");
2180 }
2181 out.push_str(&method.name);
2182 out.push('(');
2183 render_params(out, &method.params);
2184 out.push(')');
2185 if let Some(rt) = &method.return_type {
2186 out.push_str(": ");
2187 render_type(out, rt);
2188 }
2189 render_block_stmts(out, &method.body, depth + 1, map);
2190 out.push('\n');
2191}
2192
2193fn render_params(out: &mut String, params: &[TsParam]) {
2194 for (i, p) in params.iter().enumerate() {
2195 if i > 0 {
2196 out.push_str(", ");
2197 }
2198 out.push_str(&p.name);
2199 if p.optional {
2200 out.push('?');
2201 }
2202 if let Some(ty) = &p.ty {
2203 out.push_str(": ");
2204 render_type(out, ty);
2205 }
2206 }
2207}
2208
2209fn render_decl(out: &mut String, decl: &TsDecl, depth: usize, map: Option<MergeTarget<'_>>) {
2210 // `render_decl_body`'s own `TsDecl::Export` arm already writes
2211 // `"export "` before recursing into the inner declaration — the
2212 // `if let Export` special case this replaced produced byte-identical
2213 // output by duplicating that same logic one layer up (review of
2214 // #1314, smaller note: the two branches were provably the same for
2215 // every input, so only one needs to exist).
2216 out.push_str(&indent(depth));
2217 render_decl_body(out, decl, depth, map);
2218}
2219
2220/// The declaration's own text, with no leading indent — [`render_decl`]
2221/// writes the indent (and, for `Export`, the `export ` keyword) once, then
2222/// hands off here so `Export(inner)` doesn't duplicate `inner`'s own
2223/// leading whitespace.
2224///
2225/// `map`: see [`render_block_stmts`]'s own doc (#1477) — reaches a
2226/// `TsDecl::Function`'s own body, and a `TsDecl::Class`'s own constructor/
2227/// methods, unchanged; every other arm ignores it (no body of its own to
2228/// carry a `nested_map`).
2229fn render_decl_body(
2230 out: &mut String,
2231 decl: &TsDecl,
2232 depth: usize,
2233 mut map: Option<MergeTarget<'_>>,
2234) {
2235 match decl {
2236 TsDecl::Import {
2237 type_only,
2238 names,
2239 from,
2240 } => {
2241 out.push_str(if *type_only {
2242 "import type { "
2243 } else {
2244 "import { "
2245 });
2246 out.push_str(&names.join(", "));
2247 out.push_str(" } from \"");
2248 out.push_str(from);
2249 out.push_str("\";\n");
2250 }
2251 TsDecl::ImportNamespace {
2252 type_only,
2253 alias,
2254 from,
2255 } => {
2256 out.push_str(if *type_only {
2257 "import type * as "
2258 } else {
2259 "import * as "
2260 });
2261 out.push_str(alias);
2262 out.push_str(" from \"");
2263 out.push_str(from);
2264 out.push_str("\";\n");
2265 }
2266 TsDecl::ImportDefault { alias, from } => {
2267 out.push_str("import ");
2268 out.push_str(alias);
2269 out.push_str(" from \"");
2270 out.push_str(from);
2271 out.push_str("\";\n");
2272 }
2273 TsDecl::ReExport { names, from } => {
2274 out.push_str("export { ");
2275 out.push_str(&names.join(", "));
2276 out.push_str(" } from \"");
2277 out.push_str(from);
2278 out.push_str("\";\n");
2279 }
2280 TsDecl::ExportNames { names } => {
2281 out.push_str("export { ");
2282 out.push_str(&names.join(", "));
2283 out.push_str(" };\n");
2284 }
2285 TsDecl::ReExportAll { from } => {
2286 out.push_str("export * from \"");
2287 out.push_str(from);
2288 out.push_str("\";\n");
2289 }
2290 TsDecl::Export(inner) => {
2291 out.push_str("export ");
2292 render_decl_body(out, inner, depth, map);
2293 }
2294 TsDecl::Interface {
2295 name,
2296 type_params,
2297 members,
2298 } => {
2299 out.push_str("interface ");
2300 out.push_str(name);
2301 render_bare_generics(out, type_params);
2302 out.push_str(" {\n");
2303 for member in members {
2304 // #1357: a `Method` member's own `doc` renders here, not in
2305 // `render_type_member` itself — that function has no
2306 // `depth` to give `render_doc_comment`, the same "doc lives
2307 // at the call site that carries depth" split
2308 // `render_multiline_object_entry`'s own `Method` arm
2309 // already uses for the identical field.
2310 if let TsTypeMember::Method {
2311 doc: Some(text), ..
2312 } = member
2313 {
2314 render_doc_comment(out, text, depth + 1);
2315 }
2316 out.push_str(&indent(depth + 1));
2317 render_type_member(out, member);
2318 out.push_str(";\n");
2319 }
2320 out.push_str(&indent(depth));
2321 out.push_str("}\n");
2322 }
2323 TsDecl::ConstDecl { name, ty, init } => {
2324 out.push_str("const ");
2325 out.push_str(name);
2326 if let Some(ty) = ty {
2327 out.push_str(": ");
2328 render_type(out, ty);
2329 }
2330 out.push_str(" = ");
2331 render_stmt_level_expr(out, init, depth);
2332 out.push_str(";\n");
2333 }
2334 TsDecl::Class {
2335 name,
2336 fields,
2337 constructor,
2338 methods,
2339 } => {
2340 out.push_str("class ");
2341 out.push_str(name);
2342 out.push_str(" {\n");
2343 for f in fields {
2344 out.push_str(&indent(depth + 1));
2345 if f.private {
2346 out.push_str("private ");
2347 }
2348 out.push_str(&f.name);
2349 out.push_str(": ");
2350 render_type(out, &f.ty);
2351 out.push_str(";\n");
2352 }
2353 // Readability policy (this module's own doc): no blank line
2354 // between fields and the constructor; one blank line before
2355 // each method — `events_fanout.rs`'s own real class spacing.
2356 let mut wrote_member = !fields.is_empty();
2357 if let Some(ctor) = constructor {
2358 out.push_str(&indent(depth + 1));
2359 out.push_str("constructor(");
2360 render_params(out, &ctor.params);
2361 out.push(')');
2362 render_block_stmts(
2363 out,
2364 &ctor.body,
2365 depth + 1,
2366 map.as_mut().map(|t| t.reborrow()),
2367 );
2368 out.push('\n');
2369 wrote_member = true;
2370 }
2371 for m in methods {
2372 if wrote_member {
2373 out.push('\n');
2374 }
2375 render_class_method(out, m, depth, map.as_mut().map(|t| t.reborrow()));
2376 wrote_member = true;
2377 }
2378 out.push_str(&indent(depth));
2379 out.push_str("}\n");
2380 }
2381 TsDecl::Function {
2382 name,
2383 generics,
2384 params,
2385 return_type,
2386 body,
2387 is_async,
2388 inline,
2389 } => {
2390 if *is_async {
2391 out.push_str("async ");
2392 }
2393 out.push_str("function ");
2394 out.push_str(name);
2395 render_bare_generics(out, generics);
2396 out.push('(');
2397 render_params(out, params);
2398 out.push(')');
2399 if let Some(rt) = return_type {
2400 out.push_str(": ");
2401 render_type(out, rt);
2402 }
2403 if *inline {
2404 out.push(' ');
2405 render_inline_block(out, body);
2406 } else {
2407 render_block_stmts(out, body, depth, map);
2408 out.push('\n');
2409 }
2410 }
2411 TsDecl::TypeAlias {
2412 name,
2413 type_params,
2414 ty,
2415 } => {
2416 out.push_str("type ");
2417 out.push_str(name);
2418 render_bare_generics(out, type_params);
2419 // #1339: a multiline Union's own first rendered character is
2420 // its first member's leading indent, not a newline — the `=`
2421 // itself must be followed directly by `\n` here (no space
2422 // before it), matching the pre-conversion `writeln!(out,
2423 // "export type {name}{params} =")` line's own exact bytes;
2424 // every other `ty` keeps the ordinary `" = "` (space both
2425 // sides) ordinary single-line form.
2426 if matches!(
2427 ty,
2428 TsType::Union {
2429 multiline: true,
2430 ..
2431 }
2432 ) {
2433 out.push_str(" =\n");
2434 } else {
2435 out.push_str(" = ");
2436 }
2437 render_type(out, ty);
2438 out.push_str(";\n");
2439 }
2440 TsDecl::ExportDefault(expr) => {
2441 out.push_str("export default ");
2442 render_stmt_level_expr(out, expr, depth);
2443 out.push_str(";\n");
2444 }
2445 TsDecl::DeclareConst { name, ty } => {
2446 out.push_str("declare const ");
2447 out.push_str(name);
2448 out.push_str(": ");
2449 render_type(out, ty);
2450 out.push_str(";\n");
2451 }
2452 }
2453}
2454
2455/// `{ <stmts> }` for a constructor/method body — same shape as
2456/// [`render_block_body`] but over a plain `&[TsStmt]` (a `TsClassCtor`/
2457/// `TsClassMethod`'s own `body` field, not a boxed `TsStmt`), and always
2458/// followed by the caller's own `\n` (constructor/method declarations, not
2459/// a same-line `try`/`catch` continuation).
2460///
2461/// `map: Some(_)` when the caller wants a `nested_map`-bearing statement
2462/// reachable from `stmts` merged into a module source map as it prints
2463/// (#1477) — see [`crate::program::TsStmt::nested_map`]'s own doc for the
2464/// problem this solves. Reborrowed once per child and forwarded to
2465/// [`render_stmt`], which does the actual check — this function itself
2466/// holds none of that logic.
2467fn render_block_stmts(
2468 out: &mut String,
2469 stmts: &[TsStmt],
2470 depth: usize,
2471 mut map: Option<MergeTarget<'_>>,
2472) {
2473 out.push_str(" {\n");
2474 for s in stmts {
2475 render_stmt(out, s, depth + 1, map.as_mut().map(|t| t.reborrow()));
2476 }
2477 out.push_str(&indent(depth));
2478 out.push('}');
2479}
2480
2481#[cfg(test)]
2482mod tests {
2483 use super::*;
2484 use crate::program::{TsClassCtor, TsClassField, TsClassMethod, TsSwitchCase, VerbatimOrigin};
2485 use bynk_syntax::span::Span;
2486
2487 /// Pins the readability policy's statement-separation guarantee (R7.5,
2488 /// this module's own doc): every statement starts on its own generated
2489 /// line.
2490 #[test]
2491 fn prints_every_statement_in_order() {
2492 let mut program = TsProgram::new();
2493 program.push(TsStmt::verbatim(
2494 VerbatimOrigin::Contracts,
2495 "const a = 1;\n",
2496 None,
2497 ));
2498 program.push(TsStmt::verbatim(
2499 VerbatimOrigin::Secrets,
2500 "const b = 2;\n",
2501 None,
2502 ));
2503 let printed = print(&program, "x.bynk", "", "x.ts");
2504 assert_eq!(printed.text, "const a = 1;\nconst b = 2;\n");
2505 }
2506
2507 /// Pins the readability policy's statement-separation guarantee (R7.5,
2508 /// this module's own doc) in its sharpest form: nothing requires
2509 /// `Verbatim` text to be newline-terminated — the printer owns line
2510 /// structure (R7.3), so it's the one that guarantees two statements
2511 /// never share a generated line, not a `TsStmt::verbatim` caller
2512 /// obligation nobody enforces (review of #1308, finding 2).
2513 #[test]
2514 fn a_statement_missing_its_own_trailing_newline_still_gets_its_own_line() {
2515 let mut program = TsProgram::new();
2516 program.push(TsStmt::verbatim(
2517 VerbatimOrigin::Contracts,
2518 "const a = 1;",
2519 None,
2520 ));
2521 program.push(TsStmt::verbatim(
2522 VerbatimOrigin::Secrets,
2523 "const b = 2;",
2524 None,
2525 ));
2526 let printed = print(&program, "x.bynk", "", "x.ts");
2527 assert_eq!(printed.text, "const a = 1;\nconst b = 2;\n");
2528 }
2529
2530 /// Production `Verbatim` content is rarely one line — `VerbatimOrigin::
2531 /// NotYetConverted` (P7.6) wraps a whole generated document per
2532 /// statement, so one statement routinely spans dozens of generated
2533 /// lines. Neither of the two tests above exercised that shape (review
2534 /// of #1312, finding 1) — this one confirms the readability policy's
2535 /// real invariant (the *next* statement always starts on a fresh line)
2536 /// holds for multi-line content too, not just the single-line text
2537 /// those tests happened to use.
2538 #[test]
2539 fn a_multi_line_statement_still_starts_the_next_statement_on_a_fresh_line() {
2540 let mut program = TsProgram::new();
2541 program.push(TsStmt::verbatim(
2542 VerbatimOrigin::Contracts,
2543 "function a() {\n return 1;\n}\n",
2544 None,
2545 ));
2546 program.push(TsStmt::verbatim(
2547 VerbatimOrigin::Secrets,
2548 "const b = 2;\n",
2549 None,
2550 ));
2551 let printed = print(&program, "x.bynk", "", "x.ts");
2552 assert_eq!(
2553 printed.text,
2554 "function a() {\n return 1;\n}\nconst b = 2;\n"
2555 );
2556 }
2557
2558 /// `print` decides whether to append a trailing newline by checking the
2559 /// whole *buffer*, not the statement's own text (review of #1312,
2560 /// finding 2) — an empty statement's text changes nothing, so whether
2561 /// it gets a line depends entirely on what's already in the buffer.
2562 /// First in a program, the buffer is still empty, so the check fires
2563 /// and the output opens with a blank line. Pinned here so this edge
2564 /// stays a known, deliberate reading of the code rather than an
2565 /// untested accident.
2566 #[test]
2567 fn an_empty_statement_first_in_the_program_yields_a_leading_blank_line() {
2568 let mut program = TsProgram::new();
2569 program.push(TsStmt::verbatim(VerbatimOrigin::Contracts, "", None));
2570 program.push(TsStmt::verbatim(
2571 VerbatimOrigin::Secrets,
2572 "const b = 2;\n",
2573 None,
2574 ));
2575 let printed = print(&program, "x.bynk", "", "x.ts");
2576 assert_eq!(printed.text, "\nconst b = 2;\n");
2577 }
2578
2579 /// The other half of the buffer-vs-statement-text distinction (review
2580 /// of #1312, finding 2): an empty statement following one that already
2581 /// ended its own text in `\n` contributes no line at all — the buffer
2582 /// already ends in `\n`, so the check doesn't fire, and the empty
2583 /// statement's own (non-)content and the next statement's text end up
2584 /// on what reads as one generated line for the next statement alone.
2585 #[test]
2586 fn an_empty_statement_after_a_newline_terminated_one_contributes_no_line_of_its_own() {
2587 let mut program = TsProgram::new();
2588 program.push(TsStmt::verbatim(
2589 VerbatimOrigin::Contracts,
2590 "const a = 1;\n",
2591 None,
2592 ));
2593 program.push(TsStmt::verbatim(VerbatimOrigin::Secrets, "", None));
2594 program.push(TsStmt::verbatim(
2595 VerbatimOrigin::RuntimeUse,
2596 "const c = 3;\n",
2597 None,
2598 ));
2599 let printed = print(&program, "x.bynk", "", "x.ts");
2600 assert_eq!(printed.text, "const a = 1;\nconst c = 3;\n");
2601 }
2602
2603 #[test]
2604 fn no_spans_at_all_means_no_source_map() {
2605 let mut program = TsProgram::new();
2606 program.push(TsStmt::verbatim(VerbatimOrigin::Contracts, "x;\n", None));
2607 let printed = print(&program, "x.bynk", "let x = 1\n", "x.ts");
2608 assert_eq!(printed.source_map, None);
2609 }
2610
2611 /// The property that actually matters (R7.4): the printer's own
2612 /// buffer-position bookkeeping is correct — a checkpoint recorded from a
2613 /// later statement's span still resolves to the *generated* line that
2614 /// statement's own text landed on, not (as the old splice-based
2615 /// mechanism could get wrong, #4) some other buffer's offset.
2616 ///
2617 /// Review of #1308, finding 3: the original version of this test
2618 /// asserted only that the source map's `sources` array was present —
2619 /// true even with the second statement's checkpoint silently dropped,
2620 /// exactly the failure this doc comment claims is ruled out. Asserting
2621 /// full equality against a map built independently (replicating the
2622 /// printer's own record-then-write order by hand, not calling `print`)
2623 /// actually pins both statements' own offsets.
2624 #[test]
2625 fn each_statements_span_resolves_to_its_own_generated_line() {
2626 let source = "let a = 1\nlet b = 2\n";
2627 let off_a = source.find("let a").unwrap();
2628 let off_b = source.find("let b").unwrap();
2629 let span_a = Span::new(off_a, off_a + 5);
2630 let span_b = Span::new(off_b, off_b + 5);
2631 let text_a = "const a = 1;\n";
2632 let text_b = "const b = 2;\n";
2633 let mut program = TsProgram::new();
2634 program.push(TsStmt::verbatim(
2635 VerbatimOrigin::Contracts,
2636 text_a,
2637 Some(span_a),
2638 ));
2639 program.push(TsStmt::verbatim(
2640 VerbatimOrigin::Secrets,
2641 text_b,
2642 Some(span_b),
2643 ));
2644
2645 let printed = print(&program, "x.bynk", source, "x.ts");
2646
2647 let mut expected_map = SourceMapBuilder::new();
2648 expected_map.add_source("x.bynk", source);
2649 expected_map.record(0, span_a);
2650 expected_map.record(text_a.len(), span_b);
2651 let expected = expected_map.to_v3(&printed.text, "x.ts");
2652
2653 assert_eq!(printed.source_map, expected);
2654 }
2655
2656 // -- #1477: nested source-map checkpoints on an opaque body blob. --
2657
2658 /// `print_stmt_and_merge` — the `TsDecl::Function` shape (`emit_free_fn`'s
2659 /// own real site): a `Raw` body statement's own `nested_map` merges into
2660 /// the caller's map at the real print-time offset, appended into a
2661 /// buffer that already has *other* content first (`base != 0`) — proving
2662 /// this reads the caller's own real buffer position, not a fresh
2663 /// throwaway one starting at 0 (the bug this function's own first draft
2664 /// had, caught before merge: returning a `String` computes the offset
2665 /// against that empty local buffer, silently wrong the moment the
2666 /// caller splices the result anywhere but the very start of its own
2667 /// `out`).
2668 #[test]
2669 fn print_stmt_and_merge_merges_a_function_bodys_own_nested_checkpoint() {
2670 let source = "fn f() { return 1 }\n";
2671 let ret_off = source.find("return 1").unwrap();
2672 let ret_span = Span::new(ret_off, ret_off + 8);
2673
2674 let body_text = " return 1;\n";
2675 let mut nested = SourceMapBuilder::new();
2676 nested.record(0, ret_span);
2677
2678 let mut raw = TsStmt::raw(body_text, None);
2679 raw.nested_map = Some(nested);
2680
2681 let func = TsStmt::decl(
2682 TsDecl::Function {
2683 name: "f".to_string(),
2684 generics: Vec::new(),
2685 params: Vec::new(),
2686 return_type: None,
2687 body: vec![raw],
2688 is_async: false,
2689 inline: false,
2690 },
2691 None,
2692 );
2693
2694 let mut out = String::from("// header\n");
2695 let mut map = SourceMapBuilder::new();
2696 map.add_source("x.bynk", source);
2697
2698 print_stmt_and_merge(&mut out, &func, 0, &mut map, 0);
2699
2700 assert_eq!(out, "// header\nfunction f() {\n return 1;\n}\n");
2701 let base = out.find(body_text).unwrap();
2702
2703 let mut expected = SourceMapBuilder::new();
2704 expected.add_source("x.bynk", source);
2705 expected.record(base, ret_span);
2706 assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
2707 }
2708
2709 /// `print_class_method_and_merge` — the `TsClassMethod` shape
2710 /// (`emit_provider`/`emit_agent`'s own real site, today reverse-engineered
2711 /// by `emit_class_method_and_merge_source_map`'s own string-matching).
2712 /// Same non-zero-`base` proof as the `TsDecl::Function` test above.
2713 #[test]
2714 fn print_class_method_and_merge_merges_a_methods_own_nested_checkpoint() {
2715 let source = "on call m() { commit }\n";
2716 let commit_off = source.find("commit").unwrap();
2717 let commit_span = Span::new(commit_off, commit_off + 6);
2718
2719 let body_text = " this.commit();\n";
2720 let mut nested = SourceMapBuilder::new();
2721 nested.record(0, commit_span);
2722
2723 let mut raw = TsStmt::raw(body_text, None);
2724 raw.nested_map = Some(nested);
2725
2726 let method = TsClassMethod {
2727 name: "m".to_string(),
2728 private: false,
2729 is_async: false,
2730 params: Vec::new(),
2731 return_type: None,
2732 doc: None,
2733 body: vec![raw],
2734 };
2735
2736 let mut out = String::from("class C {\n");
2737 let mut map = SourceMapBuilder::new();
2738 map.add_source("x.bynk", source);
2739
2740 print_class_method_and_merge(&mut out, &method, 0, &mut map, 0);
2741
2742 let base = out.find(body_text).unwrap();
2743 let mut expected = SourceMapBuilder::new();
2744 expected.add_source("x.bynk", source);
2745 expected.record(base, commit_span);
2746 assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
2747 }
2748
2749 /// `print_object_entry_and_merge` — the `TsObjectEntry::Method` shape
2750 /// (`emit_service`'s own real per-handler site): identical mechanism,
2751 /// exercised through its own, third public entry point.
2752 #[test]
2753 fn print_object_entry_and_merge_merges_a_methods_own_nested_checkpoint() {
2754 let source = "on call h() { reply }\n";
2755 let reply_off = source.find("reply").unwrap();
2756 let reply_span = Span::new(reply_off, reply_off + 5);
2757
2758 let body_text = " return reply();\n";
2759 let mut nested = SourceMapBuilder::new();
2760 nested.record(0, reply_span);
2761
2762 let mut raw = TsStmt::raw(body_text, None);
2763 raw.nested_map = Some(nested);
2764
2765 let entry = TsObjectEntry::Method {
2766 name: "h".to_string(),
2767 is_async: false,
2768 generics: Vec::new(),
2769 params: Vec::new(),
2770 return_type: None,
2771 doc: None,
2772 inline: false,
2773 body: vec![raw],
2774 };
2775
2776 let mut out = String::from("export const svc = {\n");
2777 let mut map = SourceMapBuilder::new();
2778 map.add_source("x.bynk", source);
2779
2780 print_object_entry_and_merge(&mut out, &entry, 0, &mut map, 0);
2781
2782 let base = out.find(body_text).unwrap();
2783 let mut expected = SourceMapBuilder::new();
2784 expected.add_source("x.bynk", source);
2785 expected.record(base, reply_span);
2786 assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
2787 }
2788
2789 /// `print()`'s own top-level loop (#1477): a real `TsProgram` containing
2790 /// a `Decl`-kinded top-level statement whose own body carries a
2791 /// `nested_map` merges it automatically — no `bynk-emit` change needed
2792 /// to benefit once a real construction site sets the field, since
2793 /// `print()` already threads its own module map through `render_stmt`
2794 /// for every top-level statement.
2795 #[test]
2796 fn print_merges_a_top_level_functions_own_nested_checkpoint_automatically() {
2797 let source = "fn f() { return 1 }\n";
2798 let ret_off = source.find("return 1").unwrap();
2799 let ret_span = Span::new(ret_off, ret_off + 8);
2800
2801 let body_text = " return 1;\n";
2802 let mut nested = SourceMapBuilder::new();
2803 nested.record(0, ret_span);
2804
2805 let mut raw = TsStmt::raw(body_text, None);
2806 raw.nested_map = Some(nested);
2807
2808 let mut program = TsProgram::new();
2809 program.push(TsStmt::decl(
2810 TsDecl::Function {
2811 name: "f".to_string(),
2812 generics: Vec::new(),
2813 params: Vec::new(),
2814 return_type: None,
2815 body: vec![raw],
2816 is_async: false,
2817 inline: false,
2818 },
2819 None,
2820 ));
2821
2822 let printed = print(&program, "x.bynk", source, "x.ts");
2823 let base = printed.text.find(body_text).unwrap();
2824
2825 let mut expected = SourceMapBuilder::new();
2826 expected.add_source("x.bynk", source);
2827 expected.record(base, ret_span);
2828 assert_eq!(printed.source_map, expected.to_v3(&printed.text, "x.ts"));
2829 }
2830
2831 /// Review of #1488, finding 1: `source_id` must reach the merge, not be
2832 /// hardcoded — a multi-source module map (a test module aggregating
2833 /// several `.bynk` files, `tests_emit.rs:537`/`:1936`'s own real shape)
2834 /// tags a nested checkpoint to the *wrong* source silently if the merge
2835 /// always assumes index 0.
2836 ///
2837 /// Decisive by construction, not just an assertion on the map's own
2838 /// `sources` list (which would pass either way — every registered
2839 /// source name appears there regardless of which one a checkpoint
2840 /// actually resolves against, `to_v3`'s own doc/body confirms `sources`/
2841 /// `sourcesContent` iterate the full registered list unconditionally):
2842 /// source 0's own text is a single short line, too short to contain the
2843 /// real checkpoint's span. If the merge resolves the checkpoint against
2844 /// source 0 (the bug this test exists to catch), `to_v3`'s own
2845 /// out-of-range guard (`span.start > text.len()`) silently drops it and
2846 /// the whole map comes back `None` (its only checkpoint). Only
2847 /// resolving against the real source (id 1, correctly threaded through)
2848 /// keeps the span in range and produces a real map.
2849 #[test]
2850 fn print_stmt_and_merge_tags_a_nested_checkpoint_to_the_given_source_id() {
2851 let primary = "x\n"; // deliberately shorter than `ret_span`'s own offset
2852 let secondary = "fn f() { return 1 }\n";
2853 let ret_off = secondary.find("return 1").unwrap();
2854 let ret_span = Span::new(ret_off, ret_off + 8);
2855 assert!(
2856 ret_span.start > primary.len(),
2857 "sanity: the span must fall outside source 0's own short text"
2858 );
2859
2860 let body_text = " return 1;\n";
2861 let mut nested = SourceMapBuilder::new();
2862 nested.record(0, ret_span);
2863
2864 let mut raw = TsStmt::raw(body_text, None);
2865 raw.nested_map = Some(nested);
2866
2867 let func = TsStmt::decl(
2868 TsDecl::Function {
2869 name: "f".to_string(),
2870 generics: Vec::new(),
2871 params: Vec::new(),
2872 return_type: None,
2873 body: vec![raw],
2874 is_async: false,
2875 inline: false,
2876 },
2877 None,
2878 );
2879
2880 let mut out = String::new();
2881 let mut map = SourceMapBuilder::new();
2882 map.add_source("primary.bynk", primary); // id 0
2883 let secondary_id = map.add_source("secondary.bynk", secondary); // id 1
2884 assert_eq!(secondary_id, 1);
2885
2886 print_stmt_and_merge(&mut out, &func, 0, &mut map, secondary_id);
2887
2888 assert!(
2889 map.to_v3(&out, "x.ts").is_some(),
2890 "the checkpoint resolved against the wrong source (id 0) and was silently dropped"
2891 );
2892 }
2893
2894 /// #1486's own sibling gap to the one above, but in [`print_multi_
2895 /// source`]'s own automatic top-level loop rather than the manual
2896 /// `print_stmt_and_merge` entry point: a real `TsProgram` built for a
2897 /// multi-source module (a test/integration module aggregating several
2898 /// `.bynk` fragment files, one per case) needs each case's own
2899 /// top-level statement merged against *its own* registered source, not
2900 /// the module's primary one (id 0) — `nested_map_source_id`'s own doc
2901 /// comment. Decisive the same way the sibling test above is: source 0's
2902 /// text is too short to contain the real span, so a wrong-source merge
2903 /// silently drops the checkpoint (`to_v3`'s own out-of-range guard) and
2904 /// the map comes back missing it.
2905 #[test]
2906 fn print_multi_source_honours_a_top_level_statements_own_nested_map_source_id() {
2907 let primary = "x\n";
2908 let secondary = "fn f() { return 1 }\n";
2909 let ret_off = secondary.find("return 1").unwrap();
2910 let ret_span = Span::new(ret_off, ret_off + 8);
2911 assert!(
2912 ret_span.start > primary.len(),
2913 "sanity: the span must fall outside source 0's own short text"
2914 );
2915
2916 let body_text = " return 1;\n";
2917 let mut nested = SourceMapBuilder::new();
2918 nested.record(0, ret_span);
2919
2920 let mut raw = TsStmt::raw(body_text, None);
2921 raw.nested_map = Some(nested);
2922
2923 let mut func = TsStmt::decl(
2924 TsDecl::Function {
2925 name: "f".to_string(),
2926 generics: Vec::new(),
2927 params: Vec::new(),
2928 return_type: None,
2929 body: vec![raw],
2930 is_async: false,
2931 inline: false,
2932 },
2933 None,
2934 );
2935 func.nested_map_source_id = 1;
2936
2937 let mut program = TsProgram::new();
2938 program.push(func);
2939
2940 let mut map = SourceMapBuilder::new();
2941 map.add_source("primary.bynk", primary);
2942 map.add_source("secondary.bynk", secondary);
2943 let printed = print_multi_source(&program, map, "x.ts");
2944 assert!(
2945 printed.source_map.is_some(),
2946 "the checkpoint resolved against the wrong source (id 0) and was silently dropped"
2947 );
2948 }
2949
2950 /// Review of #1488, finding 2: a `nested_map` set on the statement handed
2951 /// directly to `print`/`print_stmt_and_merge` itself — not nested inside
2952 /// a `Decl` body — must still merge, not silently vanish. The first
2953 /// version of this mechanism only checked a body's own *direct
2954 /// children* (inside `render_block_stmts`), missing this exact case.
2955 #[test]
2956 fn print_stmt_and_merge_merges_a_bare_top_level_raw_statements_own_nested_checkpoint() {
2957 let source = "contract seal() {}\n";
2958 let off = source.find("contract").unwrap();
2959 let span = Span::new(off, off + 8);
2960
2961 let text = "seal();\n";
2962 let mut nested = SourceMapBuilder::new();
2963 nested.record(0, span);
2964
2965 let mut raw = TsStmt::raw(text, None);
2966 raw.nested_map = Some(nested);
2967
2968 let mut out = String::from("// header\n");
2969 let mut map = SourceMapBuilder::new();
2970 map.add_source("x.bynk", source);
2971
2972 print_stmt_and_merge(&mut out, &raw, 0, &mut map, 0);
2973
2974 let base = out.find(text).unwrap();
2975 let mut expected = SourceMapBuilder::new();
2976 expected.add_source("x.bynk", source);
2977 expected.record(base, span);
2978 assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
2979 }
2980
2981 /// Review of #1488, finding 3 (part 1): the merge's own line-rebasing
2982 /// arithmetic (`out_line = base_line + body_line`) is exercised for
2983 /// real, not just the degenerate `body_line == 0` case every other test
2984 /// here uses — a two-statement body with the checkpoint on the
2985 /// *second* line.
2986 #[test]
2987 fn print_stmt_and_merge_rebases_a_checkpoint_on_a_bodys_second_line() {
2988 let source = "fn f() { let a = 1\n return a }\n";
2989 let ret_off = source.find("return a").unwrap();
2990 let ret_span = Span::new(ret_off, ret_off + 8);
2991
2992 let body_text = " const a = 1;\n return a;\n";
2993 let second_line_off = body_text.find("return a;").unwrap();
2994 let mut nested = SourceMapBuilder::new();
2995 nested.record(second_line_off, ret_span);
2996
2997 let mut raw = TsStmt::raw(body_text, None);
2998 raw.nested_map = Some(nested);
2999
3000 let func = TsStmt::decl(
3001 TsDecl::Function {
3002 name: "f".to_string(),
3003 generics: Vec::new(),
3004 params: Vec::new(),
3005 return_type: None,
3006 body: vec![raw],
3007 is_async: false,
3008 inline: false,
3009 },
3010 None,
3011 );
3012
3013 let mut out = String::from("// header\n");
3014 let mut map = SourceMapBuilder::new();
3015 map.add_source("x.bynk", source);
3016
3017 print_stmt_and_merge(&mut out, &func, 0, &mut map, 0);
3018
3019 let base_of_body = out.find(body_text).unwrap();
3020 let base_of_second_line = out.find(" return a;\n").unwrap();
3021 assert!(
3022 base_of_second_line > base_of_body,
3023 "sanity: second line is after the first"
3024 );
3025
3026 let mut expected = SourceMapBuilder::new();
3027 expected.add_source("x.bynk", source);
3028 expected.record(base_of_second_line, ret_span);
3029 assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
3030 }
3031
3032 /// Review of #1488, finding 3 (part 2): `TsDecl::Class`'s own
3033 /// constructor-then-methods sequence is the one real place `map` is
3034 /// reborrowed across more than one consumer — a class with a
3035 /// `nested_map`-bearing constructor body *and* a `nested_map`-bearing
3036 /// method body proves both reborrows land correctly, not just the
3037 /// first one. `emit_provider`/`emit_agent` (both class-shaped) are
3038 /// exactly the real callers this slice unblocks.
3039 #[test]
3040 fn a_classs_constructor_and_method_both_merge_their_own_nested_checkpoints() {
3041 let source = "on new() { init }\non call m() { commit }\n";
3042 let init_off = source.find("init").unwrap();
3043 let init_span = Span::new(init_off, init_off + 4);
3044 let commit_off = source.find("commit").unwrap();
3045 let commit_span = Span::new(commit_off, commit_off + 6);
3046
3047 let ctor_body_text = " this.init();\n";
3048 let mut ctor_nested = SourceMapBuilder::new();
3049 ctor_nested.record(0, init_span);
3050 let mut ctor_raw = TsStmt::raw(ctor_body_text, None);
3051 ctor_raw.nested_map = Some(ctor_nested);
3052
3053 let method_body_text = " this.commit();\n";
3054 let mut method_nested = SourceMapBuilder::new();
3055 method_nested.record(0, commit_span);
3056 let mut method_raw = TsStmt::raw(method_body_text, None);
3057 method_raw.nested_map = Some(method_nested);
3058
3059 let class = TsDecl::Class {
3060 name: "C".to_string(),
3061 fields: Vec::new(),
3062 constructor: Some(TsClassCtor {
3063 params: Vec::new(),
3064 body: vec![ctor_raw],
3065 }),
3066 methods: vec![TsClassMethod {
3067 name: "m".to_string(),
3068 private: false,
3069 is_async: false,
3070 params: Vec::new(),
3071 return_type: None,
3072 doc: None,
3073 body: vec![method_raw],
3074 }],
3075 };
3076
3077 let mut out = String::new();
3078 let mut map = SourceMapBuilder::new();
3079 map.add_source("x.bynk", source);
3080
3081 print_stmt_and_merge(&mut out, &TsStmt::decl(class, None), 0, &mut map, 0);
3082
3083 let ctor_base = out.find(ctor_body_text).unwrap();
3084 let method_base = out.find(method_body_text).unwrap();
3085
3086 let mut expected = SourceMapBuilder::new();
3087 expected.add_source("x.bynk", source);
3088 expected.record(ctor_base, init_span);
3089 expected.record(method_base, commit_span);
3090 assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
3091 }
3092
3093 // -- P7.8 (#1313): real node rendering. --
3094
3095 #[test]
3096 fn prints_a_const_statement_with_a_destructured_binding_and_a_type_cast() {
3097 let mut program = TsProgram::new();
3098 program.push(TsStmt::const_stmt(
3099 TsBindingName::ObjectPattern(vec!["events".to_string()]),
3100 None,
3101 TsExpr::As {
3102 // #1323: `Await` no longer auto-parenthesizes under `As` —
3103 // this file's own real content wraps it explicitly via
3104 // `Paren` (see `render_expr`'s own `As` arm doc).
3105 expr: Box::new(TsExpr::Paren(Box::new(TsExpr::Await(Box::new(
3106 TsExpr::Call {
3107 callee: Box::new(TsExpr::Member {
3108 object: Box::new(TsExpr::Ident("request".to_string())),
3109 property: "json".to_string(),
3110 }),
3111 args: vec![],
3112 },
3113 ))))),
3114 ty: TsType::Object(vec![TsTypeMember::prop(
3115 "events",
3116 TsType::array(TsType::named("FanoutEvent")),
3117 )]),
3118 },
3119 None,
3120 ));
3121 let printed = print(&program, "x.bynk", "", "x.ts");
3122 assert_eq!(
3123 printed.text,
3124 "const { events } = (await request.json()) as { events: FanoutEvent[] };\n"
3125 );
3126 }
3127
3128 #[test]
3129 fn prints_if_without_braces_when_the_body_is_not_a_block() {
3130 let mut program = TsProgram::new();
3131 program.push(TsStmt::if_stmt(
3132 TsExpr::Unary {
3133 op: TsUnaryOp::Not,
3134 expr: Box::new(TsExpr::Call {
3135 callee: Box::new(TsExpr::Member {
3136 object: Box::new(TsExpr::Ident("Array".to_string())),
3137 property: "isArray".to_string(),
3138 }),
3139 args: vec![TsExpr::Ident("subs".to_string())],
3140 }),
3141 },
3142 TsStmt::continue_stmt(None),
3143 None,
3144 ));
3145 let printed = print(&program, "x.bynk", "", "x.ts");
3146 assert_eq!(printed.text, "if (!Array.isArray(subs)) continue;\n");
3147 }
3148
3149 /// Review of #1317/#1318, finding 2: a bare `Comment` as an `if`'s
3150 /// brace-free body must not fall through to the top-level `//`-line
3151 /// form — `if (cond) // text` comments out the rest of the physical
3152 /// line, leaving `if` with no body at all (a parse error). Renders as
3153 /// a block comment instead, which cannot swallow anything after it.
3154 #[test]
3155 fn a_comment_as_an_ifs_brace_free_body_renders_as_a_block_comment() {
3156 let mut program = TsProgram::new();
3157 program.push(TsStmt::if_stmt(
3158 TsExpr::Ident("cond".to_string()),
3159 TsStmt::comment("annotation", None),
3160 None,
3161 ));
3162 let printed = print(&program, "x.bynk", "", "x.ts");
3163 assert_eq!(printed.text, "if (cond) /* annotation */\n");
3164 }
3165
3166 #[test]
3167 fn prints_a_for_of_loop_with_a_braced_body() {
3168 let mut program = TsProgram::new();
3169 program.push(TsStmt::for_of(
3170 "ev",
3171 TsExpr::Ident("events".to_string()),
3172 TsStmt::block(
3173 vec![TsStmt::return_stmt(
3174 Some(TsExpr::Ident("ev".to_string())),
3175 None,
3176 )],
3177 None,
3178 ),
3179 None,
3180 ));
3181 let printed = print(&program, "x.bynk", "", "x.ts");
3182 assert_eq!(
3183 printed.text,
3184 "for (const ev of events) {\n return ev;\n}\n"
3185 );
3186 }
3187
3188 /// Arc E slice 7 (#1447): `TsStmtKind::For` — pins the exact C-style
3189 /// header shape `ListInst`/`MapInst`'s own real deserialise-side element
3190 /// loops need (`for (let i = 0; i < json.length; i++) { ... }`),
3191 /// including the postfix `++` the printer appends over `name` itself
3192 /// (see `TsStmtKind::For`'s own doc for why there is no separate
3193 /// `update` field to pass).
3194 #[test]
3195 fn prints_a_c_style_for_loop_with_a_braced_body() {
3196 let mut program = TsProgram::new();
3197 program.push(TsStmt::for_stmt(
3198 "i",
3199 TsExpr::Lit(TsLit::Num("0".to_string())),
3200 TsExpr::Binary {
3201 op: TsBinaryOp::LessThan,
3202 left: Box::new(TsExpr::Ident("i".to_string())),
3203 right: Box::new(TsExpr::Member {
3204 object: Box::new(TsExpr::Ident("json".to_string())),
3205 property: "length".to_string(),
3206 }),
3207 },
3208 TsStmt::block(
3209 vec![TsStmt::expr_stmt(TsExpr::Ident("item".to_string()), None)],
3210 None,
3211 ),
3212 None,
3213 ));
3214 let printed = print(&program, "x.bynk", "", "x.ts");
3215 assert_eq!(
3216 printed.text,
3217 "for (let i = 0; i < json.length; i++) {\n item;\n}\n"
3218 );
3219 }
3220
3221 /// A `For` loop's own brace-free body falls back to the same inline
3222 /// rendering `ForOf`'s own brace-free body already uses — exercised as
3223 /// the *outer* statement, over a `Continue` body (not reachable from any
3224 /// real `bynk-emit` content today, but a real, direct shape).
3225 #[test]
3226 fn prints_a_c_style_for_loops_brace_free_body_inline() {
3227 let mut program = TsProgram::new();
3228 program.push(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 let printed = print(&program, "x.bynk", "", "x.ts");
3240 assert_eq!(printed.text, "for (let i = 0; i < n; i++) continue;\n");
3241 }
3242
3243 /// Review of #1448, finding 1: the previous test above pins a brace-free
3244 /// `For` as the *outer* statement, which renders through `render_stmt`'s
3245 /// own dedicated `For` arm, not the `render_inline_stmt` fallback this
3246 /// slice's own `| TsStmtKind::For { .. }` line actually added. That
3247 /// fallback only fires when a `For` is itself the brace-free body of an
3248 /// ENCLOSING `if`/`for...of`/`For` — exercised directly here, nesting a
3249 /// brace-free `For` as an `if`'s own brace-free branch.
3250 #[test]
3251 fn prints_a_brace_free_for_loop_nested_as_an_ifs_own_brace_free_body() {
3252 let mut program = TsProgram::new();
3253 program.push(TsStmt::if_stmt(
3254 TsExpr::Ident("cond".to_string()),
3255 TsStmt::for_stmt(
3256 "i",
3257 TsExpr::Lit(TsLit::Num("0".to_string())),
3258 TsExpr::Binary {
3259 op: TsBinaryOp::LessThan,
3260 left: Box::new(TsExpr::Ident("i".to_string())),
3261 right: Box::new(TsExpr::Ident("n".to_string())),
3262 },
3263 TsStmt::continue_stmt(None),
3264 None,
3265 ),
3266 None,
3267 ));
3268 let printed = print(&program, "x.bynk", "", "x.ts");
3269 assert_eq!(
3270 printed.text,
3271 "if (cond) for (let i = 0; i < n; i++) continue;\n"
3272 );
3273 }
3274
3275 #[test]
3276 fn prints_a_try_catch_on_the_shared_closing_brace_line() {
3277 let mut program = TsProgram::new();
3278 program.push(TsStmt::try_catch(
3279 TsStmt::block(
3280 vec![TsStmt::expr_stmt(
3281 TsExpr::Await(Box::new(TsExpr::Call {
3282 callee: Box::new(TsExpr::Ident("deliverEvent".to_string())),
3283 args: vec![TsExpr::Ident("binding".to_string())],
3284 })),
3285 None,
3286 )],
3287 None,
3288 ),
3289 Some("e"),
3290 TsStmt::block(
3291 vec![TsStmt::expr_stmt(
3292 TsExpr::Call {
3293 callee: Box::new(TsExpr::Member {
3294 object: Box::new(TsExpr::Ident("console".to_string())),
3295 property: "error".to_string(),
3296 }),
3297 args: vec![TsExpr::Lit(TsLit::Str("failed".to_string()))],
3298 },
3299 None,
3300 )],
3301 None,
3302 ),
3303 None,
3304 ));
3305 let printed = print(&program, "x.bynk", "", "x.ts");
3306 assert_eq!(
3307 printed.text,
3308 "try {\n await deliverEvent(binding);\n} catch (e) {\n console.error(\"failed\");\n}\n"
3309 );
3310 }
3311
3312 #[test]
3313 fn prints_an_interface_with_an_inline_nested_object_type() {
3314 let mut program = TsProgram::new();
3315 program.push(TsStmt::decl(
3316 TsDecl::Interface {
3317 name: "FanoutEvent".to_string(),
3318 type_params: Vec::new(),
3319 members: vec![
3320 TsTypeMember::prop("type", TsType::named("string")),
3321 TsTypeMember::prop("payload", TsType::named("unknown")),
3322 TsTypeMember::prop(
3323 "envelope",
3324 TsType::Object(vec![
3325 TsTypeMember::prop("eventId", TsType::named("string")),
3326 TsTypeMember::prop("publisherId", TsType::named("string")),
3327 ]),
3328 ),
3329 ],
3330 },
3331 None,
3332 ));
3333 let printed = print(&program, "x.bynk", "", "x.ts");
3334 assert_eq!(
3335 printed.text,
3336 "interface FanoutEvent {\n type: string;\n payload: unknown;\n envelope: { eventId: string; publisherId: string };\n}\n"
3337 );
3338 }
3339
3340 #[test]
3341 fn prints_two_adjacent_imports_with_no_blank_line_between_them() {
3342 let mut program = TsProgram::new();
3343 program.push(TsStmt::decl(
3344 TsDecl::Import {
3345 type_only: true,
3346 names: vec!["A".to_string()],
3347 from: "../x.js".to_string(),
3348 },
3349 None,
3350 ));
3351 program.push(TsStmt::decl(
3352 TsDecl::Import {
3353 type_only: false,
3354 names: vec!["b".to_string()],
3355 from: "../x.js".to_string(),
3356 },
3357 None,
3358 ));
3359 program.push(TsStmt::decl(
3360 TsDecl::ConstDecl {
3361 name: "c".to_string(),
3362 ty: None,
3363 init: TsExpr::Lit(TsLit::Null),
3364 },
3365 None,
3366 ));
3367 let printed = print(&program, "x.bynk", "", "x.ts");
3368 assert_eq!(
3369 printed.text,
3370 "import type { A } from \"../x.js\";\nimport { b } from \"../x.js\";\n\nconst c = null;\n"
3371 );
3372 }
3373
3374 /// #1486: `no_blank_before` suppresses the automatic blank line before
3375 /// exactly the statement that carries it — a `const`/`function` pair
3376 /// neither an import, a comment, a `ReExportAll`, nor `Verbatim`, so
3377 /// none of the existing exemptions apply; without this field the two
3378 /// would print with an unwanted blank between them (confirmed by
3379 /// temporarily reverting the field during this same change — the
3380 /// printer's own default policy really does insert one here). A third,
3381 /// ordinary statement after it confirms the field is scoped to the one
3382 /// carrying it, not sticky for the rest of the program.
3383 #[test]
3384 fn no_blank_before_suppresses_exactly_the_next_statements_own_blank() {
3385 let mut program = TsProgram::new();
3386 program.push(TsStmt::decl(
3387 TsDecl::ConstDecl {
3388 name: "__FooRegistry".to_string(),
3389 ty: None,
3390 init: TsExpr::Lit(TsLit::Null),
3391 },
3392 None,
3393 ));
3394 let mut zero_fn = TsStmt::decl(
3395 TsDecl::Function {
3396 name: "__zeroOfFooState".to_string(),
3397 generics: Vec::new(),
3398 params: Vec::new(),
3399 return_type: None,
3400 body: Vec::new(),
3401 is_async: false,
3402 inline: true,
3403 },
3404 None,
3405 );
3406 zero_fn.no_blank_before = true;
3407 program.push(zero_fn);
3408 program.push(TsStmt::decl(
3409 TsDecl::ConstDecl {
3410 name: "c".to_string(),
3411 ty: None,
3412 init: TsExpr::Lit(TsLit::Null),
3413 },
3414 None,
3415 ));
3416 let printed = print(&program, "x.bynk", "", "x.ts");
3417 assert_eq!(
3418 printed.text,
3419 "const __FooRegistry = null;\nfunction __zeroOfFooState() { }\n\nconst c = null;\n"
3420 );
3421 }
3422
3423 /// The required "does the algebra hold against real content" test
3424 /// (P7.8's own accepted proposal, #1313): builds
3425 /// `bynk-emit/src/emitter/events_fanout.rs`'s own `EventsFanoutDO`
3426 /// class as real nodes (its field, constructor, and `fetch` method,
3427 /// with the exact control flow the real function emits — `for...of`,
3428 /// nested `if`-without-braces, `try`/`catch`) and asserts the printed
3429 /// text is byte-identical to what `emit_events_fanout_do` produces
3430 /// today for that class (transcribed directly from
3431 /// `bynk-emit/src/emitter/events_fanout.rs`'s own `write!` calls, not
3432 /// re-derived — including the constructor body, `this.env = (env ??
3433 /// {}) as Record<string, ServiceBinding>;`, via `TsStmtKind::Assign`,
3434 /// added in review of #1313: the accepted proposal's own grounding
3435 /// catalogue detailed the `fetch` method's body but missed the
3436 /// constructor's one real statement, which turned out to need a
3437 /// variant Decision B's list didn't name). The leading header comment
3438 /// and the `__eventRoutes` table's own per-project literal entries are
3439 /// deliberately not included here — a representative fragment covering
3440 /// the class is exactly what the accepted proposal's own "Done when"
3441 /// allows, and this is the shape that actually exercises every new
3442 /// node kind this slice adds.
3443 #[test]
3444 fn prints_events_fanout_dos_own_class_byte_identical_to_the_real_emitter() {
3445 let real_ctor = TsClassCtor {
3446 params: vec![
3447 TsParam {
3448 name: "_state".to_string(),
3449 ty: Some(TsType::named("DurableObjectState")),
3450 optional: false,
3451 },
3452 TsParam {
3453 name: "env".to_string(),
3454 ty: Some(TsType::named("unknown")),
3455 optional: true,
3456 },
3457 ],
3458 body: vec![TsStmt::assign(
3459 TsExpr::Member {
3460 object: Box::new(TsExpr::Ident("this".to_string())),
3461 property: "env".to_string(),
3462 },
3463 TsExpr::As {
3464 expr: Box::new(TsExpr::Binary {
3465 op: TsBinaryOp::NullishCoalescing,
3466 left: Box::new(TsExpr::Ident("env".to_string())),
3467 right: Box::new(TsExpr::object(vec![])),
3468 }),
3469 ty: TsType::named_with_args(
3470 "Record",
3471 vec![TsType::named("string"), TsType::named("ServiceBinding")],
3472 ),
3473 },
3474 None,
3475 )],
3476 };
3477
3478 let fetch = TsClassMethod {
3479 name: "fetch".to_string(),
3480 private: false,
3481 is_async: true,
3482 params: vec![TsParam {
3483 name: "request".to_string(),
3484 ty: Some(TsType::named("Request")),
3485 optional: false,
3486 }],
3487 return_type: Some(TsType::named_with_args(
3488 "Promise",
3489 vec![TsType::named("Response")],
3490 )),
3491 doc: None,
3492 body: vec![
3493 TsStmt::const_stmt(
3494 TsBindingName::ObjectPattern(vec!["events".to_string()]),
3495 None,
3496 TsExpr::As {
3497 expr: Box::new(TsExpr::Paren(Box::new(TsExpr::Await(Box::new(
3498 TsExpr::Call {
3499 callee: Box::new(TsExpr::Member {
3500 object: Box::new(TsExpr::Ident("request".to_string())),
3501 property: "json".to_string(),
3502 }),
3503 args: vec![],
3504 },
3505 ))))),
3506 ty: TsType::Object(vec![TsTypeMember::prop(
3507 "events",
3508 TsType::array(TsType::named("FanoutEvent")),
3509 )]),
3510 },
3511 None,
3512 ),
3513 TsStmt::for_of(
3514 "ev",
3515 TsExpr::Ident("events".to_string()),
3516 TsStmt::block(
3517 vec![
3518 TsStmt::const_stmt(
3519 TsBindingName::Ident("subs".to_string()),
3520 None,
3521 TsExpr::Index {
3522 object: Box::new(TsExpr::Ident("__eventRoutes".to_string())),
3523 index: Box::new(TsExpr::Member {
3524 object: Box::new(TsExpr::Ident("ev".to_string())),
3525 property: "type".to_string(),
3526 }),
3527 },
3528 None,
3529 ),
3530 TsStmt::if_stmt(
3531 TsExpr::Unary {
3532 op: TsUnaryOp::Not,
3533 expr: Box::new(TsExpr::Call {
3534 callee: Box::new(TsExpr::Member {
3535 object: Box::new(TsExpr::Ident("Array".to_string())),
3536 property: "isArray".to_string(),
3537 }),
3538 args: vec![TsExpr::Ident("subs".to_string())],
3539 }),
3540 },
3541 TsStmt::continue_stmt(None),
3542 None,
3543 ),
3544 TsStmt::for_of(
3545 "sub",
3546 TsExpr::Ident("subs".to_string()),
3547 TsStmt::block(
3548 vec![
3549 TsStmt::const_stmt(
3550 TsBindingName::Ident("binding".to_string()),
3551 None,
3552 TsExpr::Index {
3553 object: Box::new(TsExpr::Member {
3554 object: Box::new(TsExpr::Ident(
3555 "this".to_string(),
3556 )),
3557 property: "env".to_string(),
3558 }),
3559 index: Box::new(TsExpr::Member {
3560 object: Box::new(TsExpr::Ident(
3561 "sub".to_string(),
3562 )),
3563 property: "binding".to_string(),
3564 }),
3565 },
3566 None,
3567 ),
3568 TsStmt::if_stmt(
3569 TsExpr::Unary {
3570 op: TsUnaryOp::Not,
3571 expr: Box::new(TsExpr::Ident(
3572 "binding".to_string(),
3573 )),
3574 },
3575 TsStmt::continue_stmt(None),
3576 None,
3577 ),
3578 TsStmt::try_catch(
3579 TsStmt::block(
3580 vec![TsStmt::expr_stmt(
3581 TsExpr::Await(Box::new(TsExpr::Call {
3582 callee: Box::new(TsExpr::Ident(
3583 "deliverEvent".to_string(),
3584 )),
3585 args: vec![
3586 TsExpr::Ident("binding".to_string()),
3587 TsExpr::Member {
3588 object: Box::new(TsExpr::Ident(
3589 "sub".to_string(),
3590 )),
3591 property: "service".to_string(),
3592 },
3593 TsExpr::Member {
3594 object: Box::new(TsExpr::Ident(
3595 "ev".to_string(),
3596 )),
3597 property: "payload".to_string(),
3598 },
3599 TsExpr::Member {
3600 object: Box::new(TsExpr::Ident(
3601 "ev".to_string(),
3602 )),
3603 property: "envelope".to_string(),
3604 },
3605 ],
3606 })),
3607 None,
3608 )],
3609 None,
3610 ),
3611 Some("e"),
3612 TsStmt::block(
3613 vec![TsStmt::expr_stmt(
3614 TsExpr::Call {
3615 callee: Box::new(TsExpr::Member {
3616 object: Box::new(TsExpr::Ident(
3617 "console".to_string(),
3618 )),
3619 property: "error".to_string(),
3620 }),
3621 args: vec![
3622 TsExpr::Lit(TsLit::Str(
3623 "EventsFanout delivery failed"
3624 .to_string(),
3625 )),
3626 TsExpr::object(vec![
3627 (
3628 "event".to_string(),
3629 TsExpr::Member {
3630 object: Box::new(
3631 TsExpr::Ident(
3632 "ev".to_string(),
3633 ),
3634 ),
3635 property: "type"
3636 .to_string(),
3637 },
3638 ),
3639 (
3640 "service".to_string(),
3641 TsExpr::Member {
3642 object: Box::new(
3643 TsExpr::Ident(
3644 "sub".to_string(),
3645 ),
3646 ),
3647 property: "service"
3648 .to_string(),
3649 },
3650 ),
3651 (
3652 "error".to_string(),
3653 TsExpr::Call {
3654 callee: Box::new(
3655 TsExpr::Ident(
3656 "String"
3657 .to_string(),
3658 ),
3659 ),
3660 args: vec![TsExpr::Ident(
3661 "e".to_string(),
3662 )],
3663 },
3664 ),
3665 ]),
3666 ],
3667 },
3668 None,
3669 )],
3670 None,
3671 ),
3672 None,
3673 ),
3674 ],
3675 None,
3676 ),
3677 None,
3678 ),
3679 ],
3680 None,
3681 ),
3682 None,
3683 ),
3684 TsStmt::return_stmt(
3685 Some(TsExpr::New {
3686 callee: Box::new(TsExpr::Ident("Response".to_string())),
3687 args: vec![
3688 TsExpr::Lit(TsLit::Null),
3689 TsExpr::object(vec![(
3690 "status".to_string(),
3691 TsExpr::Lit(TsLit::Num("204".to_string())),
3692 )]),
3693 ],
3694 }),
3695 None,
3696 ),
3697 ],
3698 };
3699
3700 let class = TsDecl::Export(Box::new(TsDecl::Class {
3701 name: "EventsFanoutDO".to_string(),
3702 fields: vec![TsClassField {
3703 name: "env".to_string(),
3704 ty: TsType::named_with_args(
3705 "Record",
3706 vec![TsType::named("string"), TsType::named("ServiceBinding")],
3707 ),
3708 private: true,
3709 }],
3710 constructor: Some(real_ctor),
3711 methods: vec![fetch],
3712 }));
3713
3714 let mut program = TsProgram::new();
3715 program.push(TsStmt::decl(class, None));
3716 let printed = print(&program, "x.bynk", "", "x.ts");
3717
3718 // Transcribed directly from `bynk-emit/src/emitter/events_fanout.rs`'s
3719 // own `write!` calls, byte-for-byte including the constructor body.
3720 let expected_lines = [
3721 "export class EventsFanoutDO {",
3722 " private env: Record<string, ServiceBinding>;",
3723 " constructor(_state: DurableObjectState, env?: unknown) {",
3724 " this.env = (env ?? {}) as Record<string, ServiceBinding>;",
3725 " }",
3726 "",
3727 " async fetch(request: Request): Promise<Response> {",
3728 " const { events } = (await request.json()) as { events: FanoutEvent[] };",
3729 " for (const ev of events) {",
3730 " const subs = __eventRoutes[ev.type];",
3731 " if (!Array.isArray(subs)) continue;",
3732 " for (const sub of subs) {",
3733 " const binding = this.env[sub.binding];",
3734 " if (!binding) continue;",
3735 " try {",
3736 " await deliverEvent(binding, sub.service, ev.payload, ev.envelope);",
3737 " } catch (e) {",
3738 " console.error(\"EventsFanout delivery failed\", { event: ev.type, service: sub.service, error: String(e) });",
3739 " }",
3740 " }",
3741 " }",
3742 " return new Response(null, { status: 204 });",
3743 " }",
3744 "}",
3745 ];
3746 let expected = format!("{}\n", expected_lines.join("\n"));
3747 assert_eq!(printed.text, expected);
3748 }
3749
3750 /// Coverage gap named in review of #1314, finding 4: `Let` had no test
3751 /// anywhere, including its distinct `init: None` branch (`Const` always
3752 /// has an initialiser; `Let` is the one new statement kind that can
3753 /// omit one).
3754 #[test]
3755 fn prints_a_let_statement_with_no_initialiser() {
3756 let mut program = TsProgram::new();
3757 program.push(TsStmt::let_stmt(
3758 TsBindingName::Ident("subs".to_string()),
3759 None,
3760 None,
3761 None,
3762 ));
3763 let printed = print(&program, "x.bynk", "", "x.ts");
3764 assert_eq!(printed.text, "let subs;\n");
3765 }
3766
3767 /// Coverage gap named in review of #1314, finding 4: `TsExpr::Array`
3768 /// was never constructed in any test.
3769 #[test]
3770 fn prints_an_array_literal() {
3771 let mut program = TsProgram::new();
3772 program.push(TsStmt::expr_stmt(
3773 TsExpr::array(vec![
3774 TsExpr::Lit(TsLit::Num("1".to_string())),
3775 TsExpr::Ident("x".to_string()),
3776 ]),
3777 None,
3778 ));
3779 let printed = print(&program, "x.bynk", "", "x.ts");
3780 assert_eq!(printed.text, "[1, x];\n");
3781 }
3782
3783 /// P7.9 (#1315): `TsType::Array`'s new `readonly` modifier — the shape
3784 /// every `List`/`Query` element type `bynk-emit`'s `ts_type_ref*`/
3785 /// `ts_ty` families build (`readonly T[]`), which `TsType::array`'s own
3786 /// `readonly: false` default cannot represent.
3787 #[test]
3788 fn print_type_renders_a_readonly_array() {
3789 assert_eq!(
3790 print_type(&TsType::readonly_array(TsType::named("Order"))),
3791 "readonly Order[]"
3792 );
3793 assert_eq!(
3794 print_type(&TsType::array(TsType::named("Order"))),
3795 "Order[]"
3796 );
3797 }
3798
3799 /// A readonly array, function, union or intersection element is
3800 /// parenthesised before `[]`; a bare `readonly readonly T[][]` is TS1354.
3801 #[test]
3802 fn print_type_parenthesises_a_non_postfix_safe_array_element() {
3803 let order = || TsType::named("Order");
3804 assert_eq!(
3805 print_type(&TsType::readonly_array(TsType::readonly_array(order()))),
3806 "readonly (readonly Order[])[]"
3807 );
3808 assert_eq!(
3809 print_type(&TsType::readonly_array(TsType::array(order()))),
3810 "readonly Order[][]"
3811 );
3812 assert_eq!(
3813 print_type(&TsType::readonly_array(TsType::Fn {
3814 params: vec![order()],
3815 ret: Box::new(order()),
3816 })),
3817 "readonly ((a0: Order) => Order)[]"
3818 );
3819 assert_eq!(
3820 print_type(&TsType::array(TsType::Union {
3821 members: vec![order(), TsType::named("null")],
3822 multiline: false,
3823 })),
3824 "(Order | null)[]"
3825 );
3826 assert_eq!(
3827 print_type(&TsType::array(TsType::Intersection(vec![
3828 order(),
3829 TsType::named("Tagged"),
3830 ]))),
3831 "(Order & Tagged)[]"
3832 );
3833 }
3834
3835 /// P7.9 (#1315): `TsType::Fn`'s zero-`params` form — the query-thunk
3836 /// wrapper shape, `bynk-emit`'s own `(() => readonly T[])`.
3837 #[test]
3838 fn print_type_renders_a_zero_param_function_type() {
3839 let ty = TsType::Fn {
3840 params: vec![],
3841 ret: Box::new(TsType::readonly_array(TsType::named("Order"))),
3842 };
3843 assert_eq!(print_type(&ty), "() => readonly Order[]");
3844 }
3845
3846 /// P7.9 (#1315): `TsType::Fn`'s real parametered form — positional
3847 /// `a0`/`a1`/… names, matching `bynk-emit`'s own pre-P7.9 convention
3848 /// exactly (TypeScript requires *some* name in function-type syntax).
3849 #[test]
3850 fn print_type_renders_a_parametered_function_type_with_positional_names() {
3851 let ty = TsType::Fn {
3852 params: vec![TsType::named("string"), TsType::named("number")],
3853 ret: Box::new(TsType::named("void")),
3854 };
3855 assert_eq!(print_type(&ty), "(a0: string, a1: number) => void");
3856 }
3857
3858 /// `TsType::Union`, added during review of #1315's own implementation —
3859 /// a real gap `bynk-emit`'s `ts_ty` needed for a resolved multi-actor
3860 /// sum (`Ty::ActorSum`), beyond the accepted proposal's own `readonly`/
3861 /// `Fn` gap list. Members print `" | "`-joined, in order.
3862 #[test]
3863 fn print_type_renders_a_union_of_named_types() {
3864 let ty = TsType::union(vec![
3865 TsType::named("string"),
3866 TsType::named("number"),
3867 TsType::Object(vec![TsTypeMember::prop(
3868 "tag",
3869 TsType::named("\"literal\""),
3870 )]),
3871 ]);
3872 assert_eq!(print_type(&ty), "string | number | { tag: \"literal\" }");
3873 }
3874
3875 /// `TsStmtKind::Comment`, added for Arc C's own first real conversion
3876 /// slice (#1317). A multi-line comment (embedded `\n`) prints one
3877 /// `// `-prefixed line per real line.
3878 #[test]
3879 fn prints_a_comment_one_prefixed_line_per_embedded_newline() {
3880 let mut program = TsProgram::new();
3881 program.push(TsStmt::comment(
3882 "Generated by bynkc — do not edit by hand.\nSecond line.",
3883 None,
3884 ));
3885 let printed = print(&program, "x.bynk", "", "x.ts");
3886 assert_eq!(
3887 printed.text,
3888 "// Generated by bynkc — do not edit by hand.\n// Second line.\n"
3889 );
3890 }
3891
3892 /// Two adjacent top-level `Comment` statements get no blank line
3893 /// between them — the same exception already established for two
3894 /// adjacent `import`s, matching `events_fanout.rs`'s own real two-line
3895 /// header banner (built as two separate `Comment` statements, not one
3896 /// with an embedded newline).
3897 #[test]
3898 fn no_blank_line_between_two_adjacent_comment_statements() {
3899 let mut program = TsProgram::new();
3900 program.push(TsStmt::comment(
3901 "Generated by bynkc — do not edit by hand.",
3902 None,
3903 ));
3904 program.push(TsStmt::comment("Second line.", None));
3905 program.push(TsStmt::decl(
3906 TsDecl::Interface {
3907 name: "A".to_string(),
3908 type_params: Vec::new(),
3909 members: vec![],
3910 },
3911 None,
3912 ));
3913 let printed = print(&program, "x.bynk", "", "x.ts");
3914 assert_eq!(
3915 printed.text,
3916 "// Generated by bynkc — do not edit by hand.\n// Second line.\n\ninterface A {\n}\n"
3917 );
3918 }
3919
3920 /// `TsExpr::multiline_object`, added for Arc C's own first real
3921 /// conversion slice (#1317): `events_fanout.rs`'s own `__eventRoutes`
3922 /// table is a top-level `const` initialiser with one entry per line,
3923 /// each with its own trailing comma (including the last), closing
3924 /// brace back at the statement's own indent — TypeScript's ordinary
3925 /// multi-line object-literal convention, which `TsExpr::object`'s
3926 /// single-line form cannot represent. Only reachable through a
3927 /// statement/declaration-level renderer that carries `depth`
3928 /// (`render_stmt_level_expr`) — this exercises it via a top-level
3929 /// `ConstDecl`, the real shape `events_fanout.rs` itself uses.
3930 #[test]
3931 fn prints_a_multiline_object_as_a_top_level_const_initialiser() {
3932 let mut program = TsProgram::new();
3933 program.push(TsStmt::decl(
3934 TsDecl::ConstDecl {
3935 name: "table".to_string(),
3936 ty: None,
3937 init: TsExpr::multiline_object(vec![
3938 ("a".to_string(), TsExpr::Lit(TsLit::Num("1".to_string()))),
3939 ("b".to_string(), TsExpr::Lit(TsLit::Num("2".to_string()))),
3940 ]),
3941 },
3942 None,
3943 ));
3944 let printed = print(&program, "x.bynk", "", "x.ts");
3945 assert_eq!(printed.text, "const table = {\n a: 1,\n b: 2,\n};\n");
3946 }
3947
3948 /// Review of #1317/#1318, finding 1: an empty `multiline_object` still
3949 /// prints the open/close-brace-on-separate-lines shape, not the tight
3950 /// `{}` a single-line empty object uses — matching the pre-conversion
3951 /// `writeln!` code's own real behaviour (its `for` loop simply not
3952 /// iterating, while the open-brace and closing `};` lines were written
3953 /// unconditionally either way). This is a real, reachable shape:
3954 /// `events_fanout.rs`'s own `__eventRoutes` table can be empty for a
3955 /// context that publishes only events nobody subscribes to.
3956 #[test]
3957 fn an_empty_multiline_object_still_prints_the_open_and_close_brace_on_separate_lines() {
3958 let mut program = TsProgram::new();
3959 program.push(TsStmt::decl(
3960 TsDecl::ConstDecl {
3961 name: "table".to_string(),
3962 ty: None,
3963 init: TsExpr::multiline_object(vec![]),
3964 },
3965 None,
3966 ));
3967 let printed = print(&program, "x.bynk", "", "x.ts");
3968 assert_eq!(printed.text, "const table = {\n};\n");
3969 }
3970
3971 /// A `multiline_object` reached through the ordinary, depth-unaware
3972 /// `render_expr` recursion (nested inside another expression, not a
3973 /// statement's own top-level initialiser) falls back to single-line
3974 /// rendering — documented on `TsExpr::Object` itself as a real,
3975 /// deliberate boundary, not silently wrong. Pinned here so a future
3976 /// change to that boundary is a visible, intentional decision.
3977 #[test]
3978 fn a_nested_multiline_object_falls_back_to_single_line() {
3979 let mut program = TsProgram::new();
3980 program.push(TsStmt::expr_stmt(
3981 TsExpr::array(vec![TsExpr::multiline_object(vec![(
3982 "a".to_string(),
3983 TsExpr::Lit(TsLit::Num("1".to_string())),
3984 )])]),
3985 None,
3986 ));
3987 let printed = print(&program, "x.bynk", "", "x.ts");
3988 assert_eq!(printed.text, "[{ a: 1 }];\n");
3989 }
3990
3991 /// Coverage gap named in review of #1314, finding 4: the blank-line-
3992 /// between-top-level-declarations policy was never tested for the
3993 /// mixed `Verbatim` + real-`Decl` case — the case that actually keeps
3994 /// every P7.6 fixture's zero-diff claim true today, since every P7.6
3995 /// construction site is all-`Verbatim`. Pins both halves of the rule's
3996 /// asymmetry: no blank line is added *after* a `Verbatim` statement
3997 /// (P7.7's own boundary — its content's own trailing spacing isn't the
3998 /// printer's decision), but a blank line *is* added after a real
3999 /// `Decl` even when `Verbatim` follows it.
4000 #[test]
4001 fn no_blank_line_after_verbatim_but_one_before_it() {
4002 let mut program = TsProgram::new();
4003 program.push(TsStmt::verbatim(
4004 VerbatimOrigin::Contracts,
4005 "const legacy = 1;\n",
4006 None,
4007 ));
4008 program.push(TsStmt::decl(
4009 TsDecl::Interface {
4010 name: "A".to_string(),
4011 type_params: Vec::new(),
4012 members: vec![],
4013 },
4014 None,
4015 ));
4016 program.push(TsStmt::verbatim(
4017 VerbatimOrigin::Secrets,
4018 "const trailing = 2;\n",
4019 None,
4020 ));
4021 let printed = print(&program, "x.bynk", "", "x.ts");
4022 assert_eq!(
4023 printed.text,
4024 "const legacy = 1;\ninterface A {\n}\n\nconst trailing = 2;\n"
4025 );
4026 }
4027
4028 /// Coverage gap named in review of #1314, finding 4: nothing exercised
4029 /// `render_block_body`'s own graceful-degradation path — its doc
4030 /// comment promises a single non-`Block` statement still renders
4031 /// sensibly as a one-statement body rather than panicking, but no test
4032 /// ever passed one. `TryCatch` is the real caller that could hit this
4033 /// (its `try_block`/`catch_block` are typed as a bare `TsStmt`, not
4034 /// required to be a `Block`).
4035 #[test]
4036 fn a_try_block_that_is_not_a_block_statement_still_prints_as_a_braced_body() {
4037 let mut program = TsProgram::new();
4038 program.push(TsStmt::try_catch(
4039 TsStmt::expr_stmt(
4040 TsExpr::Call {
4041 callee: Box::new(TsExpr::Ident("risky".to_string())),
4042 args: vec![],
4043 },
4044 None,
4045 ),
4046 Some("e"),
4047 TsStmt::expr_stmt(
4048 TsExpr::Call {
4049 callee: Box::new(TsExpr::Ident("handle".to_string())),
4050 args: vec![TsExpr::Ident("e".to_string())],
4051 },
4052 None,
4053 ),
4054 None,
4055 ));
4056 let printed = print(&program, "x.bynk", "", "x.ts");
4057 assert_eq!(
4058 printed.text,
4059 "try {\n risky();\n} catch (e) {\n handle(e);\n}\n"
4060 );
4061 }
4062
4063 /// Coverage gap named in review of #1314, finding 4: `render_inline_stmt`'s
4064 /// shared fallback arm (finding 3's fix — explicit variants, not a
4065 /// wildcard) was never actually exercised by a test; every existing
4066 /// brace-free `if`/`for...of` body used `Continue`/`Return`/`ExprStmt`,
4067 /// which each have their own dedicated inline arm.
4068 #[test]
4069 fn a_brace_free_if_body_that_is_not_one_of_the_dedicated_inline_kinds_still_prints() {
4070 let mut program = TsProgram::new();
4071 program.push(TsStmt::if_stmt(
4072 TsExpr::Ident("cond".to_string()),
4073 TsStmt::const_stmt(
4074 TsBindingName::Ident("x".to_string()),
4075 None,
4076 TsExpr::Lit(TsLit::Num("1".to_string())),
4077 None,
4078 ),
4079 None,
4080 ));
4081 let printed = print(&program, "x.bynk", "", "x.ts");
4082 assert_eq!(printed.text, "if (cond) const x = 1;\n");
4083 }
4084
4085 /// Review of #1314, finding 2: nothing checked operator-precedence
4086 /// parenthesisation outside `As` — a `Binary` nested as the operand of
4087 /// `Unary`/`Member`/`Await` printed with no parens at all, silently
4088 /// changing what the text parses back to. Three of the review's own
4089 /// examples, pinned directly.
4090 #[test]
4091 fn parenthesises_a_binary_operand_of_unary_member_and_await() {
4092 let bin = || TsExpr::Binary {
4093 op: TsBinaryOp::NullishCoalescing,
4094 left: Box::new(TsExpr::Ident("a".to_string())),
4095 right: Box::new(TsExpr::Ident("b".to_string())),
4096 };
4097
4098 let mut not_program = TsProgram::new();
4099 not_program.push(TsStmt::expr_stmt(
4100 TsExpr::Unary {
4101 op: TsUnaryOp::Not,
4102 expr: Box::new(bin()),
4103 },
4104 None,
4105 ));
4106 assert_eq!(
4107 print(¬_program, "x.bynk", "", "x.ts").text,
4108 "!(a ?? b);\n"
4109 );
4110
4111 let mut member_program = TsProgram::new();
4112 member_program.push(TsStmt::expr_stmt(
4113 TsExpr::Member {
4114 object: Box::new(bin()),
4115 property: "c".to_string(),
4116 },
4117 None,
4118 ));
4119 assert_eq!(
4120 print(&member_program, "x.bynk", "", "x.ts").text,
4121 "(a ?? b).c;\n"
4122 );
4123
4124 let mut await_program = TsProgram::new();
4125 await_program.push(TsStmt::expr_stmt(TsExpr::Await(Box::new(bin())), None));
4126 assert_eq!(
4127 print(&await_program, "x.bynk", "", "x.ts").text,
4128 "await (a ?? b);\n"
4129 );
4130 }
4131
4132 /// Review of #1314, finding 2's fourth example: a right-nested `Binary`
4133 /// (`a ?? (b ?? c)`, as built) must not print as the bare `a ?? b ?? c`
4134 /// that a naive left-to-right walk would produce — that text parses
4135 /// back as `(a ?? b) ?? c`, a different tree than what was built.
4136 #[test]
4137 fn parenthesises_a_nested_binary_operand_of_another_binary() {
4138 let mut program = TsProgram::new();
4139 program.push(TsStmt::expr_stmt(
4140 TsExpr::Binary {
4141 op: TsBinaryOp::NullishCoalescing,
4142 left: Box::new(TsExpr::Ident("a".to_string())),
4143 right: Box::new(TsExpr::Binary {
4144 op: TsBinaryOp::NullishCoalescing,
4145 left: Box::new(TsExpr::Ident("b".to_string())),
4146 right: Box::new(TsExpr::Ident("c".to_string())),
4147 }),
4148 },
4149 None,
4150 ));
4151 let printed = print(&program, "x.bynk", "", "x.ts");
4152 assert_eq!(printed.text, "a ?? (b ?? c);\n");
4153 }
4154
4155 /// Review of #1314, finding 1: `TsLit::Str` escaped only `"`/`\`, so a
4156 /// literal containing a newline, tab, or carriage return printed raw —
4157 /// unterminated-string-literal TypeScript `tsc` can't parse. Pins the
4158 /// fix against every character `bynk_check::wire_default::
4159 /// escape_ts_literal` escapes.
4160 #[test]
4161 fn escapes_newline_tab_and_carriage_return_in_a_string_literal() {
4162 let mut program = TsProgram::new();
4163 program.push(TsStmt::expr_stmt(
4164 TsExpr::Lit(TsLit::Str("line one\nline two\ttabbed\r".to_string())),
4165 None,
4166 ));
4167 let printed = print(&program, "x.bynk", "", "x.ts");
4168 assert_eq!(printed.text, "\"line one\\nline two\\ttabbed\\r\";\n");
4169 }
4170
4171 // -- Arc C slice 3 (#1321, `workers.rs`): the five new shapes Decision A
4172 // names, plus the real gaps found beyond it (`TsDecl::Function`/
4173 // `TsDecl::TypeAlias`/`TsDecl::ImportNamespace`, the parameterless
4174 // `catch {}` form, and the two new `TsUnaryOp`/four new `TsBinaryOp`
4175 // operators). --
4176
4177 #[test]
4178 fn prints_a_shorthand_async_method_entry_in_a_multiline_object() {
4179 let mut program = TsProgram::new();
4180 program.push(TsStmt::return_stmt(
4181 Some(TsExpr::multiline_object_entries(vec![
4182 TsObjectEntry::Method {
4183 name: "foo".to_string(),
4184 is_async: true,
4185 generics: Vec::new(),
4186 params: vec![TsParam {
4187 name: "a".to_string(),
4188 ty: Some(TsType::named("string")),
4189 optional: false,
4190 }],
4191 return_type: None,
4192 doc: None,
4193 inline: false,
4194 body: vec![TsStmt::return_stmt(
4195 Some(TsExpr::Ident("a".to_string())),
4196 None,
4197 )],
4198 },
4199 ])),
4200 None,
4201 ));
4202 let printed = print(&program, "x.bynk", "", "x.ts");
4203 assert_eq!(
4204 printed.text,
4205 "return {\n async foo(a: string) {\n return a;\n },\n};\n"
4206 );
4207 }
4208
4209 #[test]
4210 fn prints_shorthand_and_spread_entries_inline() {
4211 let mut program = TsProgram::new();
4212 program.push(TsStmt::expr_stmt(
4213 TsExpr::object_entries(vec![
4214 TsObjectEntry::Spread(TsExpr::Ident("deps".to_string())),
4215 TsObjectEntry::Shorthand("cap1".to_string()),
4216 TsObjectEntry::Prop(
4217 "identity".to_string(),
4218 TsExpr::Ident("__caller".to_string()),
4219 ),
4220 ]),
4221 None,
4222 ));
4223 let printed = print(&program, "x.bynk", "", "x.ts");
4224 assert_eq!(printed.text, "{ ...deps, cap1, identity: __caller };\n");
4225 }
4226
4227 #[test]
4228 fn prints_an_expression_bodied_arrow() {
4229 let mut program = TsProgram::new();
4230 program.push(TsStmt::expr_stmt(
4231 TsExpr::Arrow {
4232 params: vec![TsParam {
4233 name: "events".to_string(),
4234 ty: Some(TsType::named_with_args(
4235 "Array",
4236 vec![TsType::named("Wire")],
4237 )),
4238 optional: false,
4239 }],
4240 is_async: false,
4241 generics: Vec::new(),
4242 return_type: None,
4243 body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Call {
4244 callee: Box::new(TsExpr::Ident("dispatch".to_string())),
4245 args: vec![TsExpr::Ident("events".to_string())],
4246 }))),
4247 },
4248 None,
4249 ));
4250 let printed = print(&program, "x.bynk", "", "x.ts");
4251 assert_eq!(printed.text, "(events: Array<Wire>) => dispatch(events);\n");
4252 }
4253
4254 /// #1327's own real gap: `emit_composition_root`'s `__eventsDispatch`
4255 /// closure is the first real `Arrow` site that's async.
4256 #[test]
4257 fn prints_an_async_arrow() {
4258 let mut program = TsProgram::new();
4259 program.push(TsStmt::expr_stmt(
4260 TsExpr::Arrow {
4261 params: vec![TsParam {
4262 name: "events".to_string(),
4263 ty: Some(TsType::named_with_args(
4264 "Array",
4265 vec![TsType::named("Wire")],
4266 )),
4267 optional: false,
4268 }],
4269 is_async: true,
4270 generics: Vec::new(),
4271 return_type: None,
4272 body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Ident(
4273 "{ dispatch(events); }".to_string(),
4274 )))),
4275 },
4276 None,
4277 ));
4278 let printed = print(&program, "x.bynk", "", "x.ts");
4279 assert_eq!(
4280 printed.text,
4281 "async (events: Array<Wire>) => { dispatch(events); };\n"
4282 );
4283 }
4284
4285 /// #1435 (Arc E slice 1): [`TsArrowBody::Block`], the first real
4286 /// statement-bodied arrow — pins the exact `serialisation.rs` `Float`
4287 /// non-finite guard shape (`serialise_field_expr_wire`'s own
4288 /// `WireRef::Base { base: Float, .. }` arm) directly against a hand-built
4289 /// tree, the same way #1322's own three parenthesisation tests below pin
4290 /// their own gap. Proves `render_arrow_body`'s `Block` arm reuses
4291 /// `render_compact_stmts` (an `If` with a brace-free `Throw` then-branch,
4292 /// followed by a `Return`) correctly: both statements land on the SAME
4293 /// physical line as the arrow's own `{ ... }`, with no trailing newline
4294 /// leaking out of the block into the enclosing call's own `;`.
4295 #[test]
4296 fn prints_a_block_bodied_arrow_iife() {
4297 let mut program = TsProgram::new();
4298 let guard = TsExpr::Call {
4299 callee: Box::new(TsExpr::Arrow {
4300 params: vec![TsParam {
4301 name: "v".to_string(),
4302 ty: Some(TsType::named("number")),
4303 optional: false,
4304 }],
4305 is_async: false,
4306 generics: Vec::new(),
4307 return_type: None,
4308 body: Box::new(TsArrowBody::Block(vec![
4309 TsStmt::if_stmt(
4310 TsExpr::Unary {
4311 op: TsUnaryOp::Not,
4312 expr: Box::new(TsExpr::Call {
4313 callee: Box::new(TsExpr::Member {
4314 object: Box::new(TsExpr::Ident("Number".to_string())),
4315 property: "isFinite".to_string(),
4316 }),
4317 args: vec![TsExpr::Ident("v".to_string())],
4318 }),
4319 },
4320 TsStmt::throw_stmt(
4321 TsExpr::New {
4322 callee: Box::new(TsExpr::Ident("Error".to_string())),
4323 args: vec![TsExpr::Lit(TsLit::Str(
4324 "non-finite Float at boundary".to_string(),
4325 ))],
4326 },
4327 None,
4328 ),
4329 None,
4330 ),
4331 TsStmt::return_stmt(
4332 Some(TsExpr::As {
4333 expr: Box::new(TsExpr::Ident("v".to_string())),
4334 ty: TsType::named("JsonValue"),
4335 }),
4336 None,
4337 ),
4338 ])),
4339 }),
4340 args: vec![TsExpr::Ident("value".to_string())],
4341 };
4342 program.push(TsStmt::expr_stmt(guard, None));
4343 let printed = print(&program, "x.bynk", "", "x.ts");
4344 assert_eq!(
4345 printed.text,
4346 "((v: number) => { if (!Number.isFinite(v)) throw new Error(\"non-finite Float at boundary\"); return v as JsonValue; })(value);\n"
4347 );
4348 }
4349
4350 /// Review of #1322, finding 1: `Arrow` was missing from every
4351 /// parenthesisation rule. Not reachable through any real `bynk-emit`
4352 /// content today (the one grounded `Arrow` site is an object-property
4353 /// value, rendered through the depth-unaware `render_expr` with no
4354 /// operand context) — these three tests pin the fix directly against
4355 /// hand-built trees, the same way #1314's own precedence tests did
4356 /// before any real content exercised them.
4357 #[test]
4358 fn parenthesises_an_arrow_used_as_a_call_callee() {
4359 let mut program = TsProgram::new();
4360 program.push(TsStmt::expr_stmt(
4361 TsExpr::Call {
4362 callee: Box::new(TsExpr::Arrow {
4363 params: vec![TsParam {
4364 name: "x".to_string(),
4365 ty: None,
4366 optional: false,
4367 }],
4368 is_async: false,
4369 generics: Vec::new(),
4370 return_type: None,
4371 body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Ident("x".to_string())))),
4372 }),
4373 args: vec![TsExpr::Lit(TsLit::Num("1".to_string()))],
4374 },
4375 None,
4376 ));
4377 let printed = print(&program, "x.bynk", "", "x.ts");
4378 assert_eq!(printed.text, "((x) => x)(1);\n");
4379 }
4380
4381 #[test]
4382 fn parenthesises_an_arrow_used_as_a_binary_operand() {
4383 let mut program = TsProgram::new();
4384 program.push(TsStmt::expr_stmt(
4385 TsExpr::Binary {
4386 op: TsBinaryOp::NullishCoalescing,
4387 left: Box::new(TsExpr::Ident("a".to_string())),
4388 right: Box::new(TsExpr::Arrow {
4389 params: vec![TsParam {
4390 name: "x".to_string(),
4391 ty: None,
4392 optional: false,
4393 }],
4394 is_async: false,
4395 generics: Vec::new(),
4396 return_type: None,
4397 body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Ident("y".to_string())))),
4398 }),
4399 },
4400 None,
4401 ));
4402 let printed = print(&program, "x.bynk", "", "x.ts");
4403 assert_eq!(printed.text, "a ?? ((x) => y);\n");
4404 }
4405
4406 #[test]
4407 fn parenthesises_an_arrow_used_as_an_as_operand() {
4408 let mut program = TsProgram::new();
4409 program.push(TsStmt::expr_stmt(
4410 TsExpr::As {
4411 expr: Box::new(TsExpr::Arrow {
4412 params: vec![TsParam {
4413 name: "x".to_string(),
4414 ty: None,
4415 optional: false,
4416 }],
4417 is_async: false,
4418 generics: Vec::new(),
4419 return_type: None,
4420 body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Ident("y".to_string())))),
4421 }),
4422 ty: TsType::named("Handler"),
4423 },
4424 None,
4425 ));
4426 let printed = print(&program, "x.bynk", "", "x.ts");
4427 assert_eq!(printed.text, "((x) => y) as Handler;\n");
4428 }
4429
4430 #[test]
4431 fn prints_optional_member_and_optional_index_chained() {
4432 let mut program = TsProgram::new();
4433 program.push(TsStmt::expr_stmt(
4434 TsExpr::OptionalIndex {
4435 object: Box::new(TsExpr::OptionalMember {
4436 object: Box::new(TsExpr::Ident("x".to_string())),
4437 property: "process".to_string(),
4438 }),
4439 index: Box::new(TsExpr::Lit(TsLit::Str("secret".to_string()))),
4440 },
4441 None,
4442 ));
4443 let printed = print(&program, "x.bynk", "", "x.ts");
4444 assert_eq!(printed.text, "x?.process?.[\"secret\"];\n");
4445 }
4446
4447 #[test]
4448 fn print_type_renders_an_optional_object_field() {
4449 let ty = TsType::Object(vec![
4450 TsTypeMember::optional_prop("env", TsType::named("unknown")),
4451 TsTypeMember::prop("name", TsType::named("string")),
4452 ]);
4453 assert_eq!(print_type(&ty), "{ env?: unknown; name: string }");
4454 }
4455
4456 #[test]
4457 fn prints_a_top_level_function_declaration() {
4458 let mut program = TsProgram::new();
4459 program.push(TsStmt::decl(
4460 TsDecl::Export(Box::new(TsDecl::Function {
4461 name: "compose".to_string(),
4462 generics: Vec::new(),
4463 params: vec![TsParam {
4464 name: "env".to_string(),
4465 ty: Some(TsType::named("Env")),
4466 optional: false,
4467 }],
4468 return_type: None,
4469 body: vec![TsStmt::return_stmt(Some(TsExpr::Lit(TsLit::Null)), None)],
4470 is_async: false,
4471 inline: false,
4472 })),
4473 None,
4474 ));
4475 let printed = print(&program, "x.bynk", "", "x.ts");
4476 assert_eq!(
4477 printed.text,
4478 "export function compose(env: Env) {\n return null;\n}\n"
4479 );
4480 }
4481
4482 /// #1351's own real gap: `TsDecl::Function` had no `generics` field —
4483 /// `emit_free_fn`'s own v0.20a erased generics (`export function
4484 /// foo<A, B>(...)`) needed one. Review of #1352, finding 3: every other
4485 /// existing `TsDecl::Function` test only threads `generics: Vec::new()`
4486 /// through; this pins the non-empty case directly, matching #1339's own
4487 /// precedent of a dedicated test per new field.
4488 #[test]
4489 fn prints_a_generic_top_level_function_declaration() {
4490 let mut program = TsProgram::new();
4491 program.push(TsStmt::decl(
4492 TsDecl::Export(Box::new(TsDecl::Function {
4493 name: "identity".to_string(),
4494 generics: vec!["A".to_string(), "B".to_string()],
4495 params: vec![TsParam {
4496 name: "x".to_string(),
4497 ty: Some(TsType::named("A")),
4498 optional: false,
4499 }],
4500 return_type: Some(TsType::named("A")),
4501 body: vec![TsStmt::return_stmt(
4502 Some(TsExpr::Ident("x".to_string())),
4503 None,
4504 )],
4505 is_async: false,
4506 inline: false,
4507 })),
4508 None,
4509 ));
4510 let printed = print(&program, "x.bynk", "", "x.ts");
4511 assert_eq!(
4512 printed.text,
4513 "export function identity<A, B>(x: A): A {\n return x;\n}\n"
4514 );
4515 }
4516
4517 #[test]
4518 fn prints_a_top_level_type_alias() {
4519 let mut program = TsProgram::new();
4520 program.push(TsStmt::decl(
4521 TsDecl::TypeAlias {
4522 name: "Foo".to_string(),
4523 type_params: Vec::new(),
4524 ty: TsType::named("{ get(id: any): any }"),
4525 },
4526 None,
4527 ));
4528 let printed = print(&program, "x.bynk", "", "x.ts");
4529 assert_eq!(printed.text, "type Foo = { get(id: any): any };\n");
4530 }
4531
4532 #[test]
4533 fn prints_a_namespace_import_adjacent_to_a_named_import_with_no_blank_line() {
4534 let mut program = TsProgram::new();
4535 program.push(TsStmt::decl(
4536 TsDecl::Import {
4537 type_only: false,
4538 names: vec!["a".to_string()],
4539 from: "./x.js".to_string(),
4540 },
4541 None,
4542 ));
4543 program.push(TsStmt::decl(
4544 TsDecl::ImportNamespace {
4545 type_only: false,
4546 alias: "handlers".to_string(),
4547 from: "./handlers.js".to_string(),
4548 },
4549 None,
4550 ));
4551 let printed = print(&program, "x.bynk", "", "x.ts");
4552 assert_eq!(
4553 printed.text,
4554 "import { a } from \"./x.js\";\nimport * as handlers from \"./handlers.js\";\n"
4555 );
4556 }
4557
4558 /// Arc C, step (10) (#1392): `ImportNamespace.type_only`, a parallel gap
4559 /// by omission — `TsDecl::Import` already had this field, but nothing
4560 /// before `emit_cross_context_namespace_imports`'s own conversion built
4561 /// a type-only namespace import (`import type * as ns from "...";`, a
4562 /// Workers-mode consumed-context import reaching the callee's types
4563 /// only, #661).
4564 #[test]
4565 fn prints_a_type_only_namespace_import() {
4566 let mut program = TsProgram::new();
4567 program.push(TsStmt::decl(
4568 TsDecl::ImportNamespace {
4569 type_only: true,
4570 alias: "commerce_payment".to_string(),
4571 from: "../commerce-payment/handlers.js".to_string(),
4572 },
4573 None,
4574 ));
4575 let printed = print(&program, "x.bynk", "", "x.ts");
4576 assert_eq!(
4577 printed.text,
4578 "import type * as commerce_payment from \"../commerce-payment/handlers.js\";\n"
4579 );
4580 }
4581
4582 /// Arc C, slice 37 (#1409, `tests_emit.rs`'s own slice G): `emit_integration_
4583 /// module`'s own per-participant `import worker_{ns} from "../workers/{dir}/
4584 /// index.js";` (the participant's Worker entry module's default export) —
4585 /// the first real default import anywhere in `bynk-emit`'s own converted
4586 /// content.
4587 #[test]
4588 fn prints_a_default_import() {
4589 let mut program = TsProgram::new();
4590 program.push(TsStmt::decl(
4591 TsDecl::ImportDefault {
4592 alias: "worker_shop_api".to_string(),
4593 from: "../workers/shop-api/index.js".to_string(),
4594 },
4595 None,
4596 ));
4597 let printed = print(&program, "x.bynk", "", "x.ts");
4598 assert_eq!(
4599 printed.text,
4600 "import worker_shop_api from \"../workers/shop-api/index.js\";\n"
4601 );
4602 }
4603
4604 /// #1321's own real gap beyond the accepted proposal's five: ES2019's
4605 /// optional catch binding (`emit_http_sum_wrapper`'s own `} catch {`,
4606 /// no `(e)` at all).
4607 #[test]
4608 fn prints_a_parameterless_catch() {
4609 let mut program = TsProgram::new();
4610 program.push(TsStmt::try_catch(
4611 TsStmt::block(vec![], None),
4612 None::<String>,
4613 TsStmt::block(vec![], None),
4614 None,
4615 ));
4616 let printed = print(&program, "x.bynk", "", "x.ts");
4617 assert_eq!(printed.text, "try {\n} catch {\n}\n");
4618 }
4619
4620 #[test]
4621 fn prints_typeof_and_the_new_comparison_and_logical_operators() {
4622 let mut program = TsProgram::new();
4623 program.push(TsStmt::expr_stmt(
4624 TsExpr::Unary {
4625 op: TsUnaryOp::Typeof,
4626 expr: Box::new(TsExpr::Ident("x".to_string())),
4627 },
4628 None,
4629 ));
4630 program.push(TsStmt::expr_stmt(
4631 TsExpr::Binary {
4632 op: TsBinaryOp::StrictEq,
4633 left: Box::new(TsExpr::Ident("a".to_string())),
4634 right: Box::new(TsExpr::Ident("b".to_string())),
4635 },
4636 None,
4637 ));
4638 program.push(TsStmt::expr_stmt(
4639 TsExpr::Binary {
4640 op: TsBinaryOp::StrictNotEq,
4641 left: Box::new(TsExpr::Ident("a".to_string())),
4642 right: Box::new(TsExpr::Ident("b".to_string())),
4643 },
4644 None,
4645 ));
4646 program.push(TsStmt::expr_stmt(
4647 TsExpr::Binary {
4648 op: TsBinaryOp::And,
4649 left: Box::new(TsExpr::Ident("a".to_string())),
4650 right: Box::new(TsExpr::Ident("b".to_string())),
4651 },
4652 None,
4653 ));
4654 program.push(TsStmt::expr_stmt(
4655 TsExpr::Binary {
4656 op: TsBinaryOp::Or,
4657 left: Box::new(TsExpr::Ident("a".to_string())),
4658 right: Box::new(TsExpr::Ident("b".to_string())),
4659 },
4660 None,
4661 ));
4662 let printed = print(&program, "x.bynk", "", "x.ts");
4663 assert_eq!(
4664 printed.text,
4665 "typeof x;\n\na === b;\n\na !== b;\n\na && b;\n\na || b;\n"
4666 );
4667 }
4668
4669 /// #1321's own real gap: with only `??` in the algebra, a nested
4670 /// `Binary` operand of another `Binary` was always parenthesized,
4671 /// regardless of operator (see `parenthesises_a_nested_binary_operand_
4672 /// of_another_binary`, unchanged). `workers.rs`'s own real content
4673 /// nests a *strictly higher precedence* comparison inside `||`/`&&`
4674 /// (`__authz === null || !__authz.startsWith(...)`) and the byte-golden
4675 /// fixtures have no parens around it — this pins that the printer
4676 /// omits them precisely in that case, not more broadly.
4677 #[test]
4678 fn a_strictly_higher_precedence_comparison_needs_no_parens_inside_or_or_and() {
4679 let mut program = TsProgram::new();
4680 program.push(TsStmt::expr_stmt(
4681 TsExpr::Binary {
4682 op: TsBinaryOp::Or,
4683 left: Box::new(TsExpr::Binary {
4684 op: TsBinaryOp::StrictEq,
4685 left: Box::new(TsExpr::Ident("a".to_string())),
4686 right: Box::new(TsExpr::Lit(TsLit::Null)),
4687 }),
4688 right: Box::new(TsExpr::Unary {
4689 op: TsUnaryOp::Not,
4690 expr: Box::new(TsExpr::Ident("b".to_string())),
4691 }),
4692 },
4693 None,
4694 ));
4695 let printed = print(&program, "x.bynk", "", "x.ts");
4696 assert_eq!(printed.text, "a === null || !b;\n");
4697 }
4698
4699 // -- #1323 (workers_entry.rs): Switch, ExportDefault, Conditional,
4700 // TsType::Object readonly/method members, and the smaller mechanical
4701 // gaps found during implementation (ReExport, Blank, TsLit::Bool,
4702 // TsExpr::Paren, same-operator-chain flattening). --
4703
4704 /// #1323's own largest gap: `switch (<discriminant>) { <cases> }`. A
4705 /// non-`default` `case` is `{ }`-blocked; `default` is not — pinned
4706 /// against `workers_entry.rs`'s own real internal-dispatch shape.
4707 #[test]
4708 fn prints_a_switch_with_a_braced_case_and_an_unbraced_default() {
4709 let mut program = TsProgram::new();
4710 program.push(TsStmt::switch_stmt(
4711 TsExpr::Ident("servicePath".to_string()),
4712 vec![
4713 TsSwitchCase {
4714 test: Some(TsExpr::Lit(TsLit::Str("orders".to_string()))),
4715 body: vec![TsStmt::return_stmt(None, None)],
4716 default_braced: false,
4717 case_braced: true,
4718 },
4719 TsSwitchCase {
4720 test: None,
4721 body: vec![TsStmt::expr_stmt(
4722 TsExpr::Call {
4723 callee: Box::new(TsExpr::Member {
4724 object: Box::new(TsExpr::Ident("console".to_string())),
4725 property: "log".to_string(),
4726 }),
4727 args: vec![],
4728 },
4729 None,
4730 )],
4731 default_braced: false,
4732 case_braced: true,
4733 },
4734 ],
4735 None,
4736 ));
4737 let printed = print(&program, "x.bynk", "", "x.ts");
4738 assert_eq!(
4739 printed.text,
4740 "switch (servicePath) {\n case \"orders\": {\n return;\n }\n default:\n console.log();\n}\n"
4741 );
4742 }
4743
4744 /// Arc C, slice 33 (`tests_emit.rs` slice C, #1401): `TsSwitchCase.
4745 /// default_braced` — `emit_stub_class`'s own `ReturnsEach` dispatch
4746 /// braces its `default` case, unlike `workers_entry.rs`'s own unbraced
4747 /// one pinned just above.
4748 #[test]
4749 fn prints_a_switch_with_a_braced_default() {
4750 let mut program = TsProgram::new();
4751 program.push(TsStmt::switch_stmt(
4752 TsExpr::Ident("__k".to_string()),
4753 vec![TsSwitchCase {
4754 test: None,
4755 body: vec![TsStmt::expr_stmt(TsExpr::Ident("x".to_string()), None)],
4756 default_braced: true,
4757 case_braced: true,
4758 }],
4759 None,
4760 ));
4761 let printed = print(&program, "x.bynk", "", "x.ts");
4762 assert_eq!(
4763 printed.text,
4764 "switch (__k) {\n default: {\n x;\n }\n}\n"
4765 );
4766 }
4767
4768 /// Arc E slice 6 (#1445): `TsSwitchCase.case_braced` — the mirror-image
4769 /// gap `default_braced` left open. `emit_sum_codec`'s own payload-free
4770 /// variant case is unbraced right beside a braced payload-carrying
4771 /// sibling in the *same* switch, pinned against `212_json_codec`'s own
4772 /// mixed `Status` fixture (`case "Pending": return { kind: "Pending"
4773 /// };` then `case "Shipped": { ... }`).
4774 #[test]
4775 fn prints_a_switch_with_an_unbraced_case_beside_a_braced_one() {
4776 let mut program = TsProgram::new();
4777 program.push(TsStmt::switch_stmt(
4778 TsExpr::Ident("kind".to_string()),
4779 vec![
4780 TsSwitchCase {
4781 test: Some(TsExpr::Lit(TsLit::Str("Pending".to_string()))),
4782 body: vec![TsStmt::return_stmt(
4783 Some(TsExpr::Ident("a".to_string())),
4784 None,
4785 )],
4786 default_braced: false,
4787 case_braced: false,
4788 },
4789 TsSwitchCase {
4790 test: Some(TsExpr::Lit(TsLit::Str("Shipped".to_string()))),
4791 body: vec![TsStmt::return_stmt(
4792 Some(TsExpr::Ident("b".to_string())),
4793 None,
4794 )],
4795 default_braced: false,
4796 case_braced: true,
4797 },
4798 ],
4799 None,
4800 ));
4801 let printed = print(&program, "x.bynk", "", "x.ts");
4802 assert_eq!(
4803 printed.text,
4804 "switch (kind) {\n case \"Pending\":\n return a;\n case \"Shipped\": {\n return b;\n }\n}\n"
4805 );
4806 }
4807
4808 /// #1323's own second gap: `export default <expr>;` — a default export
4809 /// of a bare expression, not a declaration. Uses the same depth-aware
4810 /// multiline-object handling `const`/`let`/`return` already get.
4811 #[test]
4812 fn prints_an_export_default_object_literal_multiline() {
4813 let mut program = TsProgram::new();
4814 program.push(TsStmt::decl(
4815 TsDecl::ExportDefault(TsExpr::multiline_object(vec![(
4816 "fetch".to_string(),
4817 TsExpr::Ident("handler".to_string()),
4818 )])),
4819 None,
4820 ));
4821 let printed = print(&program, "x.bynk", "", "x.ts");
4822 assert_eq!(printed.text, "export default {\n fetch: handler,\n};\n");
4823 }
4824
4825 /// #1355's own real gap: a `multiline: true` `TsExpr::Object` nested as
4826 /// one `Prop`'s own value inside ANOTHER multiline object —
4827 /// `emit_messages_bundle`'s own real doubly-nested `{ locale: { code:
4828 /// expr, ... }, ... }` table. Before this slice, `render_multiline_
4829 /// object_entry`'s own `Prop` arm rendered its value through the plain,
4830 /// depth-unaware `render_expr`, which ignores `multiline` entirely —
4831 /// this would have silently collapsed the inner object to one line.
4832 #[test]
4833 fn prints_a_nested_multiline_object_as_a_props_own_value() {
4834 let mut program = TsProgram::new();
4835 program.push(TsStmt::const_stmt(
4836 TsBindingName::Ident("byLocale".to_string()),
4837 None,
4838 TsExpr::multiline_object_entries(vec![TsObjectEntry::Prop(
4839 "\"en\"".to_string(),
4840 TsExpr::multiline_object_entries(vec![
4841 TsObjectEntry::Prop(
4842 "\"greeting\"".to_string(),
4843 TsExpr::Ident("renderGreeting".to_string()),
4844 ),
4845 TsObjectEntry::Prop(
4846 "\"farewell\"".to_string(),
4847 TsExpr::Ident("renderFarewell".to_string()),
4848 ),
4849 ]),
4850 )]),
4851 None,
4852 ));
4853 let printed = print(&program, "x.bynk", "", "x.ts");
4854 assert_eq!(
4855 printed.text,
4856 "const byLocale = {\n \"en\": {\n \"greeting\": renderGreeting,\n \"farewell\": renderFarewell,\n },\n};\n"
4857 );
4858 }
4859
4860 /// Review of #1356, finding 2: `render_multiline_object_entry`'s own
4861 /// `Prop` arm routes through `render_stmt_level_expr` for BOTH
4862 /// `TsExpr::Object`/`Array` (see `TsExpr::Array`'s own doc, updated by
4863 /// #1355) — the object case is pinned above, this is the array sibling,
4864 /// making that doc claim self-verifying rather than merely asserted.
4865 #[test]
4866 fn prints_a_nested_multiline_array_as_a_props_own_value() {
4867 let mut program = TsProgram::new();
4868 program.push(TsStmt::const_stmt(
4869 TsBindingName::Ident("byGroup".to_string()),
4870 None,
4871 TsExpr::multiline_object_entries(vec![TsObjectEntry::Prop(
4872 "\"a\"".to_string(),
4873 TsExpr::multiline_array(vec![
4874 TsExpr::Ident("one".to_string()),
4875 TsExpr::Ident("two".to_string()),
4876 ]),
4877 )]),
4878 None,
4879 ));
4880 let printed = print(&program, "x.bynk", "", "x.ts");
4881 assert_eq!(
4882 printed.text,
4883 "const byGroup = {\n \"a\": [\n one,\n two,\n ],\n};\n"
4884 );
4885 }
4886
4887 /// #1323's own third gap: `test ? consequent : alternate`.
4888 #[test]
4889 fn prints_a_conditional_expression() {
4890 let mut program = TsProgram::new();
4891 program.push(TsStmt::expr_stmt(
4892 TsExpr::Conditional {
4893 test: Box::new(TsExpr::Binary {
4894 op: TsBinaryOp::StrictEq,
4895 left: Box::new(TsExpr::Ident("method".to_string())),
4896 right: Box::new(TsExpr::Lit(TsLit::Str("HEAD".to_string()))),
4897 }),
4898 consequent: Box::new(TsExpr::Lit(TsLit::Num("1".to_string()))),
4899 alternate: Box::new(TsExpr::Lit(TsLit::Num("2".to_string()))),
4900 },
4901 None,
4902 ));
4903 let printed = print(&program, "x.bynk", "", "x.ts");
4904 assert_eq!(printed.text, "method === \"HEAD\" ? 1 : 2;\n");
4905 }
4906
4907 /// Review-anticipated gap (matching #1322 finding 1's own class): a
4908 /// `Conditional` used as a binary operand, an `as`-operand, or a call
4909 /// callee all need parens — closed proactively in this same slice
4910 /// rather than left for a review round to re-find.
4911 #[test]
4912 fn parenthesises_a_conditional_used_as_a_binary_operand_and_an_as_operand() {
4913 let cond = || TsExpr::Conditional {
4914 test: Box::new(TsExpr::Ident("a".to_string())),
4915 consequent: Box::new(TsExpr::Ident("b".to_string())),
4916 alternate: Box::new(TsExpr::Ident("c".to_string())),
4917 };
4918 let mut program = TsProgram::new();
4919 program.push(TsStmt::expr_stmt(
4920 TsExpr::Binary {
4921 op: TsBinaryOp::NullishCoalescing,
4922 left: Box::new(TsExpr::Ident("x".to_string())),
4923 right: Box::new(cond()),
4924 },
4925 None,
4926 ));
4927 program.push(TsStmt::expr_stmt(
4928 TsExpr::As {
4929 expr: Box::new(cond()),
4930 ty: TsType::named("string"),
4931 },
4932 None,
4933 ));
4934 let printed = print(&program, "x.bynk", "", "x.ts");
4935 assert_eq!(
4936 printed.text,
4937 "x ?? (a ? b : c);\n\n(a ? b : c) as string;\n"
4938 );
4939 }
4940
4941 /// #1323's own real gap: a `Method` member of a type-position object
4942 /// literal (no body), alongside a `readonly` property.
4943 #[test]
4944 fn print_type_renders_a_readonly_field_and_a_method_signature() {
4945 let ty = TsType::Object(vec![
4946 TsTypeMember::readonly_prop("cron", TsType::named("string")),
4947 TsTypeMember::method("ack", vec![], TsType::named("void")),
4948 TsTypeMember::method(
4949 "waitUntil",
4950 vec![TsParam {
4951 name: "promise".to_string(),
4952 ty: Some(TsType::named_with_args(
4953 "Promise",
4954 vec![TsType::named("unknown")],
4955 )),
4956 optional: false,
4957 }],
4958 TsType::named("void"),
4959 ),
4960 ]);
4961 assert_eq!(
4962 print_type(&ty),
4963 "{ readonly cron: string; ack(): void; waitUntil(promise: Promise<unknown>): void }"
4964 );
4965 }
4966
4967 /// #1323's own real gap: `TsObjectEntry::Method` gained a
4968 /// `return_type` field (mirroring `TsClassMethod`'s own existing one) —
4969 /// `workers_entry.rs`'s own `export default { fetch, scheduled?,
4970 /// queue? }` entries all carry an explicit return-type annotation,
4971 /// unlike `workers.rs`'s own `compose`-returned wrapper methods
4972 /// (`#1321`), none of which do.
4973 #[test]
4974 fn prints_a_multiline_object_methods_own_return_type_annotation() {
4975 let mut program = TsProgram::new();
4976 program.push(TsStmt::decl(
4977 TsDecl::ExportDefault(TsExpr::multiline_object_entries(vec![
4978 TsObjectEntry::Method {
4979 name: "fetch".to_string(),
4980 is_async: true,
4981 generics: Vec::new(),
4982 params: vec![],
4983 return_type: Some(TsType::named_with_args(
4984 "Promise",
4985 vec![TsType::named("Response")],
4986 )),
4987 doc: None,
4988 inline: false,
4989 body: vec![],
4990 },
4991 ])),
4992 None,
4993 ));
4994 let printed = print(&program, "x.bynk", "", "x.ts");
4995 assert_eq!(
4996 printed.text,
4997 "export default {\n async fetch(): Promise<Response> {\n },\n};\n"
4998 );
4999 }
5000
5001 /// #1323's own real correction: `Await` no longer auto-parenthesizes
5002 /// under `As` — `workers_entry.rs`'s own real `await request.json() as
5003 /// JsonValue` has no parens (`as` binds looser than `await`, so none
5004 /// are grammatically needed; P7.8's original "always parenthesize"
5005 /// reasoning conflated one file's own real text with a grammar
5006 /// requirement).
5007 #[test]
5008 fn an_await_used_as_an_as_operand_no_longer_needs_parens() {
5009 let mut program = TsProgram::new();
5010 program.push(TsStmt::const_stmt(
5011 TsBindingName::Ident("args".to_string()),
5012 None,
5013 TsExpr::As {
5014 expr: Box::new(TsExpr::Await(Box::new(TsExpr::Call {
5015 callee: Box::new(TsExpr::Member {
5016 object: Box::new(TsExpr::Ident("request".to_string())),
5017 property: "json".to_string(),
5018 }),
5019 args: vec![],
5020 }))),
5021 ty: TsType::named("JsonValue"),
5022 },
5023 None,
5024 ));
5025 let printed = print(&program, "x.bynk", "", "x.ts");
5026 assert_eq!(
5027 printed.text,
5028 "const args = await request.json() as JsonValue;\n"
5029 );
5030 }
5031
5032 /// #1353's own real gap: `bynk_ts::TsStmtKind::Throw`, added for
5033 /// `emit_contract_guarded_body`'s own real `throw __e;` sites. Review of
5034 /// #1354, finding 1: every real call site today reaches this only
5035 /// through an `InlineBlock` (`render_stmt(out, stmt, 0)`), so the
5036 /// top-level, non-inline `render_stmt` arm's own `indent(depth)` at a
5037 /// nonzero depth was untested — pinned directly here, mirroring
5038 /// `Return`'s own shape exactly.
5039 #[test]
5040 fn prints_a_throw_statement_at_a_nested_depth() {
5041 let stmt = TsStmt::throw_stmt(TsExpr::Ident("__e".to_string()), None);
5042 assert_eq!(print_stmt(&stmt, 2), " throw __e;\n");
5043 }
5044
5045 /// #1353's own real gap, byte-for-byte: the whole contract-guard shape
5046 /// `emit_contract_guarded_body` builds (`if (!(pred)) { const __e = ...;
5047 /// __e.name = ...; throw __e; }`) — an `If` over an `InlineBlock` of a
5048 /// `Const`/`Assign`/`Throw` trio — asserted as one exact string. Review
5049 /// of #1354, finding 1: no existing test (fixture, `contract_behaviour.
5050 /// rs`, `source_map.rs`) asserts this shape's own bytes; they only
5051 /// `contains`-check a runtime message or a generated line number, none
5052 /// would catch a missing space, a reordered statement, or a lost
5053 /// indent.
5054 #[test]
5055 fn prints_a_contract_guard_if_stmt() {
5056 let cond = TsExpr::Unary {
5057 op: TsUnaryOp::Not,
5058 expr: Box::new(TsExpr::Paren(Box::new(TsExpr::Ident("n > 0".to_string())))),
5059 };
5060 let new_error = TsExpr::New {
5061 callee: Box::new(TsExpr::Ident("Error".to_string())),
5062 args: vec![TsExpr::template_lit(
5063 vec![
5064 "contract violated: precondition \\`positive\\` of scale (n=${n})".to_string(),
5065 ],
5066 Vec::new(),
5067 )],
5068 };
5069 let const_e = TsStmt::const_stmt(
5070 TsBindingName::Ident("__e".to_string()),
5071 None,
5072 new_error,
5073 None,
5074 );
5075 let assign_name = TsStmt::assign(
5076 TsExpr::Member {
5077 object: Box::new(TsExpr::Ident("__e".to_string())),
5078 property: "name".to_string(),
5079 },
5080 TsExpr::Lit(TsLit::Str("BynkContractError".to_string())),
5081 None,
5082 );
5083 let throw_e = TsStmt::throw_stmt(TsExpr::Ident("__e".to_string()), None);
5084 let then_branch = TsStmt::inline_block(vec![const_e, assign_name, throw_e], None);
5085 let if_stmt = TsStmt::if_stmt(cond, then_branch, None);
5086 assert_eq!(
5087 print_stmt(&if_stmt, 1),
5088 " if (!(n > 0)) { const __e = new Error(`contract violated: precondition \\`positive\\` of scale (n=${n})`); __e.name = \"BynkContractError\"; throw __e; }\n"
5089 );
5090 }
5091
5092 /// #1323's own real gap: `if`'s own `else` branch, printed on a fresh
5093 /// line at the `if`'s own indent, then following the same block-vs-
5094 /// inline rule the `if` itself does — `workers_entry.rs`'s own real
5095 /// queue-consumer ack/retry dispatch.
5096 #[test]
5097 fn prints_an_if_else_with_an_inline_then_and_an_inline_block_else() {
5098 let mut program = TsProgram::new();
5099 program.push(TsStmt::if_else_stmt(
5100 TsExpr::Binary {
5101 op: TsBinaryOp::StrictEq,
5102 left: Box::new(TsExpr::Member {
5103 object: Box::new(TsExpr::Ident("result".to_string())),
5104 property: "tag".to_string(),
5105 }),
5106 right: Box::new(TsExpr::Lit(TsLit::Str("Ack".to_string()))),
5107 },
5108 TsStmt::expr_stmt(
5109 TsExpr::Call {
5110 callee: Box::new(TsExpr::Member {
5111 object: Box::new(TsExpr::Ident("msg".to_string())),
5112 property: "ack".to_string(),
5113 }),
5114 args: vec![],
5115 },
5116 None,
5117 ),
5118 TsStmt::inline_block(
5119 vec![TsStmt::expr_stmt(
5120 TsExpr::Call {
5121 callee: Box::new(TsExpr::Member {
5122 object: Box::new(TsExpr::Ident("msg".to_string())),
5123 property: "retry".to_string(),
5124 }),
5125 args: vec![],
5126 },
5127 None,
5128 )],
5129 None,
5130 ),
5131 None,
5132 ));
5133 let printed = print(&program, "x.bynk", "", "x.ts");
5134 assert_eq!(
5135 printed.text,
5136 "if (result.tag === \"Ack\") msg.ack();\nelse { msg.retry(); }\n"
5137 );
5138 }
5139
5140 /// #1323's own real gap: a `TryCatch`'s own `catch` body can be an
5141 /// `InlineBlock` too — the braces stay on their own lines (this shape's
5142 /// usual convention), but the content packs onto one compact line.
5143 /// `workers_entry.rs`'s own real queue-consumer catch clause.
5144 #[test]
5145 fn a_try_catchs_own_catch_block_can_be_an_inline_block() {
5146 let mut program = TsProgram::new();
5147 program.push(TsStmt::try_catch(
5148 TsStmt::block(
5149 vec![TsStmt::expr_stmt(TsExpr::Ident("work".to_string()), None)],
5150 None,
5151 ),
5152 Some("e"),
5153 TsStmt::inline_block(
5154 vec![
5155 TsStmt::expr_stmt(
5156 TsExpr::Call {
5157 callee: Box::new(TsExpr::Member {
5158 object: Box::new(TsExpr::Ident("console".to_string())),
5159 property: "error".to_string(),
5160 }),
5161 args: vec![TsExpr::Ident("e".to_string())],
5162 },
5163 None,
5164 ),
5165 TsStmt::expr_stmt(
5166 TsExpr::Call {
5167 callee: Box::new(TsExpr::Member {
5168 object: Box::new(TsExpr::Ident("msg".to_string())),
5169 property: "retry".to_string(),
5170 }),
5171 args: vec![],
5172 },
5173 None,
5174 ),
5175 ],
5176 None,
5177 ),
5178 None,
5179 ));
5180 let printed = print(&program, "x.bynk", "", "x.ts");
5181 assert_eq!(
5182 printed.text,
5183 "try {\n work;\n} catch (e) {\n console.error(e); msg.retry();\n}\n"
5184 );
5185 }
5186
5187 /// #1323's own real gap: `TsBinaryOp::GreaterThan` — the request-body
5188 /// ceiling guard's own `Number(__contentLength) > <cap>`, the one real
5189 /// site needing a relational (not equality) comparison.
5190 #[test]
5191 fn prints_a_greater_than_comparison() {
5192 let mut program = TsProgram::new();
5193 program.push(TsStmt::expr_stmt(
5194 TsExpr::Binary {
5195 op: TsBinaryOp::GreaterThan,
5196 left: Box::new(TsExpr::Call {
5197 callee: Box::new(TsExpr::Ident("Number".to_string())),
5198 args: vec![TsExpr::Ident("__contentLength".to_string())],
5199 }),
5200 right: Box::new(TsExpr::Lit(TsLit::Num("100".to_string()))),
5201 },
5202 None,
5203 ));
5204 let printed = print(&program, "x.bynk", "", "x.ts");
5205 assert_eq!(printed.text, "Number(__contentLength) > 100;\n");
5206 }
5207
5208 /// #1323's own real gap: `TsTypeMember::Index` — `workers_entry.rs`'s
5209 /// own `args as { [k: string]: JsonValue }`, textually distinct from
5210 /// the semantically-equivalent `Record<string, JsonValue>`.
5211 #[test]
5212 fn print_type_renders_an_index_signature() {
5213 let ty = TsType::Object(vec![TsTypeMember::index(
5214 "k",
5215 TsType::named("string"),
5216 TsType::named("JsonValue"),
5217 )]);
5218 assert_eq!(print_type(&ty), "{ [k: string]: JsonValue }");
5219 }
5220
5221 /// #1323's own real gap, found implementing `workers_entry.rs`:
5222 /// `export { a, b } from "spec";` — a re-export, distinct from both
5223 /// `Import` and `Export`. Not classified alongside imports for the
5224 /// "no blank line between adjacent imports" exception.
5225 #[test]
5226 fn prints_a_re_export_with_the_ordinary_blank_line_rule() {
5227 let mut program = TsProgram::new();
5228 program.push(TsStmt::decl(
5229 TsDecl::ImportNamespace {
5230 type_only: false,
5231 alias: "handlers".to_string(),
5232 from: "./handlers.js".to_string(),
5233 },
5234 None,
5235 ));
5236 program.push(TsStmt::decl(
5237 TsDecl::ReExport {
5238 names: vec!["Orders".to_string()],
5239 from: "./handlers.js".to_string(),
5240 },
5241 None,
5242 ));
5243 let printed = print(&program, "x.bynk", "", "x.ts");
5244 assert_eq!(
5245 printed.text,
5246 "import * as handlers from \"./handlers.js\";\n\nexport { Orders } from \"./handlers.js\";\n"
5247 );
5248 }
5249
5250 /// #1323's own real gap, found implementing `workers_entry.rs`: a bare
5251 /// blank line usable at any nesting depth, distinct from `print()`'s
5252 /// own top-level-only policy.
5253 #[test]
5254 fn a_blank_statement_prints_one_empty_line_inside_a_nested_block() {
5255 let mut program = TsProgram::new();
5256 program.push(TsStmt::if_stmt(
5257 TsExpr::Ident("cond".to_string()),
5258 TsStmt::block(
5259 vec![
5260 TsStmt::expr_stmt(TsExpr::Ident("a".to_string()), None),
5261 TsStmt::blank(None),
5262 TsStmt::expr_stmt(TsExpr::Ident("b".to_string()), None),
5263 ],
5264 None,
5265 ),
5266 None,
5267 ));
5268 let printed = print(&program, "x.bynk", "", "x.ts");
5269 assert_eq!(printed.text, "if (cond) {\n a;\n\n b;\n}\n");
5270 }
5271
5272 /// #1323's own real gap, found implementing `workers_entry.rs`:
5273 /// `CorsPolicy.credentials`/`SecurityPolicy.nosniff` are real booleans.
5274 #[test]
5275 fn prints_a_boolean_literal() {
5276 let mut program = TsProgram::new();
5277 program.push(TsStmt::expr_stmt(
5278 TsExpr::object(vec![
5279 ("credentials".to_string(), TsExpr::Lit(TsLit::Bool(true))),
5280 ("nosniff".to_string(), TsExpr::Lit(TsLit::Bool(false))),
5281 ]),
5282 None,
5283 ));
5284 let printed = print(&program, "x.bynk", "", "x.ts");
5285 assert_eq!(printed.text, "{ credentials: true, nosniff: false };\n");
5286 }
5287
5288 /// #1323's own real gap: an explicit `Paren` always prints its own
5289 /// literal parens, even when the wrapped expression's own precedence
5290 /// would not otherwise need any — `workers_entry.rs`'s CORS-preflight
5291 /// guard wraps its path-match condition in `(...)` unconditionally,
5292 /// even for a single equality check with nothing lower-precedence than
5293 /// the outer `&&` inside it (which the ordinary precedence-derived
5294 /// rules correctly do NOT parenthesize on their own).
5295 #[test]
5296 fn an_explicit_paren_always_prints_regardless_of_the_inner_expressions_own_precedence() {
5297 let mut program = TsProgram::new();
5298 program.push(TsStmt::expr_stmt(
5299 TsExpr::Binary {
5300 op: TsBinaryOp::And,
5301 left: Box::new(TsExpr::Ident("method".to_string())),
5302 right: Box::new(TsExpr::Paren(Box::new(TsExpr::Binary {
5303 op: TsBinaryOp::StrictEq,
5304 left: Box::new(TsExpr::Ident("path".to_string())),
5305 right: Box::new(TsExpr::Lit(TsLit::Str("/foo".to_string()))),
5306 }))),
5307 },
5308 None,
5309 ));
5310 let printed = print(&program, "x.bynk", "", "x.ts");
5311 assert_eq!(printed.text, "method && (path === \"/foo\");\n");
5312 }
5313
5314 /// #1323's own real gap: a 3-term `||` chain of the *same* operator
5315 /// prints flat, matching `emit_call_handler_dispatch`'s own real
5316 /// `typeof args !== "object" || args === null || Array.isArray(args)`
5317 /// — the pre-#1323 "always parenthesize equal precedence" rule would
5318 /// have wrongly added parens around the first two terms.
5319 #[test]
5320 fn a_three_term_or_chain_of_the_same_operator_prints_flat() {
5321 let mut program = TsProgram::new();
5322 program.push(TsStmt::expr_stmt(
5323 TsExpr::Binary {
5324 op: TsBinaryOp::Or,
5325 left: Box::new(TsExpr::Binary {
5326 op: TsBinaryOp::Or,
5327 left: Box::new(TsExpr::Binary {
5328 op: TsBinaryOp::StrictNotEq,
5329 left: Box::new(TsExpr::Unary {
5330 op: TsUnaryOp::Typeof,
5331 expr: Box::new(TsExpr::Ident("args".to_string())),
5332 }),
5333 right: Box::new(TsExpr::Lit(TsLit::Str("object".to_string()))),
5334 }),
5335 right: Box::new(TsExpr::Binary {
5336 op: TsBinaryOp::StrictEq,
5337 left: Box::new(TsExpr::Ident("args".to_string())),
5338 right: Box::new(TsExpr::Lit(TsLit::Null)),
5339 }),
5340 }),
5341 right: Box::new(TsExpr::Call {
5342 callee: Box::new(TsExpr::Member {
5343 object: Box::new(TsExpr::Ident("Array".to_string())),
5344 property: "isArray".to_string(),
5345 }),
5346 args: vec![TsExpr::Ident("args".to_string())],
5347 }),
5348 },
5349 None,
5350 ));
5351 let printed = print(&program, "x.bynk", "", "x.ts");
5352 assert_eq!(
5353 printed.text,
5354 "typeof args !== \"object\" || args === null || Array.isArray(args);\n"
5355 );
5356 }
5357
5358 /// Arc C, step (11) (#1388): `TsBinaryOp::Add`'s own real, dominant real
5359 /// site — a message template's own literal/placeholder segments,
5360 /// left-folded via `.join(" + ")` — needs the identical flat-chain
5361 /// treatment `||`/`&&` already established, joining the same
5362 /// associativity exemption in `render_binary_operand`, not a
5363 /// bespoke third rule.
5364 #[test]
5365 fn a_three_term_add_chain_of_the_same_operator_prints_flat() {
5366 let mut program = TsProgram::new();
5367 program.push(TsStmt::expr_stmt(
5368 TsExpr::Binary {
5369 op: TsBinaryOp::Add,
5370 left: Box::new(TsExpr::Binary {
5371 op: TsBinaryOp::Add,
5372 left: Box::new(TsExpr::Lit(TsLit::Str("a".to_string()))),
5373 right: Box::new(TsExpr::Lit(TsLit::Str("b".to_string()))),
5374 }),
5375 right: Box::new(TsExpr::Lit(TsLit::Str("c".to_string()))),
5376 },
5377 None,
5378 ));
5379 let printed = print(&program, "x.bynk", "", "x.ts");
5380 assert_eq!(printed.text, "\"a\" + \"b\" + \"c\";\n");
5381 }
5382
5383 /// Review of #1389, finding 1: unlike `||`/`&&` (semantically
5384 /// associative — flattening either side is safe), `+` is only
5385 /// grammatically left-associative — `1 + (2 + "3")` and `(1 + 2) + "3"`
5386 /// disagree once a number joins the chain. A right-nested `Add` must
5387 /// keep its parens, matching every other non-`Or`/`And` same-operator
5388 /// chain's own treatment (`a_right_nested_strict_eq_chain_keeps_its_
5389 /// parens`/`a_right_nested_greater_than_chain_keeps_its_parens`).
5390 #[test]
5391 fn a_right_nested_add_chain_keeps_its_parens() {
5392 let mut program = TsProgram::new();
5393 program.push(TsStmt::expr_stmt(
5394 TsExpr::Binary {
5395 op: TsBinaryOp::Add,
5396 left: Box::new(TsExpr::Lit(TsLit::Num("1".to_string()))),
5397 right: Box::new(TsExpr::Binary {
5398 op: TsBinaryOp::Add,
5399 left: Box::new(TsExpr::Lit(TsLit::Num("2".to_string()))),
5400 right: Box::new(TsExpr::Lit(TsLit::Str("3".to_string()))),
5401 }),
5402 },
5403 None,
5404 ));
5405 let printed = print(&program, "x.bynk", "", "x.ts");
5406 assert_eq!(printed.text, "1 + (2 + \"3\");\n");
5407 }
5408
5409 /// `Add`'s own new `binary_precedence` entry (higher than
5410 /// `GreaterThan`, matching real JS/TS) — a nested `Add` under a
5411 /// `GreaterThan` needs no parens (`+` binds tighter), the reverse
5412 /// direction does.
5413 #[test]
5414 fn add_binds_tighter_than_greater_than_on_both_sides() {
5415 let mut program = TsProgram::new();
5416 program.push(TsStmt::expr_stmt(
5417 TsExpr::Binary {
5418 op: TsBinaryOp::GreaterThan,
5419 left: Box::new(TsExpr::Binary {
5420 op: TsBinaryOp::Add,
5421 left: Box::new(TsExpr::Ident("a".to_string())),
5422 right: Box::new(TsExpr::Ident("b".to_string())),
5423 }),
5424 right: Box::new(TsExpr::Ident("c".to_string())),
5425 },
5426 None,
5427 ));
5428 let printed = print(&program, "x.bynk", "", "x.ts");
5429 assert_eq!(printed.text, "a + b > c;\n");
5430 }
5431
5432 /// The reverse nesting: a `GreaterThan` operand under an outer `Add`
5433 /// needs its parens (`+` binds tighter, so `Add`'s own precedence check
5434 /// against a lower-precedence `GreaterThan` operand must still fire).
5435 #[test]
5436 fn greater_than_nested_under_add_keeps_its_parens() {
5437 let mut program = TsProgram::new();
5438 program.push(TsStmt::expr_stmt(
5439 TsExpr::Binary {
5440 op: TsBinaryOp::Add,
5441 left: Box::new(TsExpr::Ident("a".to_string())),
5442 right: Box::new(TsExpr::Binary {
5443 op: TsBinaryOp::GreaterThan,
5444 left: Box::new(TsExpr::Ident("b".to_string())),
5445 right: Box::new(TsExpr::Ident("c".to_string())),
5446 }),
5447 },
5448 None,
5449 ));
5450 let printed = print(&program, "x.bynk", "", "x.ts");
5451 assert_eq!(printed.text, "a + (b > c);\n");
5452 }
5453
5454 /// Review of #1324, finding 1: the same-operator flattening #1323 added
5455 /// for `||`/`&&` was wrongly applied to every operator, including
5456 /// non-associative ones — a right-nested `StrictEq` inside `StrictEq`
5457 /// must keep its parens (`a === b === c` parses as `(a === b) === c`,
5458 /// not the tree's real `a === (b === c)`).
5459 #[test]
5460 fn a_right_nested_strict_eq_chain_keeps_its_parens() {
5461 let mut program = TsProgram::new();
5462 program.push(TsStmt::expr_stmt(
5463 TsExpr::Binary {
5464 op: TsBinaryOp::StrictEq,
5465 left: Box::new(TsExpr::Ident("a".to_string())),
5466 right: Box::new(TsExpr::Binary {
5467 op: TsBinaryOp::StrictEq,
5468 left: Box::new(TsExpr::Ident("b".to_string())),
5469 right: Box::new(TsExpr::Ident("c".to_string())),
5470 }),
5471 },
5472 None,
5473 ));
5474 let printed = print(&program, "x.bynk", "", "x.ts");
5475 assert_eq!(printed.text, "a === (b === c);\n");
5476 }
5477
5478 /// Same finding, for the new `GreaterThan` operator: `a > b > c` parses
5479 /// as `(a > b) > c` (a boolean compared against `c`), not the tree's
5480 /// real `a > (b > c)` — a right-nested chain must keep its parens.
5481 #[test]
5482 fn a_right_nested_greater_than_chain_keeps_its_parens() {
5483 let mut program = TsProgram::new();
5484 program.push(TsStmt::expr_stmt(
5485 TsExpr::Binary {
5486 op: TsBinaryOp::GreaterThan,
5487 left: Box::new(TsExpr::Ident("a".to_string())),
5488 right: Box::new(TsExpr::Binary {
5489 op: TsBinaryOp::GreaterThan,
5490 left: Box::new(TsExpr::Ident("b".to_string())),
5491 right: Box::new(TsExpr::Ident("c".to_string())),
5492 }),
5493 },
5494 None,
5495 ));
5496 let printed = print(&program, "x.bynk", "", "x.ts");
5497 assert_eq!(printed.text, "a > (b > c);\n");
5498 }
5499
5500 /// Review of #1324, finding 2: a `Blank` used as an `if`'s brace-free
5501 /// body used to render as a bare newline, silently letting the very
5502 /// next statement in the enclosing block become the `if`'s own body —
5503 /// now it prints an honest empty statement (`;`) that can't swallow
5504 /// anything after it.
5505 #[test]
5506 fn a_blank_if_body_prints_an_empty_statement_not_a_swallowing_newline() {
5507 // Nested inside a `Block`, not two top-level program statements —
5508 // `print`'s own blank-line-between-statements policy only applies
5509 // between top-level statements (see its own doc comment), so a
5510 // top-level pair would mask the swallowing bug this test exists to
5511 // catch: the real hazard is two statements sharing one physical
5512 // line with nothing separating them, exactly what `render_stmt`'s
5513 // `Block` arm produces with no blank-line insertion of its own.
5514 let mut program = TsProgram::new();
5515 program.push(TsStmt::block(
5516 vec![
5517 TsStmt::if_stmt(TsExpr::Ident("cond".to_string()), TsStmt::blank(None), None),
5518 TsStmt::expr_stmt(
5519 TsExpr::Call {
5520 callee: Box::new(TsExpr::Ident("nextStatement".to_string())),
5521 args: vec![],
5522 },
5523 None,
5524 ),
5525 ],
5526 None,
5527 ));
5528 let printed = print(&program, "x.bynk", "", "x.ts");
5529 assert_eq!(printed.text, "{\n if (cond) ;\n nextStatement();\n}\n");
5530 }
5531
5532 /// A left-nested same-operator chain also keeps its parens for a
5533 /// non-associative operator (unlike the `||`/`&&` case above) —
5534 /// `(a === b) === c` reads the same as `a === b === c` would parse, so
5535 /// this is really about `render_binary_operand` not silently dropping
5536 /// parens it should keep, not about a left/right asymmetry.
5537 #[test]
5538 fn a_left_nested_strict_eq_chain_keeps_its_parens() {
5539 let mut program = TsProgram::new();
5540 program.push(TsStmt::expr_stmt(
5541 TsExpr::Binary {
5542 op: TsBinaryOp::StrictEq,
5543 left: Box::new(TsExpr::Binary {
5544 op: TsBinaryOp::StrictEq,
5545 left: Box::new(TsExpr::Ident("a".to_string())),
5546 right: Box::new(TsExpr::Ident("b".to_string())),
5547 }),
5548 right: Box::new(TsExpr::Ident("c".to_string())),
5549 },
5550 None,
5551 ));
5552 let printed = print(&program, "x.bynk", "", "x.ts");
5553 assert_eq!(printed.text, "(a === b) === c;\n");
5554 }
5555
5556 // -- #1325: emit_test_main's own new shapes ------------------------------
5557
5558 #[test]
5559 fn prints_a_template_literal_with_substitutions() {
5560 let mut program = TsProgram::new();
5561 program.push(TsStmt::expr_stmt(
5562 TsExpr::template_lit(
5563 vec![
5564 String::new(),
5565 " passed, ".to_string(),
5566 " failed.".to_string(),
5567 ],
5568 vec![
5569 TsExpr::Ident("passed".to_string()),
5570 TsExpr::Ident("failed".to_string()),
5571 ],
5572 ),
5573 None,
5574 ));
5575 let printed = print(&program, "x.bynk", "", "x.ts");
5576 assert_eq!(printed.text, "`${passed} passed, ${failed} failed.`;\n");
5577 }
5578
5579 /// A template literal's own static parts print with no escaping applied
5580 /// by the printer — see `TsExpr::TemplateLit`'s own doc for why: a
5581 /// generic escaper would double the literal backslash of an
5582 /// already-pre-formed JS unicode escape (`✓`), corrupting it. This
5583 /// pins the real, grounded shape directly: the six ASCII characters
5584 /// `✓` pass through unchanged, not doubled into `\\u2713`.
5585 #[test]
5586 fn a_template_literal_part_carrying_a_preformed_unicode_escape_is_not_reescaped() {
5587 let mut program = TsProgram::new();
5588 program.push(TsStmt::expr_stmt(
5589 TsExpr::template_lit(
5590 vec![" \\u2713 ".to_string(), String::new()],
5591 vec![TsExpr::Ident("r".to_string())],
5592 ),
5593 None,
5594 ));
5595 let printed = print(&program, "x.bynk", "", "x.ts");
5596 assert_eq!(printed.text, "` \\u2713 ${r}`;\n");
5597 }
5598
5599 #[test]
5600 fn a_raw_literal_prints_exactly_as_given_with_no_escaping() {
5601 let mut program = TsProgram::new();
5602 program.push(TsStmt::expr_stmt(
5603 TsExpr::Lit(TsLit::Raw("\"integration \\u00b7 \"".to_string())),
5604 None,
5605 ));
5606 let printed = print(&program, "x.bynk", "", "x.ts");
5607 assert_eq!(printed.text, "\"integration \\u00b7 \";\n");
5608 }
5609
5610 #[test]
5611 fn prints_a_multiline_array_literal() {
5612 let mut program = TsProgram::new();
5613 program.push(TsStmt::const_stmt(
5614 TsBindingName::Ident("modules".to_string()),
5615 None,
5616 TsExpr::multiline_array(vec![
5617 TsExpr::object(vec![(
5618 "name".to_string(),
5619 TsExpr::Lit(TsLit::Str("a".to_string())),
5620 )]),
5621 TsExpr::object(vec![(
5622 "name".to_string(),
5623 TsExpr::Lit(TsLit::Str("b".to_string())),
5624 )]),
5625 ]),
5626 None,
5627 ));
5628 let printed = print(&program, "x.bynk", "", "x.ts");
5629 assert_eq!(
5630 printed.text,
5631 "const modules = [\n { name: \"a\" },\n { name: \"b\" },\n];\n"
5632 );
5633 }
5634
5635 /// Same reachability boundary as `TsExpr::Object`'s own `multiline`
5636 /// field (see its doc) — a `multiline: true` array nested inside another
5637 /// expression falls back to single-line via the depth-unaware
5638 /// `render_expr` recursion.
5639 #[test]
5640 fn a_nested_multiline_array_falls_back_to_single_line() {
5641 let mut program = TsProgram::new();
5642 program.push(TsStmt::expr_stmt(
5643 TsExpr::array(vec![TsExpr::multiline_array(vec![TsExpr::Lit(
5644 TsLit::Num("1".to_string()),
5645 )])]),
5646 None,
5647 ));
5648 let printed = print(&program, "x.bynk", "", "x.ts");
5649 assert_eq!(printed.text, "[[1]];\n");
5650 }
5651
5652 #[test]
5653 fn prints_a_declare_const_ambient_binding() {
5654 let mut program = TsProgram::new();
5655 program.push(TsStmt::decl(
5656 TsDecl::DeclareConst {
5657 name: "process".to_string(),
5658 ty: TsType::Object(vec![TsTypeMember::prop("env", TsType::named("unknown"))]),
5659 },
5660 None,
5661 ));
5662 let printed = print(&program, "x.bynk", "", "x.ts");
5663 assert_eq!(printed.text, "declare const process: { env: unknown };\n");
5664 }
5665
5666 #[test]
5667 fn prints_an_async_top_level_function() {
5668 let mut program = TsProgram::new();
5669 program.push(TsStmt::decl(
5670 TsDecl::Function {
5671 name: "main".to_string(),
5672 generics: Vec::new(),
5673 params: vec![],
5674 return_type: None,
5675 body: vec![TsStmt::return_stmt(None, None)],
5676 is_async: true,
5677 inline: false,
5678 },
5679 None,
5680 ));
5681 let printed = print(&program, "x.bynk", "", "x.ts");
5682 assert_eq!(printed.text, "async function main() {\n return;\n}\n");
5683 }
5684
5685 /// #1369 (Arc C, slice 20): `TsDecl::Function.inline` — the zero-factory
5686 /// shape (`function name(): Ret { return <expr>; }` all on one line)
5687 /// `emit_agent` needs, mirroring `TsObjectEntry::Method.inline`'s own
5688 /// single-line-vs-multi-line precedent (#1337) at a different node kind.
5689 #[test]
5690 fn prints_an_inline_top_level_function() {
5691 let mut program = TsProgram::new();
5692 program.push(TsStmt::decl(
5693 TsDecl::Function {
5694 name: "zero".to_string(),
5695 generics: Vec::new(),
5696 params: vec![],
5697 return_type: Some(TsType::named("Foo")),
5698 body: vec![TsStmt::return_stmt(
5699 Some(TsExpr::object(vec![(
5700 "n".to_string(),
5701 TsExpr::Lit(TsLit::Num("0".to_string())),
5702 )])),
5703 None,
5704 )],
5705 is_async: false,
5706 inline: true,
5707 },
5708 None,
5709 ));
5710 let printed = print(&program, "x.bynk", "", "x.ts");
5711 assert_eq!(printed.text, "function zero(): Foo { return { n: 0 }; }\n");
5712 }
5713
5714 #[test]
5715 fn prints_a_postfix_increment_statement() {
5716 let mut program = TsProgram::new();
5717 program.push(TsStmt::increment(TsExpr::Ident("passed".to_string()), None));
5718 let printed = print(&program, "x.bynk", "", "x.ts");
5719 assert_eq!(printed.text, "passed++;\n");
5720 }
5721
5722 /// An `Increment` inside an `InlineBlock` renders correctly through
5723 /// `render_inline_stmt`'s fallback — `emit_test_main`'s own real
5724 /// `{ passed++; console.log(...); }` shape.
5725 #[test]
5726 fn a_postfix_increment_renders_correctly_inside_an_inline_block() {
5727 let mut program = TsProgram::new();
5728 program.push(TsStmt::if_stmt(
5729 TsExpr::Ident("cond".to_string()),
5730 TsStmt::inline_block(
5731 vec![
5732 TsStmt::increment(TsExpr::Ident("passed".to_string()), None),
5733 TsStmt::expr_stmt(TsExpr::Ident("next".to_string()), None),
5734 ],
5735 None,
5736 ),
5737 None,
5738 ));
5739 let printed = print(&program, "x.bynk", "", "x.ts");
5740 assert_eq!(printed.text, "if (cond) { passed++; next; }\n");
5741 }
5742
5743 /// `same_line_else` with a `Block` then-branch — `} else {` on one
5744 /// physical line, distinct from the fresh-line default pinned elsewhere
5745 /// in this module.
5746 #[test]
5747 fn same_line_else_puts_the_else_keyword_after_the_closing_brace() {
5748 let mut program = TsProgram::new();
5749 program.push(TsStmt::if_else_same_line_stmt(
5750 TsExpr::Ident("cond".to_string()),
5751 TsStmt::block(
5752 vec![TsStmt::expr_stmt(TsExpr::Ident("a".to_string()), None)],
5753 None,
5754 ),
5755 TsStmt::block(
5756 vec![TsStmt::expr_stmt(TsExpr::Ident("b".to_string()), None)],
5757 None,
5758 ),
5759 None,
5760 ));
5761 let printed = print(&program, "x.bynk", "", "x.ts");
5762 assert_eq!(printed.text, "if (cond) {\n a;\n} else {\n b;\n}\n");
5763 }
5764
5765 /// `same_line_else` with `InlineBlock` branches on both sides —
5766 /// `emit_test_main`'s own real `if (r.pass) { ... } else { ... }` shape,
5767 /// entirely on one generated line.
5768 #[test]
5769 fn same_line_else_with_inline_block_branches_stays_on_one_line() {
5770 let mut program = TsProgram::new();
5771 program.push(TsStmt::if_else_same_line_stmt(
5772 TsExpr::Ident("cond".to_string()),
5773 TsStmt::inline_block(
5774 vec![TsStmt::expr_stmt(TsExpr::Ident("a".to_string()), None)],
5775 None,
5776 ),
5777 TsStmt::inline_block(
5778 vec![TsStmt::expr_stmt(TsExpr::Ident("b".to_string()), None)],
5779 None,
5780 ),
5781 None,
5782 ));
5783 let printed = print(&program, "x.bynk", "", "x.ts");
5784 assert_eq!(printed.text, "if (cond) { a; } else { b; }\n");
5785 }
5786
5787 /// `same_line_else` with a brace-free `then_branch` falls back to the
5788 /// ordinary fresh-line rendering — nothing real needs `<inline-stmt>
5789 /// else {`, and the printer doesn't claim to support it (see
5790 /// `TsStmtKind::If`'s own doc).
5791 #[test]
5792 fn same_line_else_with_a_brace_free_then_branch_falls_back_to_fresh_line() {
5793 let mut program = TsProgram::new();
5794 program.push(TsStmt::if_else_same_line_stmt(
5795 TsExpr::Ident("cond".to_string()),
5796 TsStmt::continue_stmt(None),
5797 TsStmt::block(
5798 vec![TsStmt::expr_stmt(TsExpr::Ident("b".to_string()), None)],
5799 None,
5800 ),
5801 None,
5802 ));
5803 let printed = print(&program, "x.bynk", "", "x.ts");
5804 assert_eq!(printed.text, "if (cond) continue;\nelse {\n b;\n}\n");
5805 }
5806
5807 /// `TsDecl::ReExportAll` prints `export * from "spec";` — no braces, no
5808 /// name list, matching `emit_commons_barrel`'s own real per-file line.
5809 #[test]
5810 fn re_export_all_prints_a_wildcard_re_export() {
5811 let mut program = TsProgram::new();
5812 program.push(TsStmt::decl(
5813 TsDecl::ReExportAll {
5814 from: "./thing/make.js".to_string(),
5815 },
5816 None,
5817 ));
5818 let printed = print(&program, "x.bynk", "", "x.ts");
5819 assert_eq!(printed.text, "export * from \"./thing/make.js\";\n");
5820 }
5821
5822 /// Review of #1329's own grounding: `emit_commons_barrel`'s real barrel
5823 /// module is one header `Comment` immediately followed by one `export
5824 /// *` line per constituent source file, every one of those lines
5825 /// adjacent with no blank line anywhere — the exact byte shape pinned
5826 /// here, matching `251_multi_file_commons_test`'s own real
5827 /// `expected/thing.ts`.
5828 #[test]
5829 fn a_header_comment_and_consecutive_re_export_alls_have_no_blank_lines() {
5830 let mut program = TsProgram::new();
5831 program.push(TsStmt::comment(
5832 "Generated by bynkc — do not edit by hand.",
5833 None,
5834 ));
5835 program.push(TsStmt::decl(
5836 TsDecl::ReExportAll {
5837 from: "./thing/make.js".to_string(),
5838 },
5839 None,
5840 ));
5841 program.push(TsStmt::decl(
5842 TsDecl::ReExportAll {
5843 from: "./thing/widget.js".to_string(),
5844 },
5845 None,
5846 ));
5847 let printed = print(&program, "x.bynk", "", "x.ts");
5848 assert_eq!(
5849 printed.text,
5850 "// Generated by bynkc — do not edit by hand.\n\
5851 export * from \"./thing/make.js\";\n\
5852 export * from \"./thing/widget.js\";\n"
5853 );
5854 }
5855
5856 /// Review of #1330: the grouping rule added for #1329 is scoped to
5857 /// `ReExportAll`-adjacent-to-`ReExportAll` (and a `Comment` immediately
5858 /// before one) — a `ReExportAll` next to anything else still gets the
5859 /// ordinary blank line. `emit_commons_barrel` is `ReExportAll`'s only
5860 /// producer today, so this is the invariant keeping the rule scoped if
5861 /// a future slice ever gives it a second one.
5862 #[test]
5863 fn a_re_export_all_next_to_a_non_re_export_all_gets_the_ordinary_blank_line() {
5864 let mut program = TsProgram::new();
5865 program.push(TsStmt::decl(
5866 TsDecl::ImportNamespace {
5867 type_only: false,
5868 alias: "handlers".to_string(),
5869 from: "./handlers.js".to_string(),
5870 },
5871 None,
5872 ));
5873 program.push(TsStmt::decl(
5874 TsDecl::ReExportAll {
5875 from: "./thing/make.js".to_string(),
5876 },
5877 None,
5878 ));
5879 program.push(TsStmt::comment("trailing note", None));
5880 let printed = print(&program, "x.bynk", "", "x.ts");
5881 assert_eq!(
5882 printed.text,
5883 "import * as handlers from \"./handlers.js\";\n\n\
5884 export * from \"./thing/make.js\";\n\n\
5885 // trailing note\n"
5886 );
5887 }
5888
5889 /// #1333: `print_stmt` prints a `TsStmtKind::DocComment` as a real JSDoc
5890 /// block, matching `emit_doc_block`'s own real multi-line shape
5891 /// (`137_agent_instantiation_workers/expected/workers/demo-counter/
5892 /// handlers.ts`'s own real header comment).
5893 #[test]
5894 fn print_stmt_renders_a_multi_line_doc_comment() {
5895 let stmt = TsStmt::doc_comment(
5896 "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.",
5897 None,
5898 );
5899 assert_eq!(
5900 print_stmt(&stmt, 0),
5901 "/**\n \
5902 * A minimal stateful agent in the bundle target: instantiation lowers through the\n \
5903 * generated factory, the method call is a direct call, and state persists per key\n \
5904 * across calls within a session.\n \
5905 */\n"
5906 );
5907 }
5908
5909 /// #1333: a literal `*/` inside doc text escapes to `*\/`, matching
5910 /// `emit_doc_block`'s own pre-conversion behaviour exactly (issue
5911 /// #720 — an unescaped `*/` would otherwise close the comment early
5912 /// and let trailing text land as executable top-level TypeScript).
5913 #[test]
5914 fn print_stmt_escapes_a_literal_comment_terminator_inside_doc_text() {
5915 let stmt = TsStmt::doc_comment("docs */ ; (globalThis as any).PWNED = true; /*", None);
5916 let printed = print_stmt(&stmt, 0);
5917 assert!(!printed.contains("*/ ;"), "unescaped terminator: {printed}");
5918 assert_eq!(
5919 printed,
5920 "/**\n * docs *\\/ ; (globalThis as any).PWNED = true; /*\n */\n"
5921 );
5922 }
5923
5924 /// #1333: a blank line inside doc text prints as a bare ` *`, no
5925 /// trailing space — distinct from a non-blank line's own ` * <line>`.
5926 #[test]
5927 fn print_stmt_renders_a_blank_doc_line_as_a_bare_star() {
5928 let stmt = TsStmt::doc_comment("first paragraph\n\nsecond paragraph", None);
5929 assert_eq!(
5930 print_stmt(&stmt, 0),
5931 "/**\n * first paragraph\n *\n * second paragraph\n */\n"
5932 );
5933 }
5934
5935 /// #1333: `print_stmt`'s own `depth` parameter indents every line of
5936 /// the JSDoc block, matching `render_stmt`'s own 2-space-per-level
5937 /// convention — `emit_doc_block`'s real callers pass `INDENT_STEP`
5938 /// (2 raw spaces = depth 1) for a nested doc comment.
5939 #[test]
5940 fn print_stmt_indents_a_doc_comment_at_depth() {
5941 let stmt = TsStmt::doc_comment("a method", None);
5942 assert_eq!(print_stmt(&stmt, 1), " /**\n * a method\n */\n");
5943 }
5944
5945 /// #1337: `TsStmtKind::Raw` prints its own text exactly as given — no
5946 /// leading indent (unlike every other statement kind, whose own
5947 /// `render_stmt` arm prefixes `indent(depth)`), no added semicolon or
5948 /// braces. `emit_method`'s own opaque `lower.rs`-sourced body is
5949 /// already fully, absolutely indented by the time it's captured into
5950 /// one `Raw` node, so the printer must contribute nothing further —
5951 /// the same reasoning `Verbatim` already established for pre-rendered
5952 /// content, `Raw`'s own doc explains why it's a distinct kind.
5953 #[test]
5954 fn print_stmt_renders_raw_text_verbatim_with_no_added_indent_or_punctuation() {
5955 let stmt = TsStmt::raw(" return x + 1;\n", None);
5956 // depth is passed but deliberately has no effect on Raw's own output.
5957 assert_eq!(print_stmt(&stmt, 3), " return x + 1;\n");
5958 }
5959
5960 /// #1337: multi-line `Raw` text (the real shape — a whole function
5961 /// body, not one line) passes through with every embedded line intact,
5962 /// confirming the printer doesn't split, re-indent, or otherwise
5963 /// interpret it.
5964 #[test]
5965 fn print_stmt_renders_multi_line_raw_text_unchanged() {
5966 let stmt = TsStmt::raw(
5967 " const r = Uuid.of(crypto.randomUUID());\n return r.value;\n",
5968 None,
5969 );
5970 assert_eq!(
5971 print_stmt(&stmt, 0),
5972 " const r = Uuid.of(crypto.randomUUID());\n return r.value;\n"
5973 );
5974 }
5975
5976 /// #1337: `TsObjectEntry::Method` with a `Raw` body — `emit_method`'s
5977 /// own real shape (`{method}{generics}({params}): {ret} { <raw body>
5978 /// },`), pinned at the actual depth its own object literal renders at
5979 /// (a top-level `export const {...}`, depth 0 → entries at depth 1,
5980 /// two-space indent, matching `emit_method`'s own pre-conversion
5981 /// hand-written `" {method}..."` line).
5982 #[test]
5983 fn multiline_object_renders_a_method_entry_with_a_raw_body() {
5984 let mut out = String::new();
5985 render_multiline_object(
5986 &mut out,
5987 &[TsObjectEntry::Method {
5988 name: "of".to_string(),
5989 is_async: false,
5990 generics: Vec::new(),
5991 params: vec![TsParam {
5992 name: "value".to_string(),
5993 ty: Some(TsType::named("string")),
5994 optional: false,
5995 }],
5996 return_type: Some(TsType::named("Uuid")),
5997 doc: None,
5998 inline: false,
5999 body: vec![TsStmt::raw(" return value as Uuid;\n", None)],
6000 }],
6001 0,
6002 );
6003 assert_eq!(
6004 out,
6005 "{\n of(value: string): Uuid {\n return value as Uuid;\n },\n}"
6006 );
6007 }
6008
6009 /// #1337: `TsObjectEntry::Method`'s own `generics` field prints
6010 /// `<A, U>` between the method name and its parameter list —
6011 /// `Box.map`'s own real shape (`402_generic_instance_method`, a
6012 /// single-file-form fixture the accepted proposal's own project-form
6013 /// search missed).
6014 #[test]
6015 fn multiline_object_renders_a_generic_method_entry() {
6016 let mut out = String::new();
6017 render_multiline_object(
6018 &mut out,
6019 &[TsObjectEntry::Method {
6020 name: "map".to_string(),
6021 is_async: false,
6022 generics: vec!["A".to_string(), "U".to_string()],
6023 params: vec![
6024 TsParam {
6025 name: "self".to_string(),
6026 ty: Some(TsType::named("Box<A>")),
6027 optional: false,
6028 },
6029 TsParam {
6030 name: "f".to_string(),
6031 ty: Some(TsType::named("(a0: A) => U")),
6032 optional: false,
6033 },
6034 ],
6035 return_type: Some(TsType::named("Box<U>")),
6036 doc: None,
6037 inline: false,
6038 body: vec![TsStmt::return_stmt(
6039 Some(TsExpr::Ident("{ value: f(self.value) }".to_string())),
6040 None,
6041 )],
6042 }],
6043 0,
6044 );
6045 assert_eq!(
6046 out,
6047 "{\n map<A, U>(self: Box<A>, f: (a0: A) => U): Box<U> {\n return { value: f(self.value) };\n },\n}"
6048 );
6049 }
6050
6051 /// #1337: a method with no generics prints no `<>` at all — the
6052 /// ordinary, dominant case (every prior slice's own real method
6053 /// entries), confirming `generics: Vec::new()` is a true no-op, not
6054 /// an empty `<>`.
6055 #[test]
6056 fn multiline_object_renders_a_non_generic_method_entry_with_no_angle_brackets() {
6057 let mut out = String::new();
6058 render_multiline_object(
6059 &mut out,
6060 &[TsObjectEntry::Method {
6061 name: "get".to_string(),
6062 is_async: false,
6063 generics: Vec::new(),
6064 params: vec![],
6065 return_type: Some(TsType::named("void")),
6066 doc: None,
6067 inline: false,
6068 body: vec![],
6069 }],
6070 0,
6071 );
6072 assert_eq!(out, "{\n get(): void {\n },\n}");
6073 }
6074
6075 /// #1337: `TsObjectEntry::Method`'s own `doc` field prints a JSDoc
6076 /// block immediately before the method entry, same indent, no blank
6077 /// line between — `Timestamp.diff`'s own real shape
6078 /// (`65_money_uses_time`), reusing `render_doc_comment` (the same
6079 /// renderer `TsStmtKind::DocComment` already uses) rather than a
6080 /// second copy.
6081 #[test]
6082 fn multiline_object_renders_a_method_entry_with_a_preceding_doc_comment() {
6083 let mut out = String::new();
6084 render_multiline_object(
6085 &mut out,
6086 &[TsObjectEntry::Method {
6087 name: "diff".to_string(),
6088 is_async: false,
6089 generics: Vec::new(),
6090 params: vec![TsParam {
6091 name: "self".to_string(),
6092 ty: Some(TsType::named("Timestamp")),
6093 optional: false,
6094 }],
6095 return_type: Some(TsType::named("Span")),
6096 doc: Some("Compute the duration between two timestamps.".to_string()),
6097 inline: false,
6098 body: vec![TsStmt::raw(" return 0;\n", None)],
6099 }],
6100 0,
6101 );
6102 assert_eq!(
6103 out,
6104 "{\n /**\n * Compute the duration between two timestamps.\n */\n diff(self: Timestamp): Span {\n return 0;\n },\n}"
6105 );
6106 }
6107
6108 /// #1337: `TsObjectEntry::Method`'s own `inline: true` renders the
6109 /// whole entry — signature and one-statement body alike — on ONE
6110 /// physical line, `emit_forwarded_methods`'s own real shape
6111 /// (`255_context_uses_commons_static_method`'s own real `equals`
6112 /// entry: `equals(self: Cents, other: Cents): boolean { return
6113 /// __CommonsCents.equals(self, other) as unknown as boolean; },`),
6114 /// distinct from every other real `Method` entry in this tree
6115 /// (always multi-line, `inline: false`).
6116 #[test]
6117 fn multiline_object_renders_an_inline_method_entry_on_one_line() {
6118 let mut out = String::new();
6119 render_multiline_object(
6120 &mut out,
6121 &[TsObjectEntry::Method {
6122 name: "equals".to_string(),
6123 is_async: false,
6124 generics: Vec::new(),
6125 params: vec![
6126 TsParam {
6127 name: "self".to_string(),
6128 ty: Some(TsType::named("Cents")),
6129 optional: false,
6130 },
6131 TsParam {
6132 name: "other".to_string(),
6133 ty: Some(TsType::named("Cents")),
6134 optional: false,
6135 },
6136 ],
6137 return_type: Some(TsType::named("boolean")),
6138 doc: None,
6139 inline: true,
6140 body: vec![TsStmt::return_stmt(
6141 Some(TsExpr::As {
6142 expr: Box::new(TsExpr::As {
6143 expr: Box::new(TsExpr::Call {
6144 callee: Box::new(TsExpr::Member {
6145 object: Box::new(TsExpr::Ident("__CommonsCents".to_string())),
6146 property: "equals".to_string(),
6147 }),
6148 args: vec![
6149 TsExpr::Ident("self".to_string()),
6150 TsExpr::Ident("other".to_string()),
6151 ],
6152 }),
6153 ty: TsType::named("unknown"),
6154 }),
6155 ty: TsType::named("boolean"),
6156 }),
6157 None,
6158 )],
6159 }],
6160 0,
6161 );
6162 assert_eq!(
6163 out,
6164 "{\n equals(self: Cents, other: Cents): boolean { return __CommonsCents.equals(self, other) as unknown as boolean; },\n}"
6165 );
6166 }
6167
6168 /// Review of #1338, finding 4: `print_object_entry` — the exact public
6169 /// API `emit_refined_type`/`emit_record_type`/`emit_sum_type`'s own
6170 /// real call sites depend on — had no direct test; every #1337 test
6171 /// above drives `render_multiline_object` instead. Pins the contract
6172 /// those call sites rely on: `print_object_entry(&entry, 0)` produces
6173 /// the same text as the entry-slice portion of what
6174 /// `render_multiline_object`'s own depth-0 output produces for the
6175 /// same entry (its own depth convention — the entry lands one level
6176 /// deeper than the object, i.e. `depth + 1` — matches exactly, not
6177 /// coincidentally, since both paths route through the same
6178 /// `render_multiline_object_entry`).
6179 #[test]
6180 fn print_object_entry_matches_the_multiline_objects_own_entry_slice() {
6181 // A real `TsStmt::return_stmt` body, not `Raw` — `Raw`'s own baked-in
6182 // indent only matches depth 0 (finding 3's own new guard), so an
6183 // ordinary real-node body is what this depth-convention contract
6184 // needs to prove at a non-zero depth.
6185 let entry = TsObjectEntry::Method {
6186 name: "of".to_string(),
6187 is_async: false,
6188 generics: Vec::new(),
6189 params: vec![TsParam {
6190 name: "value".to_string(),
6191 ty: Some(TsType::named("string")),
6192 optional: false,
6193 }],
6194 return_type: Some(TsType::named("Uuid")),
6195 doc: None,
6196 inline: false,
6197 body: vec![TsStmt::return_stmt(
6198 Some(TsExpr::As {
6199 expr: Box::new(TsExpr::Ident("value".to_string())),
6200 ty: TsType::named("Uuid"),
6201 }),
6202 None,
6203 )],
6204 };
6205
6206 let mut whole_object = String::new();
6207 render_multiline_object(&mut whole_object, std::slice::from_ref(&entry), 0);
6208 // `render_multiline_object`'s own entry line is followed by `\n`
6209 // then the closing `}` (no blank line between them for a single
6210 // entry) — `print_object_entry`'s own output keeps that same
6211 // trailing `\n` (it has no closing brace of its own to attach to),
6212 // so the real equivalence is entry-slice-plus-newline, not a bare
6213 // slice.
6214 let entry_slice_with_trailing_newline = whole_object
6215 .strip_prefix("{\n")
6216 .and_then(|s| s.strip_suffix('}'))
6217 .expect("render_multiline_object's own single-entry output has a { }-wrapper");
6218
6219 assert_eq!(
6220 print_object_entry(&entry, 0),
6221 entry_slice_with_trailing_newline
6222 );
6223 }
6224
6225 /// #1339's own real gap: `emit_refined_type`'s own branded-type alias,
6226 /// `{base} & { readonly __brand: "..." }`, has no representation among
6227 /// `Named`/`Array`/`Object`/`Fn`/`Union` — mirrors `Union`'s own single-
6228 /// line, ` & `-joined shape exactly.
6229 #[test]
6230 fn print_type_renders_an_intersection() {
6231 let ty = TsType::intersection(vec![
6232 TsType::named("string"),
6233 TsType::Object(vec![TsTypeMember::readonly_prop(
6234 "__brand",
6235 TsType::named("\"Order\""),
6236 )]),
6237 ]);
6238 assert_eq!(print_type(&ty), "string & { readonly __brand: \"Order\" }");
6239 }
6240
6241 /// #1339's own real gap: `emit_sum_type`'s own multi-line discriminated
6242 /// union — a leading `|` on every line except the first (which gets
6243 /// equivalent spacing instead, matching the pre-conversion `writeln!`
6244 /// code's own `let pipe = if i == 0 { " " } else { "|" };` exactly), no
6245 /// trailing newline or `;` of its own (the caller, `TsDecl::TypeAlias`'s
6246 /// own render arm, owns both).
6247 #[test]
6248 fn print_type_renders_a_multiline_union() {
6249 let ty = TsType::multiline_union(vec![
6250 TsType::Object(vec![TsTypeMember::readonly_prop(
6251 "tag",
6252 TsType::named("\"a\""),
6253 )]),
6254 TsType::Object(vec![
6255 TsTypeMember::readonly_prop("tag", TsType::named("\"b\"")),
6256 TsTypeMember::readonly_prop("value", TsType::named("number")),
6257 ]),
6258 ]);
6259 assert_eq!(
6260 print_type(&ty),
6261 " { readonly tag: \"a\" }\n | { readonly tag: \"b\"; readonly value: number }"
6262 );
6263 }
6264
6265 /// #1339: `TsDecl::TypeAlias`'s own multiline-union special case — the
6266 /// `=` is followed directly by `\n` (no trailing space, matching the
6267 /// pre-conversion `writeln!(out, "export type {name}{params} =")`
6268 /// line's own exact bytes), each variant on its own line, the closing
6269 /// `;` appended directly to the last variant's own line. Also pins
6270 /// `type_params`' own bare-generics rendering on the alias header.
6271 #[test]
6272 fn prints_a_generic_sum_types_own_multiline_type_alias() {
6273 let mut program = TsProgram::new();
6274 program.push(TsStmt::decl(
6275 TsDecl::Export(Box::new(TsDecl::TypeAlias {
6276 name: "Opt".to_string(),
6277 type_params: vec!["T".to_string()],
6278 ty: TsType::multiline_union(vec![
6279 TsType::Object(vec![TsTypeMember::readonly_prop(
6280 "tag",
6281 TsType::named("\"none\""),
6282 )]),
6283 TsType::Object(vec![
6284 TsTypeMember::readonly_prop("tag", TsType::named("\"some\"")),
6285 TsTypeMember::readonly_prop("value", TsType::named("T")),
6286 ]),
6287 ]),
6288 })),
6289 None,
6290 ));
6291 let printed = print(&program, "x.bynk", "", "x.ts");
6292 assert_eq!(
6293 printed.text,
6294 "export type Opt<T> =\n \
6295 { readonly tag: \"none\" }\n \
6296 | { readonly tag: \"some\"; readonly value: T };\n"
6297 );
6298 }
6299
6300 /// #1339's own real gap: `emit_record_type`'s own `export interface
6301 /// {name}{params} { readonly {field}: {ty}; ... }` — bare generic names
6302 /// on the interface header, `readonly` on every real member here.
6303 #[test]
6304 fn prints_a_generic_interface_with_readonly_members() {
6305 let mut program = TsProgram::new();
6306 program.push(TsStmt::decl(
6307 TsDecl::Export(Box::new(TsDecl::Interface {
6308 name: "Box".to_string(),
6309 type_params: vec!["T".to_string()],
6310 members: vec![TsTypeMember::readonly_prop("value", TsType::named("T"))],
6311 })),
6312 None,
6313 ));
6314 let printed = print(&program, "x.bynk", "", "x.ts");
6315 assert_eq!(
6316 printed.text,
6317 "export interface Box<T> {\n readonly value: T;\n}\n"
6318 );
6319 }
6320
6321 /// #1357's own real gap: `TsTypeMember::Method` had no `generics`/`doc`
6322 /// fields — `emit_capability`'s own interface methods are genuinely
6323 /// generic (no monomorphisation) and doc-commented per op. `doc` renders
6324 /// at `TsDecl::Interface`'s own render arm (not `render_type_member`
6325 /// itself, which has no `depth` to give `render_doc_comment`), mirroring
6326 /// `TsObjectEntry::Method.doc`'s own identical split (#1337).
6327 #[test]
6328 fn prints_a_generic_documented_interface_method() {
6329 let mut program = TsProgram::new();
6330 program.push(TsStmt::decl(
6331 TsDecl::Export(Box::new(TsDecl::Interface {
6332 name: "Clock".to_string(),
6333 type_params: Vec::new(),
6334 members: vec![TsTypeMember::Method {
6335 name: "now".to_string(),
6336 generics: vec!["T".to_string()],
6337 params: vec![TsParam {
6338 name: "unit".to_string(),
6339 ty: Some(TsType::named("T")),
6340 optional: false,
6341 }],
6342 ret: TsType::named("number"),
6343 doc: Some("Returns the current time.".to_string()),
6344 }],
6345 })),
6346 None,
6347 ));
6348 let printed = print(&program, "x.bynk", "", "x.ts");
6349 assert_eq!(
6350 printed.text,
6351 "export interface Clock {\n /**\n * Returns the current time.\n */\n now<T>(unit: T): number;\n}\n"
6352 );
6353 }
6354
6355 /// #1359's own real need: `print_class_method` — a class-method sibling
6356 /// of `print_object_entry`'s own fragment-printing entry point,
6357 /// `emit_provider`'s own hand-written class wrapper prints each real
6358 /// method through this directly. `depth` means the *class's* own
6359 /// depth, so at `depth == 0` the method itself lands at `indent(1)`,
6360 /// its body at `indent(2)`, matching `TsDecl::Class`'s own equivalent
6361 /// per-method indent exactly — but with no automatic blank-line
6362 /// insertion of its own (unlike `TsDecl::Class`'s own render arm),
6363 /// since `emit_provider`'s own real spacing has none between methods.
6364 #[test]
6365 fn prints_a_single_class_method_fragment() {
6366 let method = TsClassMethod {
6367 name: "double".to_string(),
6368 private: false,
6369 is_async: true,
6370 params: vec![TsParam {
6371 name: "n".to_string(),
6372 ty: Some(TsType::named("number")),
6373 optional: false,
6374 }],
6375 return_type: Some(TsType::named("number")),
6376 doc: None,
6377 body: vec![TsStmt::return_stmt(
6378 Some(TsExpr::Binary {
6379 op: TsBinaryOp::NullishCoalescing,
6380 left: Box::new(TsExpr::Ident("n".to_string())),
6381 right: Box::new(TsExpr::Lit(TsLit::Num("0".to_string()))),
6382 }),
6383 None,
6384 )],
6385 };
6386 assert_eq!(
6387 print_class_method(&method, 0),
6388 " async double(n: number): number {\n return n ?? 0;\n }\n"
6389 );
6390 }
6391
6392 /// Arc C, slice 21 (#1371): `TsClassMethod.private` — pins both the
6393 /// keyword itself and its render-*before*-`async` ordering directly,
6394 /// the same precedent `TsClassField.private` already has (its own
6395 /// direct test, not left to transitive `bynkc` fixture coverage alone)
6396 /// — review of #1372 caught this one still missing for the sibling
6397 /// field.
6398 #[test]
6399 fn prints_a_private_class_method_fragment() {
6400 let method = TsClassMethod {
6401 name: "loadState".to_string(),
6402 private: true,
6403 is_async: true,
6404 params: Vec::new(),
6405 return_type: Some(TsType::named_with_args(
6406 "Promise",
6407 vec![TsType::named("OrderState")],
6408 )),
6409 doc: None,
6410 body: vec![TsStmt::return_stmt(
6411 Some(TsExpr::Ident("state".to_string())),
6412 None,
6413 )],
6414 };
6415 assert_eq!(
6416 print_class_method(&method, 0),
6417 " private async loadState(): Promise<OrderState> {\n return state;\n }\n"
6418 );
6419 }
6420
6421 /// Arc C, slice 23 (#1375): `TsClassMethod.doc` — the grounding pass's
6422 /// own second predicted gap (#1366), the same need
6423 /// `TsObjectEntry::Method.doc` (#1337) and `TsTypeMember::Method.doc`
6424 /// (#1357) already solved for their own node kinds, closed here for the
6425 /// third and last real method-shaped node in this crate.
6426 #[test]
6427 fn prints_a_documented_class_method_fragment() {
6428 let method = TsClassMethod {
6429 name: "spend".to_string(),
6430 private: false,
6431 is_async: false,
6432 params: Vec::new(),
6433 return_type: Some(TsType::named("void")),
6434 doc: Some("Debits the account.".to_string()),
6435 body: vec![],
6436 };
6437 assert_eq!(
6438 print_class_method(&method, 0),
6439 " /**\n * Debits the account.\n */\n spend(): void {\n }\n"
6440 );
6441 }
6442
6443 /// #1339's own real gap: `TsExpr::Arrow` had no `generics`/`return_type`
6444 /// field — `emit_sum_type`'s own generic payload-constructor arrows
6445 /// (`<T>(name: T): Sum<T> => (...)`) need both. The object-literal body
6446 /// is wrapped in an explicit `Paren` — `Arrow`'s own renderer does not
6447 /// auto-parenthesise an object body the way real JS/TS syntax requires
6448 /// to disambiguate it from a block.
6449 #[test]
6450 fn prints_a_generic_arrow_with_a_parenthesised_object_body() {
6451 let mut program = TsProgram::new();
6452 program.push(TsStmt::expr_stmt(
6453 TsExpr::Arrow {
6454 params: vec![TsParam {
6455 name: "value".to_string(),
6456 ty: Some(TsType::named("T")),
6457 optional: false,
6458 }],
6459 is_async: false,
6460 generics: vec!["T".to_string()],
6461 return_type: Some(TsType::named_with_args("Sum", vec![TsType::named("T")])),
6462 body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Paren(Box::new(
6463 TsExpr::object_entries(vec![
6464 TsObjectEntry::Prop(
6465 "tag".to_string(),
6466 TsExpr::Lit(TsLit::Str("some".to_string())),
6467 ),
6468 TsObjectEntry::Shorthand("value".to_string()),
6469 ]),
6470 ))))),
6471 },
6472 None,
6473 ));
6474 let printed = print(&program, "x.bynk", "", "x.ts");
6475 assert_eq!(
6476 printed.text,
6477 "<T>(value: T): Sum<T> => ({ tag: \"some\", value });\n"
6478 );
6479 }
6480
6481 /// Arc C, slice 33 (#1401): `TsBinaryOp::LessThan` — pins the operator
6482 /// text itself.
6483 #[test]
6484 fn prints_a_less_than_comparison() {
6485 let mut program = TsProgram::new();
6486 program.push(TsStmt::expr_stmt(
6487 TsExpr::Binary {
6488 op: TsBinaryOp::LessThan,
6489 left: Box::new(TsExpr::Ident("a".to_string())),
6490 right: Box::new(TsExpr::Ident("b".to_string())),
6491 },
6492 None,
6493 ));
6494 let printed = print(&program, "x.bynk", "", "x.ts");
6495 assert_eq!(printed.text, "a < b;\n");
6496 }
6497
6498 /// `LessThan` shares `GreaterThan`'s own precedence tier — a nested
6499 /// `Add` under it needs no parens (`+` binds tighter), mirroring
6500 /// `add_binds_tighter_than_greater_than_on_both_sides` for the other
6501 /// relational operator.
6502 #[test]
6503 fn add_binds_tighter_than_less_than() {
6504 let mut program = TsProgram::new();
6505 program.push(TsStmt::expr_stmt(
6506 TsExpr::Binary {
6507 op: TsBinaryOp::LessThan,
6508 left: Box::new(TsExpr::Binary {
6509 op: TsBinaryOp::Add,
6510 left: Box::new(TsExpr::Ident("a".to_string())),
6511 right: Box::new(TsExpr::Ident("b".to_string())),
6512 }),
6513 right: Box::new(TsExpr::Ident("c".to_string())),
6514 },
6515 None,
6516 ));
6517 let printed = print(&program, "x.bynk", "", "x.ts");
6518 assert_eq!(printed.text, "a + b < c;\n");
6519 }
6520
6521 /// Arc C, slice 34 (`tests_emit.rs` slice D, #1403): `TsBinaryOp::
6522 /// InstanceOf` — pins the keyword operator text itself, sharing
6523 /// `LessThan`/`GreaterThan`'s own precedence tier.
6524 #[test]
6525 fn prints_an_instanceof_check() {
6526 let mut program = TsProgram::new();
6527 program.push(TsStmt::expr_stmt(
6528 TsExpr::Binary {
6529 op: TsBinaryOp::InstanceOf,
6530 left: Box::new(TsExpr::Ident("e".to_string())),
6531 right: Box::new(TsExpr::Ident("ExpectationError".to_string())),
6532 },
6533 None,
6534 ));
6535 let printed = print(&program, "x.bynk", "", "x.ts");
6536 assert_eq!(printed.text, "e instanceof ExpectationError;\n");
6537 }
6538
6539 /// Review of #1404, finding 2: `prints_an_instanceof_check` alone pins
6540 /// the operator text but not the precedence tier the same diff added to
6541 /// `binary_precedence` — it passes identically whether `InstanceOf` sits
6542 /// at tier 5, tier 1, or is missing from that arm entirely. `Add` (tier
6543 /// 6) binds tighter, mirroring `add_binds_tighter_than_less_than`
6544 /// for the other relational operator sharing this tier.
6545 #[test]
6546 fn add_binds_tighter_than_instanceof() {
6547 let mut program = TsProgram::new();
6548 program.push(TsStmt::expr_stmt(
6549 TsExpr::Binary {
6550 op: TsBinaryOp::InstanceOf,
6551 left: Box::new(TsExpr::Binary {
6552 op: TsBinaryOp::Add,
6553 left: Box::new(TsExpr::Ident("a".to_string())),
6554 right: Box::new(TsExpr::Ident("b".to_string())),
6555 }),
6556 right: Box::new(TsExpr::Ident("C".to_string())),
6557 },
6558 None,
6559 ));
6560 let printed = print(&program, "x.bynk", "", "x.ts");
6561 assert_eq!(printed.text, "a + b instanceof C;\n");
6562 }
6563
6564 /// Review of #1404, finding 2 (the more valuable half): a catch clause
6565 /// naturally grows into `e instanceof A || e instanceof B` as a second
6566 /// error type is added. `InstanceOf` (tier 5) binds tighter than `Or`
6567 /// (tier 2), so this must print flat — a wrong tier would silently
6568 /// over-parenthesize it into `(e instanceof A) || (e instanceof B)`.
6569 #[test]
6570 fn instanceof_binds_tighter_than_or_on_both_sides() {
6571 let mut program = TsProgram::new();
6572 program.push(TsStmt::expr_stmt(
6573 TsExpr::Binary {
6574 op: TsBinaryOp::Or,
6575 left: Box::new(TsExpr::Binary {
6576 op: TsBinaryOp::InstanceOf,
6577 left: Box::new(TsExpr::Ident("e".to_string())),
6578 right: Box::new(TsExpr::Ident("A".to_string())),
6579 }),
6580 right: Box::new(TsExpr::Binary {
6581 op: TsBinaryOp::InstanceOf,
6582 left: Box::new(TsExpr::Ident("e".to_string())),
6583 right: Box::new(TsExpr::Ident("B".to_string())),
6584 }),
6585 },
6586 None,
6587 ));
6588 let printed = print(&program, "x.bynk", "", "x.ts");
6589 assert_eq!(printed.text, "e instanceof A || e instanceof B;\n");
6590 }
6591
6592 /// Arc E slice 5 (`serialisation.rs`, #1443): `TsBinaryOp::In` — pins
6593 /// the keyword operator text itself, sharing `LessThan`/`GreaterThan`/
6594 /// `InstanceOf`'s own precedence tier.
6595 #[test]
6596 fn prints_an_in_check() {
6597 let mut program = TsProgram::new();
6598 program.push(TsStmt::expr_stmt(
6599 TsExpr::Binary {
6600 op: TsBinaryOp::In,
6601 left: Box::new(TsExpr::Lit(TsLit::Str("name".to_string()))),
6602 right: Box::new(TsExpr::Ident("obj".to_string())),
6603 },
6604 None,
6605 ));
6606 let printed = print(&program, "x.bynk", "", "x.ts");
6607 assert_eq!(printed.text, "\"name\" in obj;\n");
6608 }
6609
6610 /// Mirrors `add_binds_tighter_than_instanceof`/`add_binds_tighter_than_
6611 /// less_than` for the newest member of this shared precedence tier:
6612 /// `Add` (tier 6) binds tighter than `In` (tier 5).
6613 #[test]
6614 fn add_binds_tighter_than_in() {
6615 let mut program = TsProgram::new();
6616 program.push(TsStmt::expr_stmt(
6617 TsExpr::Binary {
6618 op: TsBinaryOp::In,
6619 left: Box::new(TsExpr::Binary {
6620 op: TsBinaryOp::Add,
6621 left: Box::new(TsExpr::Ident("a".to_string())),
6622 right: Box::new(TsExpr::Ident("b".to_string())),
6623 }),
6624 right: Box::new(TsExpr::Ident("obj".to_string())),
6625 },
6626 None,
6627 ));
6628 let printed = print(&program, "x.bynk", "", "x.ts");
6629 assert_eq!(printed.text, "a + b in obj;\n");
6630 }
6631
6632 /// Mirrors `instanceof_binds_tighter_than_or_on_both_sides`: `In` (tier
6633 /// 5) binds tighter than `Or` (tier 2), so a real
6634 /// `"a" in obj || "b" in obj` reads flat with no parens — a wrong tier
6635 /// would silently over-parenthesize it.
6636 #[test]
6637 fn in_binds_tighter_than_or_on_both_sides() {
6638 let mut program = TsProgram::new();
6639 program.push(TsStmt::expr_stmt(
6640 TsExpr::Binary {
6641 op: TsBinaryOp::Or,
6642 left: Box::new(TsExpr::Binary {
6643 op: TsBinaryOp::In,
6644 left: Box::new(TsExpr::Lit(TsLit::Str("a".to_string()))),
6645 right: Box::new(TsExpr::Ident("obj".to_string())),
6646 }),
6647 right: Box::new(TsExpr::Binary {
6648 op: TsBinaryOp::In,
6649 left: Box::new(TsExpr::Lit(TsLit::Str("b".to_string()))),
6650 right: Box::new(TsExpr::Ident("obj".to_string())),
6651 }),
6652 },
6653 None,
6654 ));
6655 let printed = print(&program, "x.bynk", "", "x.ts");
6656 assert_eq!(printed.text, "\"a\" in obj || \"b\" in obj;\n");
6657 }
6658
6659 /// Review of #1471, finding 1: `TsBinaryOp::GreaterThanEq`/`LessThanEq`
6660 /// landed with no direct `bynk-ts` test — only pinned indirectly through
6661 /// `bynk-emit`'s own fixture goldens. This test and the four below close
6662 /// that locality gap, mirroring the coverage every other operator on
6663 /// this precedence tier already has (`prints_an_in_check`/`add_binds_
6664 /// tighter_than_in`/`in_binds_tighter_than_or_on_both_sides`).
6665 #[test]
6666 fn prints_a_greater_than_or_equal_comparison() {
6667 let mut program = TsProgram::new();
6668 program.push(TsStmt::expr_stmt(
6669 TsExpr::Binary {
6670 op: TsBinaryOp::GreaterThanEq,
6671 left: Box::new(TsExpr::Ident("a".to_string())),
6672 right: Box::new(TsExpr::Ident("b".to_string())),
6673 },
6674 None,
6675 ));
6676 let printed = print(&program, "x.bynk", "", "x.ts");
6677 assert_eq!(printed.text, "a >= b;\n");
6678 }
6679
6680 #[test]
6681 fn prints_a_less_than_or_equal_comparison() {
6682 let mut program = TsProgram::new();
6683 program.push(TsStmt::expr_stmt(
6684 TsExpr::Binary {
6685 op: TsBinaryOp::LessThanEq,
6686 left: Box::new(TsExpr::Ident("a".to_string())),
6687 right: Box::new(TsExpr::Ident("b".to_string())),
6688 },
6689 None,
6690 ));
6691 let printed = print(&program, "x.bynk", "", "x.ts");
6692 assert_eq!(printed.text, "a <= b;\n");
6693 }
6694
6695 /// Pins `GreaterThanEq`/`LessThanEq`'s own tier-5 placement — the real
6696 /// `pred_condition_and_message` `InRange` shape (#1471) nests both under
6697 /// `And` (tier 3) and must print flat, with no parens on either side. A
6698 /// regression that demoted either operator to `And`'s own tier or below
6699 /// would silently wrap this in parens instead.
6700 #[test]
6701 fn greater_than_eq_and_less_than_eq_nest_flat_under_and() {
6702 let mut program = TsProgram::new();
6703 program.push(TsStmt::expr_stmt(
6704 TsExpr::Binary {
6705 op: TsBinaryOp::And,
6706 left: Box::new(TsExpr::Binary {
6707 op: TsBinaryOp::GreaterThanEq,
6708 left: Box::new(TsExpr::Ident("value".to_string())),
6709 right: Box::new(TsExpr::Lit(TsLit::Num("0".to_string()))),
6710 }),
6711 right: Box::new(TsExpr::Binary {
6712 op: TsBinaryOp::LessThanEq,
6713 left: Box::new(TsExpr::Ident("value".to_string())),
6714 right: Box::new(TsExpr::Lit(TsLit::Num("100".to_string()))),
6715 }),
6716 },
6717 None,
6718 ));
6719 let printed = print(&program, "x.bynk", "", "x.ts");
6720 assert_eq!(printed.text, "value >= 0 && value <= 100;\n");
6721 }
6722
6723 /// Same tier, *different* operator: `render_binary_operand`'s own doc
6724 /// says equal precedence still parenthesises unless the two sides are
6725 /// the exact same associative operator — `GreaterThan`/`GreaterThanEq`
6726 /// are equal-tier but distinct, so nesting one under the other must
6727 /// still keep its parens (the `_ => true` fallback of the equal-
6728 /// precedence match, newly reachable now this tier has more than one
6729 /// non-keyword operator).
6730 #[test]
6731 fn a_greater_than_eq_operand_of_a_different_relational_op_still_parenthesises() {
6732 let mut program = TsProgram::new();
6733 program.push(TsStmt::expr_stmt(
6734 TsExpr::Binary {
6735 op: TsBinaryOp::GreaterThan,
6736 left: Box::new(TsExpr::Ident("a".to_string())),
6737 right: Box::new(TsExpr::Binary {
6738 op: TsBinaryOp::GreaterThanEq,
6739 left: Box::new(TsExpr::Ident("b".to_string())),
6740 right: Box::new(TsExpr::Ident("c".to_string())),
6741 }),
6742 },
6743 None,
6744 ));
6745 let printed = print(&program, "x.bynk", "", "x.ts");
6746 assert_eq!(printed.text, "a > (b >= c);\n");
6747 }
6748
6749 /// `GreaterThanEq`/`LessThanEq` nested in themselves are NOT associative
6750 /// (`a >= b >= c` does not parse as `a >= (b >= c)`), unlike `||`/`&&` —
6751 /// so, unlike `in_binds_tighter_than_or_on_both_sides`'s own flat `In`
6752 /// chain, a same-operator nesting here must still parenthesise.
6753 #[test]
6754 fn a_right_nested_greater_than_eq_chain_keeps_its_parens() {
6755 let mut program = TsProgram::new();
6756 program.push(TsStmt::expr_stmt(
6757 TsExpr::Binary {
6758 op: TsBinaryOp::GreaterThanEq,
6759 left: Box::new(TsExpr::Ident("a".to_string())),
6760 right: Box::new(TsExpr::Binary {
6761 op: TsBinaryOp::GreaterThanEq,
6762 left: Box::new(TsExpr::Ident("b".to_string())),
6763 right: Box::new(TsExpr::Ident("c".to_string())),
6764 }),
6765 },
6766 None,
6767 ));
6768 let printed = print(&program, "x.bynk", "", "x.ts");
6769 assert_eq!(printed.text, "a >= (b >= c);\n");
6770 }
6771}