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_open_for(condition)`
80    /// falling back to `lang.block_syntax().block_open`. Carries the condition text
81    /// from `begin_control_flow` (e.g., `"if x > 0"`, `"for i in range(10)"`).
82    /// Empty string means no condition (e.g., a bare `{ }` block).
83    BlockOpen(String),
84    /// Terminal block close delimiter — resolved at render time via
85    /// `lang.block_close_for(condition)` falling back to `lang.block_syntax().block_close`.
86    /// Carries the condition from the matching `begin_control_flow`.
87    /// Emits: closer + newline.
88    BlockClose(String),
89    /// Non-terminal block close before a branch keyword (`else`, `elif`, `catch`).
90    /// Like `BlockClose` but emits closer + space (not newline) so the branch
91    /// keyword continues on the same line (e.g., `} else {`).
92    /// Suppressed when `block_syntax().close_on_transition` is `false`.
93    BranchClose(String),
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    /// Access the node tree for rewriting. Used by language rewrite passes.
151    pub fn nodes_mut(&mut self) -> &mut Vec<CodeNode> {
152        &mut self.nodes
153    }
154
155    /// Create a CodeBlock from a single format string and arguments.
156    pub fn of(format: &str, args: impl IntoArgs) -> Result<Self, crate::error::SigilStitchError> {
157        let mut builder = CodeBlockBuilder::new();
158        builder.add(format, args);
159        builder.build()
160    }
161
162    /// Check if this code block is empty.
163    pub fn is_empty(&self) -> bool {
164        self.nodes.is_empty()
165    }
166
167    /// Check if this code block ends with a newline or block close.
168    pub fn ends_with_newline_or_block_close(&self) -> bool {
169        fn check_last(nodes: &[CodeNode]) -> bool {
170            match nodes.last() {
171                Some(CodeNode::Newline | CodeNode::BlockClose(_)) => true,
172                Some(CodeNode::Sequence(children)) => check_last(children),
173                Some(CodeNode::Nested(inner)) => check_last(&inner.nodes),
174                _ => false,
175            }
176        }
177        check_last(&self.nodes)
178    }
179
180    /// Collect all import references from this code block.
181    pub fn collect_imports(&self, out: &mut Vec<ImportRef>) {
182        crate::import_collector::walk_nodes(&self.nodes, out);
183    }
184
185    /// Render this code block to a string without import resolution.
186    ///
187    /// Creates a temporary empty import group and renders using the given
188    /// language and target line width. Useful for quick one-off rendering
189    /// in tests or when import management is not needed.
190    pub fn render_standalone(
191        &self,
192        lang: &dyn CodeLang,
193        width: usize,
194    ) -> Result<String, crate::error::SigilStitchError> {
195        let imports = crate::import::ImportGroup::new();
196        let mut renderer = crate::code_renderer::CodeRenderer::new(lang, &imports, width);
197        renderer.render(self)
198    }
199}
200
201/// Builder for constructing [`CodeBlock`] instances.
202///
203/// Provides methods for adding formatted code fragments, statements, control
204/// flow blocks, and nested code blocks. Format strings use `%T`, `%N`, `%S`,
205/// `%L` for type/name/string/literal substitution, and `%W`, `%>`, `%<` for
206/// soft line breaks and indentation.
207///
208/// # Examples
209///
210/// ```
211/// use sigil_stitch::code_block::CodeBlock;
212/// use sigil_stitch::lang::typescript::TypeScript;
213///
214/// let mut cb = CodeBlock::builder();
215/// cb.begin_control_flow("if (x > 0)", ());
216/// cb.add_statement("return x", ());
217/// cb.next_control_flow("else", ());
218/// cb.add_statement("return -x", ());
219/// cb.end_control_flow();
220/// let block = cb.build().unwrap();
221/// ```
222#[derive(Debug)]
223pub struct CodeBlockBuilder {
224    nodes: Vec<CodeNode>,
225    indent_depth: i32,
226    block_stack: Vec<String>,
227    errors: Vec<crate::error::SigilStitchError>,
228}
229
230impl CodeBlockBuilder {
231    /// Create a new empty code block builder.
232    pub fn new() -> Self {
233        Self {
234            nodes: Vec::new(),
235            indent_depth: 0,
236            block_stack: Vec::new(),
237            errors: Vec::new(),
238        }
239    }
240
241    /// Add a formatted code fragment.
242    pub fn add(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
243        let arg_vec = args.into_args();
244        let parsed = match parse_format(format) {
245            Ok(parts) => parts,
246            Err(err) => {
247                self.errors.push(err);
248                return self;
249            }
250        };
251
252        let consuming_specifiers: Vec<String> = parsed
253            .iter()
254            .filter_map(|p| match p {
255                FormatPart::Arg(s) => Some(format!("%{}", s.format_char())),
256                _ => None,
257            })
258            .collect();
259
260        let expected_args = consuming_specifiers.len();
261
262        if expected_args != arg_vec.len() {
263            let actual_arg_kinds: Vec<String> = arg_vec
264                .iter()
265                .map(|a| match a {
266                    Arg::TypeName(_) => "TypeName".to_string(),
267                    Arg::Name(_) => "Name".to_string(),
268                    Arg::StringLit(_) => "StringLit".to_string(),
269                    Arg::Literal(_) => "Literal".to_string(),
270                    Arg::Code(_) => "Code".to_string(),
271                })
272                .collect();
273            self.errors
274                .push(crate::error::SigilStitchError::FormatArgCount {
275                    format: format.to_string(),
276                    expected: expected_args,
277                    actual: arg_vec.len(),
278                    expected_specifiers: consuming_specifiers,
279                    actual_arg_kinds,
280                });
281            return self;
282        }
283
284        let new_nodes = parts_args_to_nodes(&parsed, &arg_vec);
285        self.nodes.extend(new_nodes);
286        self
287    }
288
289    /// Add a statement (wraps in %[...%] and appends language semicolon).
290    pub fn add_statement(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
291        self.nodes.push(CodeNode::StatementBegin);
292        self.add(format, args);
293        self.nodes.push(CodeNode::StatementEnd);
294        self.nodes.push(CodeNode::Newline);
295        self
296    }
297
298    /// Begin a control flow block (e.g., "if foo" -> "if foo {\n" + indent).
299    ///
300    /// The **raw format string** (not the interpolated result) is stored as
301    /// the condition text and passed to `block_open_for` / `block_close_for`
302    /// at render time, enabling language backends to emit context-aware
303    /// delimiters (e.g., Bash `then`/`fi` for `if`, `do`/`done` for `for`).
304    ///
305    /// Because backends pattern-match on the stored condition (e.g.,
306    /// `condition.starts_with("if ")`), avoid interpolating into the keyword
307    /// prefix — `begin_control_flow("if %L", expr)` works, but
308    /// `begin_control_flow("%L x", some_keyword)` would not be recognized.
309    pub fn begin_control_flow(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
310        let condition = format.to_string();
311        self.block_stack.push(condition.clone());
312        self.add(format, args);
313        self.nodes.push(CodeNode::BlockOpen(condition));
314        self.nodes.push(CodeNode::Newline);
315        self.nodes.push(CodeNode::Indent);
316        self.indent_depth += 1;
317        self
318    }
319
320    /// Add an else/else-if clause (e.g., "} else {" or "elif ...:" for Python).
321    pub fn next_control_flow(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
322        let condition = self.block_stack.last().cloned().unwrap_or_default();
323        self.nodes.push(CodeNode::Dedent);
324        self.indent_depth -= 1;
325        self.nodes.push(CodeNode::BranchClose(condition));
326        self.add(format, args);
327        let new_condition = format.to_string();
328        self.nodes.push(CodeNode::BlockOpen(new_condition));
329        self.nodes.push(CodeNode::Newline);
330        self.nodes.push(CodeNode::Indent);
331        self.indent_depth += 1;
332        self
333    }
334
335    /// End a control flow block (emits "}" or language-specific closer, and decreases indent).
336    pub fn end_control_flow(&mut self) -> &mut Self {
337        let condition = self.block_stack.pop().unwrap_or_default();
338        self.nodes.push(CodeNode::Dedent);
339        self.indent_depth -= 1;
340        self.nodes.push(CodeNode::BlockClose(condition));
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_stores_condition() {
824        let mut b = CodeBlock::builder();
825        b.begin_control_flow("class Functor f", ());
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_open = block
830            .nodes
831            .iter()
832            .any(|n| matches!(n, CodeNode::BlockOpen(s) if s == "class Functor f"));
833        assert!(has_open, "should contain BlockOpen with condition text");
834        let has_close = block
835            .nodes
836            .iter()
837            .any(|n| matches!(n, CodeNode::BlockClose(s) if s == "class Functor f"));
838        assert!(has_close, "should contain BlockClose with condition text");
839    }
840
841    #[test]
842    fn test_begin_control_flow_match_empty_open() {
843        let mut b = CodeBlock::builder();
844        b.begin_control_flow("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_open = block
850            .nodes
851            .iter()
852            .any(|n| matches!(n, CodeNode::BlockOpen(s) if s == "match x with"));
853        assert!(has_open, "should contain BlockOpen(\"match x with\")");
854    }
855}