Skip to main content

sigil_stitch/
code_block.rs

1use crate::code_node::{CodeNode, parts_args_to_nodes};
2use crate::import::ImportRef;
3use crate::lang::CodeLang;
4use crate::type_name::TypeName;
5
6/// Argument-consuming format specifier kinds.
7///
8/// This is the single source of truth for what interpolation specifiers exist.
9/// Both the library's `parse_format()` and the `sigil_quote!` macro's codegen
10/// map to these same logical kinds. The macro crate cannot import this type
11/// (proc-macro dependency cycle), but the format characters are shared: the
12/// macro emits `%T`/`%N`/`%S`/`%L` strings that `parse_format` then parses
13/// via [`Specifier::from_format_char`].
14///
15/// Adding a variant here without handling it in `parse_format` and
16/// `parts_args_to_nodes` will cause exhaustiveness errors.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
18pub enum Specifier {
19    /// `%T` / `$T` — type reference (consumes `Arg::TypeName`).
20    Type,
21    /// `%N` / `$N` — name identifier (consumes `Arg::Name`).
22    Name,
23    /// `%S` / `$S` — string literal (consumes `Arg::StringLit`).
24    StringLit,
25    /// `%L` / `$L` / `$C` — literal value or nested code block (consumes `Arg::Literal` or `Arg::Code`).
26    Literal,
27}
28
29impl Specifier {
30    /// Map a format-string character to a specifier.
31    ///
32    /// Returns `None` for characters that are not argument-consuming specifiers
33    /// (e.g. `W`, `>`, `<`, `[`, `]`, `%`).
34    pub fn from_format_char(ch: char) -> Option<Self> {
35        match ch {
36            'T' => Some(Self::Type),
37            'N' => Some(Self::Name),
38            'S' => Some(Self::StringLit),
39            'L' => Some(Self::Literal),
40            _ => None,
41        }
42    }
43
44    /// The format-string character for this specifier.
45    pub fn format_char(self) -> char {
46        match self {
47            Self::Type => 'T',
48            Self::Name => 'N',
49            Self::StringLit => 'S',
50            Self::Literal => 'L',
51        }
52    }
53
54    /// All defined specifier variants.
55    pub fn all() -> &'static [Self] {
56        &[Self::Type, Self::Name, Self::StringLit, Self::Literal]
57    }
58}
59
60/// A parsed format specifier from a format string.
61#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
62pub(crate) enum FormatPart {
63    /// Literal text (no interpolation).
64    Literal(String),
65    /// An argument-consuming specifier (`%T`, `%N`, `%S`, `%L`).
66    Arg(Specifier),
67    /// `%W` - soft line break point (no argument consumed).
68    Wrap,
69    /// `%>` - increase indent (no argument consumed).
70    Indent,
71    /// `%<` - decrease indent (no argument consumed).
72    Dedent,
73    /// `%[` - statement begin (no argument consumed).
74    StatementBegin,
75    /// `%]` - statement end (no argument consumed).
76    StatementEnd,
77    /// Newline.
78    Newline,
79    /// Block open delimiter — resolved at render time via `lang.block_syntax().block_open`.
80    /// Emitted by control-flow builders; braces for TS/Rust/Go, colon for Python.
81    BlockOpen,
82    /// Block open with an overridden delimiter (not resolved via `lang.block_syntax().block_open`).
83    /// Emitted by `begin_control_flow_with_open` for constructs that need a
84    /// different opener than the language default (e.g., Haskell `where` vs `=`).
85    BlockOpenOverride(String),
86    /// Block close delimiter (terminal) — resolved at render time via `lang.block_syntax().block_close`.
87    /// Emitted by `end_control_flow`. When non-empty, also emits a trailing newline.
88    /// When empty (indent-only languages like OCaml/Haskell/Python), emits nothing.
89    BlockClose,
90    /// Block close delimiter (transitional) — resolved at render time via
91    /// `lang.block_syntax().block_close` + `" "`. Used by `next_control_flow` to emit `} else`.
92    /// When `block_close()` is empty, emits nothing (Python: dedent-only transition).
93    BlockCloseTransition,
94}
95
96/// An argument to a CodeBlock format string.
97#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
98pub enum Arg {
99    /// A type name reference (used by `%T`).
100    TypeName(TypeName),
101    /// A name string (used by `%N`).
102    Name(String),
103    /// A string literal value (used by `%S`).
104    StringLit(String),
105    /// A literal string value or nested code block (used by `%L`).
106    Literal(String),
107    /// A nested code block (used by `%L`).
108    Code(CodeBlock),
109}
110
111/// An immutable code fragment with embedded type references.
112///
113/// `CodeBlock` is the core composition primitive in sigil-stitch. It stores a tree
114/// of [`CodeNode`] nodes — self-contained IR nodes produced from format strings
115/// (`%T`, `%N`, `%S`, `%L`, etc.). CodeBlocks are produced by [`CodeBlockBuilder`]
116/// and consumed by [`FileSpec`](crate::spec::file_spec::FileSpec) during rendering.
117/// Type references embedded via `%T` are automatically tracked for import resolution.
118///
119/// Use [`CodeBlock::builder()`] to construct a block incrementally, or
120/// [`CodeBlock::of()`] for simple one-liners.
121///
122/// # Examples
123///
124/// ```
125/// use sigil_stitch::code_block::CodeBlock;
126/// use sigil_stitch::lang::typescript::TypeScript;
127/// use sigil_stitch::type_name::TypeName;
128///
129/// // One-liner with a type reference:
130/// let user = TypeName::importable("./models", "User");
131/// let block = CodeBlock::of("const u: %T = getUser()", (user,)).unwrap();
132///
133/// // Multi-statement block via builder:
134/// let mut cb = CodeBlock::builder();
135/// cb.add_statement("const x = 1", ());
136/// cb.add_statement("const y = 2", ());
137/// let block = cb.build().unwrap();
138/// ```
139#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
140pub struct CodeBlock {
141    pub(crate) nodes: Vec<CodeNode>,
142}
143
144impl CodeBlock {
145    /// Create a new CodeBlockBuilder.
146    pub fn builder() -> CodeBlockBuilder {
147        CodeBlockBuilder::new()
148    }
149
150    /// Create a CodeBlock from a single format string and arguments.
151    pub fn of(format: &str, args: impl IntoArgs) -> Result<Self, crate::error::SigilStitchError> {
152        let mut builder = CodeBlockBuilder::new();
153        builder.add(format, args);
154        builder.build()
155    }
156
157    /// Check if this code block is empty.
158    pub fn is_empty(&self) -> bool {
159        self.nodes.is_empty()
160    }
161
162    /// Check if this code block ends with a newline or block close.
163    pub fn ends_with_newline_or_block_close(&self) -> bool {
164        fn check_last(nodes: &[CodeNode]) -> bool {
165            match nodes.last() {
166                Some(CodeNode::Newline | CodeNode::BlockClose) => true,
167                Some(CodeNode::Sequence(children)) => check_last(children),
168                Some(CodeNode::Nested(inner)) => check_last(&inner.nodes),
169                _ => false,
170            }
171        }
172        check_last(&self.nodes)
173    }
174
175    /// Collect all import references from this code block.
176    pub fn collect_imports(&self, out: &mut Vec<ImportRef>) {
177        crate::import_collector::walk_nodes(&self.nodes, out);
178    }
179
180    /// Render this code block to a string without import resolution.
181    ///
182    /// Creates a temporary empty import group and renders using the given
183    /// language and target line width. Useful for quick one-off rendering
184    /// in tests or when import management is not needed.
185    pub fn render_standalone(
186        &self,
187        lang: &dyn CodeLang,
188        width: usize,
189    ) -> Result<String, crate::error::SigilStitchError> {
190        let imports = crate::import::ImportGroup::new();
191        let mut renderer = crate::code_renderer::CodeRenderer::new(lang, &imports, width);
192        renderer.render(self)
193    }
194}
195
196/// Builder for constructing [`CodeBlock`] instances.
197///
198/// Provides methods for adding formatted code fragments, statements, control
199/// flow blocks, and nested code blocks. Format strings use `%T`, `%N`, `%S`,
200/// `%L` for type/name/string/literal substitution, and `%W`, `%>`, `%<` for
201/// soft line breaks and indentation.
202///
203/// # Examples
204///
205/// ```
206/// use sigil_stitch::code_block::CodeBlock;
207/// use sigil_stitch::lang::typescript::TypeScript;
208///
209/// let mut cb = CodeBlock::builder();
210/// cb.begin_control_flow("if (x > 0)", ());
211/// cb.add_statement("return x", ());
212/// cb.next_control_flow("else", ());
213/// cb.add_statement("return -x", ());
214/// cb.end_control_flow();
215/// let block = cb.build().unwrap();
216/// ```
217#[derive(Debug)]
218pub struct CodeBlockBuilder {
219    nodes: Vec<CodeNode>,
220    indent_depth: i32,
221    errors: Vec<crate::error::SigilStitchError>,
222}
223
224impl CodeBlockBuilder {
225    /// Create a new empty code block builder.
226    pub fn new() -> Self {
227        Self {
228            nodes: Vec::new(),
229            indent_depth: 0,
230            errors: Vec::new(),
231        }
232    }
233
234    /// Add a formatted code fragment.
235    pub fn add(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
236        let arg_vec = args.into_args();
237        let parsed = match parse_format(format) {
238            Ok(parts) => parts,
239            Err(err) => {
240                self.errors.push(err);
241                return self;
242            }
243        };
244
245        let consuming_specifiers: Vec<String> = parsed
246            .iter()
247            .filter_map(|p| match p {
248                FormatPart::Arg(s) => Some(format!("%{}", s.format_char())),
249                _ => None,
250            })
251            .collect();
252
253        let expected_args = consuming_specifiers.len();
254
255        if expected_args != arg_vec.len() {
256            let actual_arg_kinds: Vec<String> = arg_vec
257                .iter()
258                .map(|a| match a {
259                    Arg::TypeName(_) => "TypeName".to_string(),
260                    Arg::Name(_) => "Name".to_string(),
261                    Arg::StringLit(_) => "StringLit".to_string(),
262                    Arg::Literal(_) => "Literal".to_string(),
263                    Arg::Code(_) => "Code".to_string(),
264                })
265                .collect();
266            self.errors
267                .push(crate::error::SigilStitchError::FormatArgCount {
268                    format: format.to_string(),
269                    expected: expected_args,
270                    actual: arg_vec.len(),
271                    expected_specifiers: consuming_specifiers,
272                    actual_arg_kinds,
273                });
274            return self;
275        }
276
277        let new_nodes = parts_args_to_nodes(&parsed, &arg_vec);
278        self.nodes.extend(new_nodes);
279        self
280    }
281
282    /// Add a statement (wraps in %[...%] and appends language semicolon).
283    pub fn add_statement(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
284        self.nodes.push(CodeNode::StatementBegin);
285        self.add(format, args);
286        self.nodes.push(CodeNode::StatementEnd);
287        self.nodes.push(CodeNode::Newline);
288        self
289    }
290
291    /// Begin a control flow block (e.g., "if foo" -> "if foo {\n" + indent).
292    pub fn begin_control_flow(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
293        self.add(format, args);
294        self.nodes.push(CodeNode::BlockOpen);
295        self.nodes.push(CodeNode::Newline);
296        self.nodes.push(CodeNode::Indent);
297        self.indent_depth += 1;
298        self
299    }
300
301    /// Begin a control flow block with a custom block-open string.
302    ///
303    /// Like [`begin_control_flow`](Self::begin_control_flow), but uses
304    /// `custom_open` instead of the language's `block_open()`. Pass `""`
305    /// to suppress the block opener entirely (e.g., OCaml `match x with`).
306    pub fn begin_control_flow_with_open(
307        &mut self,
308        format: &str,
309        args: impl IntoArgs,
310        custom_open: &str,
311    ) -> &mut Self {
312        self.add(format, args);
313        if !custom_open.is_empty() {
314            self.nodes
315                .push(CodeNode::BlockOpenOverride(custom_open.to_string()));
316        }
317        self.nodes.push(CodeNode::Newline);
318        self.nodes.push(CodeNode::Indent);
319        self.indent_depth += 1;
320        self
321    }
322
323    /// Add an else/else-if clause (e.g., "} else {" or "elif ...:" for Python).
324    pub fn next_control_flow(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
325        self.nodes.push(CodeNode::Dedent);
326        self.indent_depth -= 1;
327        self.nodes.push(CodeNode::BlockCloseTransition);
328        self.add(format, args);
329        self.nodes.push(CodeNode::BlockOpen);
330        self.nodes.push(CodeNode::Newline);
331        self.nodes.push(CodeNode::Indent);
332        self.indent_depth += 1;
333        self
334    }
335
336    /// End a control flow block (emits "}" or nothing for Python, and decreases indent).
337    pub fn end_control_flow(&mut self) -> &mut Self {
338        self.nodes.push(CodeNode::Dedent);
339        self.indent_depth -= 1;
340        self.nodes.push(CodeNode::BlockClose);
341        self
342    }
343
344    /// Add a blank line.
345    pub fn add_line(&mut self) -> &mut Self {
346        self.nodes.push(CodeNode::Newline);
347        self
348    }
349
350    /// Add an inline comment.
351    pub fn add_comment(&mut self, text: &str) -> &mut Self {
352        self.nodes.push(CodeNode::Comment(text.to_string()));
353        self.nodes.push(CodeNode::Newline);
354        self
355    }
356
357    /// Add a nested CodeBlock inline.
358    pub fn add_code(&mut self, block: CodeBlock) -> &mut Self {
359        self.nodes.push(CodeNode::Nested(block));
360        self
361    }
362
363    /// Build the immutable CodeBlock.
364    ///
365    /// Returns an error if any format string had an argument count mismatch,
366    /// or if indent depth is not balanced (unmatched
367    /// begin_control_flow / end_control_flow).
368    pub fn build(self) -> Result<CodeBlock, crate::error::SigilStitchError> {
369        if let Some(err) = self.errors.into_iter().next() {
370            return Err(err);
371        }
372        if self.indent_depth != 0 {
373            return Err(crate::error::SigilStitchError::UnbalancedIndent {
374                depth: self.indent_depth,
375            });
376        }
377        Ok(CodeBlock { nodes: self.nodes })
378    }
379
380    /// Build the CodeBlock, panicking on error.
381    pub fn build_unwrap(self) -> CodeBlock {
382        self.build().unwrap()
383    }
384}
385
386impl Default for CodeBlockBuilder {
387    fn default() -> Self {
388        Self::new()
389    }
390}
391
392/// Parse a format string into FormatParts.
393fn parse_format(format: &str) -> Result<Vec<FormatPart>, crate::error::SigilStitchError> {
394    let mut parts = Vec::new();
395    let mut current_literal = String::new();
396    let mut chars = format.char_indices().peekable();
397
398    while let Some(&(_, ch)) = chars.peek() {
399        if ch == '%' {
400            chars.next();
401            if let Some(&(_, spec)) = chars.peek() {
402                chars.next();
403                let part = match spec {
404                    'W' => Some(FormatPart::Wrap),
405                    '>' => Some(FormatPart::Indent),
406                    '<' => Some(FormatPart::Dedent),
407                    '[' => Some(FormatPart::StatementBegin),
408                    ']' => Some(FormatPart::StatementEnd),
409                    '%' => {
410                        current_literal.push('%');
411                        continue;
412                    }
413                    _ => match Specifier::from_format_char(spec) {
414                        Some(s) => Some(FormatPart::Arg(s)),
415                        None => {
416                            return Err(crate::error::SigilStitchError::InvalidFormatSpecifier {
417                                format: format.to_string(),
418                                specifier: spec,
419                            });
420                        }
421                    },
422                };
423                if let Some(part) = part {
424                    if !current_literal.is_empty() {
425                        parts.push(FormatPart::Literal(std::mem::take(&mut current_literal)));
426                    }
427                    parts.push(part);
428                }
429            }
430        } else if ch == '\n' {
431            chars.next();
432            if !current_literal.is_empty() {
433                parts.push(FormatPart::Literal(std::mem::take(&mut current_literal)));
434            }
435            parts.push(FormatPart::Newline);
436        } else {
437            chars.next();
438            current_literal.push(ch);
439        }
440    }
441
442    if !current_literal.is_empty() {
443        parts.push(FormatPart::Literal(current_literal));
444    }
445
446    Ok(parts)
447}
448
449// === IntoArgs trait and implementations ===
450
451/// Trait for converting various types into a `Vec<Arg>` for format strings.
452///
453/// Implemented for `()` (no args), `TypeName`, `&str`, `String`, `CodeBlock`,
454/// `NameArg`, `StringLitArg`, `Vec<Arg>`, and tuples up to 8 elements.
455/// Bare strings convert to `Arg::Literal`; use [`NameArg`] or [`StringLitArg`]
456/// wrappers to target `%N` or `%S` specifiers instead.
457pub trait IntoArgs {
458    /// Convert into a vector of format arguments.
459    fn into_args(self) -> Vec<Arg>;
460}
461
462/// Empty args (for format strings with no specifiers).
463impl IntoArgs for () {
464    fn into_args(self) -> Vec<Arg> {
465        Vec::new()
466    }
467}
468
469/// Single TypeName arg.
470impl IntoArgs for TypeName {
471    fn into_args(self) -> Vec<Arg> {
472        vec![Arg::TypeName(self)]
473    }
474}
475
476/// Single string arg (as literal).
477impl IntoArgs for &str {
478    fn into_args(self) -> Vec<Arg> {
479        vec![Arg::Literal(self.to_string())]
480    }
481}
482
483impl IntoArgs for String {
484    fn into_args(self) -> Vec<Arg> {
485        vec![Arg::Literal(self)]
486    }
487}
488
489/// Single CodeBlock arg.
490impl IntoArgs for CodeBlock {
491    fn into_args(self) -> Vec<Arg> {
492        vec![Arg::Code(self)]
493    }
494}
495
496/// Pre-built args vector (used by specs that dynamically build format strings).
497impl IntoArgs for Vec<Arg> {
498    fn into_args(self) -> Vec<Arg> {
499        self
500    }
501}
502
503/// A wrapper to explicitly mark a string as a Name arg (for `%N`).
504///
505/// By default, bare strings convert to `Arg::Literal` (for `%L`). Wrap with
506/// `NameArg` when your format string uses `%N`.
507///
508/// # Examples
509///
510/// ```
511/// use sigil_stitch::code_block::{CodeBlock, NameArg};
512/// use sigil_stitch::lang::typescript::TypeScript;
513///
514/// let mut cb = CodeBlock::builder();
515/// cb.add("this.%N()", (NameArg("getData".to_string()),));
516/// let block = cb.build().unwrap();
517/// ```
518pub struct NameArg(pub String);
519
520impl IntoArgs for NameArg {
521    fn into_args(self) -> Vec<Arg> {
522        vec![Arg::Name(self.0)]
523    }
524}
525
526/// A wrapper to explicitly mark a string as a StringLit arg (for `%S`).
527///
528/// By default, bare strings convert to `Arg::Literal` (for `%L`). Wrap with
529/// `StringLitArg` when your format string uses `%S` to emit a quoted string.
530///
531/// # Examples
532///
533/// ```
534/// use sigil_stitch::code_block::{CodeBlock, StringLitArg};
535/// use sigil_stitch::lang::typescript::TypeScript;
536///
537/// let mut cb = CodeBlock::builder();
538/// cb.add_statement("const msg = %S", (StringLitArg("hello".to_string()),));
539/// let block = cb.build().unwrap();
540/// ```
541pub struct StringLitArg(pub String);
542
543impl IntoArgs for StringLitArg {
544    fn into_args(self) -> Vec<Arg> {
545        vec![Arg::StringLit(self.0)]
546    }
547}
548
549// Individual Arg conversions.
550impl From<TypeName> for Arg {
551    fn from(tn: TypeName) -> Self {
552        Arg::TypeName(tn)
553    }
554}
555
556impl From<&str> for Arg {
557    fn from(s: &str) -> Self {
558        Arg::Literal(s.to_string())
559    }
560}
561
562impl From<String> for Arg {
563    fn from(s: String) -> Self {
564        Arg::Literal(s)
565    }
566}
567
568impl From<CodeBlock> for Arg {
569    fn from(cb: CodeBlock) -> Self {
570        Arg::Code(cb)
571    }
572}
573
574impl From<NameArg> for Arg {
575    fn from(n: NameArg) -> Self {
576        Arg::Name(n.0)
577    }
578}
579
580impl From<StringLitArg> for Arg {
581    fn from(s: StringLitArg) -> Self {
582        Arg::StringLit(s.0)
583    }
584}
585
586// Tuple implementations for IntoArgs.
587// Each element must implement Into<Arg>.
588
589macro_rules! impl_into_args_tuple {
590    ($($idx:tt $T:ident),+) => {
591        impl<$($T: Into<Arg>),+> IntoArgs for ($($T,)+) {
592            fn into_args(self) -> Vec<Arg> {
593                vec![$(self.$idx.into()),+]
594            }
595        }
596    };
597}
598
599impl_into_args_tuple!(0 A);
600impl_into_args_tuple!(0 A, 1 B);
601impl_into_args_tuple!(0 A, 1 B, 2 C);
602impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D);
603impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E);
604impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
605impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
606impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
607
608#[cfg(test)]
609mod tests {
610    use super::*;
611    use crate::code_node::CodeNode;
612
613    #[test]
614    fn test_parse_all_specifiers() {
615        let parts = parse_format("hello %T world %N %S %L %W %> %< %[ %]").unwrap();
616        assert!(parts.contains(&FormatPart::Arg(Specifier::Type)));
617        assert!(parts.contains(&FormatPart::Arg(Specifier::Name)));
618        assert!(parts.contains(&FormatPart::Arg(Specifier::StringLit)));
619        assert!(parts.contains(&FormatPart::Arg(Specifier::Literal)));
620        assert!(parts.contains(&FormatPart::Wrap));
621        assert!(parts.contains(&FormatPart::Indent));
622        assert!(parts.contains(&FormatPart::Dedent));
623        assert!(parts.contains(&FormatPart::StatementBegin));
624        assert!(parts.contains(&FormatPart::StatementEnd));
625    }
626
627    #[test]
628    fn test_parse_literal_percent() {
629        let parts = parse_format("100%%").unwrap();
630        assert_eq!(parts, vec![FormatPart::Literal("100%".to_string())]);
631    }
632
633    #[test]
634    fn test_parse_empty() {
635        let parts = parse_format("").unwrap();
636        assert!(parts.is_empty());
637    }
638
639    #[test]
640    fn test_parse_newlines() {
641        let parts = parse_format("line1\nline2").unwrap();
642        assert_eq!(
643            parts,
644            vec![
645                FormatPart::Literal("line1".to_string()),
646                FormatPart::Newline,
647                FormatPart::Literal("line2".to_string()),
648            ]
649        );
650    }
651
652    #[test]
653    fn test_builder_add_statement() {
654        let mut b = CodeBlock::builder();
655        b.add_statement("const x = %L", "42");
656        let block = b.build().unwrap();
657
658        assert!(!block.is_empty());
659        let has_stmt_begin = block
660            .nodes
661            .iter()
662            .any(|n| matches!(n, CodeNode::StatementBegin));
663        let has_stmt_end = block
664            .nodes
665            .iter()
666            .any(|n| matches!(n, CodeNode::StatementEnd));
667        assert!(has_stmt_begin);
668        assert!(has_stmt_end);
669    }
670
671    #[test]
672    fn test_builder_control_flow() {
673        let mut b = CodeBlock::builder();
674        b.begin_control_flow("if (x > 0)", ());
675        b.add_statement("return x", ());
676        b.end_control_flow();
677        let block = b.build().unwrap();
678
679        assert!(!block.is_empty());
680    }
681
682    #[test]
683    fn test_builder_unbalanced_control_flow() {
684        let mut b = CodeBlock::builder();
685        b.begin_control_flow("if (x)", ());
686        b.add_statement("y()", ());
687        // missing end_control_flow
688        let result = b.build();
689        assert!(result.is_err());
690        assert!(result.unwrap_err().to_string().contains("unbalanced"));
691    }
692
693    #[test]
694    fn test_mismatched_arg_count() {
695        let mut b = CodeBlock::builder();
696        b.add("%T", ());
697        let result = b.build();
698        assert!(result.is_err());
699        assert!(
700            result
701                .unwrap_err()
702                .to_string()
703                .contains("expects 1 args but got 0")
704        );
705    }
706
707    #[test]
708    fn test_into_args_tuple() {
709        let user = TypeName::importable("./models", "User");
710        let args: Vec<Arg> = (user, "hello").into_args();
711        assert_eq!(args.len(), 2);
712        assert!(matches!(&args[0], Arg::TypeName(_)));
713        assert!(matches!(&args[1], Arg::Literal(s) if s == "hello"));
714    }
715
716    #[test]
717    fn test_into_args_single_typename() {
718        let user = TypeName::importable("./models", "User");
719        let args: Vec<Arg> = user.into_args();
720        assert_eq!(args.len(), 1);
721    }
722
723    #[test]
724    fn test_into_args_single_str() {
725        let args: Vec<Arg> = "hello".into_args();
726        assert_eq!(args.len(), 1);
727        assert!(matches!(&args[0], Arg::Literal(s) if s == "hello"));
728    }
729
730    #[test]
731    fn test_collect_imports_from_codeblock() {
732        let user = TypeName::importable("./models", "User");
733        let tag = TypeName::importable("./models", "Tag");
734        let mut b = CodeBlock::builder();
735        b.add_statement("const u: %T = getUser()", (user,));
736        b.add_statement("const t: %T = getTag()", (tag,));
737        let block = b.build().unwrap();
738
739        let mut imports = Vec::new();
740        block.collect_imports(&mut imports);
741        assert_eq!(imports.len(), 2);
742        assert_eq!(imports[0].name, "User");
743        assert_eq!(imports[1].name, "Tag");
744    }
745
746    #[test]
747    fn test_nested_codeblock_imports() {
748        let user = TypeName::importable("./models", "User");
749        let mut ib = CodeBlock::builder();
750        ib.add_statement("return new %T()", (user,));
751        let inner = ib.build().unwrap();
752
753        let mut ob = CodeBlock::builder();
754        ob.add_code(inner);
755        let outer = ob.build().unwrap();
756
757        let mut imports = Vec::new();
758        outer.collect_imports(&mut imports);
759        assert_eq!(imports.len(), 1);
760        assert_eq!(imports[0].name, "User");
761    }
762
763    #[test]
764    fn test_name_arg() {
765        let mut b = CodeBlock::builder();
766        b.add("this.%N()", (NameArg("getUser".to_string()),));
767        let block = b.build().unwrap();
768        let has_name = block
769            .nodes
770            .iter()
771            .any(|n| matches!(n, CodeNode::NameRef(s) if s == "getUser"));
772        assert!(has_name);
773    }
774
775    #[test]
776    fn test_string_lit_arg() {
777        let mut b = CodeBlock::builder();
778        b.add("const x = %S", (StringLitArg("hello".to_string()),));
779        let block = b.build().unwrap();
780        let has_str_lit = block
781            .nodes
782            .iter()
783            .any(|n| matches!(n, CodeNode::StringLit(s) if s == "hello"));
784        assert!(has_str_lit);
785    }
786
787    #[test]
788    fn test_invalid_format_specifier() {
789        let mut b = CodeBlock::builder();
790        b.add("hello %X world", ());
791        let result = b.build();
792        assert!(result.is_err());
793        let err_msg = result.unwrap_err().to_string();
794        assert!(err_msg.contains("invalid format specifier"));
795        assert!(err_msg.contains("%X"));
796    }
797
798    #[test]
799    fn test_parse_format_invalid_specifier_returns_error() {
800        let result = parse_format("foo %Z bar");
801        assert!(result.is_err());
802        let err_msg = result.unwrap_err().to_string();
803        assert!(err_msg.contains("invalid format specifier"));
804        assert!(err_msg.contains("%Z"));
805    }
806
807    #[test]
808    fn test_mismatched_arg_count_includes_specifiers_and_kinds() {
809        let user = TypeName::importable("./models", "User");
810        let mut b = CodeBlock::builder();
811        b.add("%T %S %L", (user,));
812        let result = b.build();
813        assert!(result.is_err());
814        let err_msg = result.unwrap_err().to_string();
815        assert!(err_msg.contains("expects 3 args but got 1"));
816        assert!(err_msg.contains("%T"));
817        assert!(err_msg.contains("%S"));
818        assert!(err_msg.contains("%L"));
819        assert!(err_msg.contains("TypeName"));
820    }
821
822    #[test]
823    fn test_begin_control_flow_with_open_non_empty() {
824        let mut b = CodeBlock::builder();
825        b.begin_control_flow_with_open("class Functor f", (), " where");
826        b.add_statement("fmap :: (a -> b) -> f a -> f b", ());
827        b.end_control_flow();
828        let block = b.build().unwrap();
829        let has_override = block
830            .nodes
831            .iter()
832            .any(|n| matches!(n, CodeNode::BlockOpenOverride(s) if s == " where"));
833        assert!(has_override, "should contain BlockOpenOverride(\" where\")");
834        let has_block_open = block.nodes.iter().any(|n| matches!(n, CodeNode::BlockOpen));
835        assert!(
836            !has_block_open,
837            "should NOT contain BlockOpen when override is used"
838        );
839    }
840
841    #[test]
842    fn test_begin_control_flow_with_open_empty() {
843        let mut b = CodeBlock::builder();
844        b.begin_control_flow_with_open("match x with", (), "");
845        b.add("| Red -> red", ());
846        b.add_line();
847        b.end_control_flow();
848        let block = b.build().unwrap();
849        let has_override = block
850            .nodes
851            .iter()
852            .any(|n| matches!(n, CodeNode::BlockOpenOverride(_)));
853        assert!(
854            !has_override,
855            "empty custom_open should skip BlockOpenOverride"
856        );
857        let has_block_open = block.nodes.iter().any(|n| matches!(n, CodeNode::BlockOpen));
858        assert!(!has_block_open, "should NOT contain BlockOpen either");
859    }
860}