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