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