Skip to main content

alef_codegen/
doc_emission.rs

1//! Language-native documentation comment emission.
2//! Provides standardized functions for emitting doc comments in different languages.
3
4/// Emit PHPDoc-style comments (/** ... */)
5/// Used for PHP classes, methods, and properties.
6pub fn emit_phpdoc(out: &mut String, doc: &str, indent: &str) {
7    if doc.is_empty() {
8        return;
9    }
10    out.push_str(indent);
11    out.push_str("/**\n");
12    for line in doc.lines() {
13        out.push_str(indent);
14        out.push_str(" * ");
15        out.push_str(&escape_phpdoc_line(line));
16        out.push('\n');
17    }
18    out.push_str(indent);
19    out.push_str(" */\n");
20}
21
22/// Escape PHPDoc line: handle */ sequences that could close the comment early.
23fn escape_phpdoc_line(s: &str) -> String {
24    s.replace("*/", "* /")
25}
26
27/// Emit C# XML documentation comments (/// <summary> ... </summary>)
28/// Used for C# classes, structs, methods, and properties.
29pub fn emit_csharp_doc(out: &mut String, doc: &str, indent: &str) {
30    if doc.is_empty() {
31        return;
32    }
33    out.push_str(indent);
34    out.push_str("/// <summary>\n");
35    for line in doc.lines() {
36        out.push_str(indent);
37        out.push_str("/// ");
38        out.push_str(&escape_csharp_doc_line(line));
39        out.push('\n');
40    }
41    out.push_str(indent);
42    out.push_str("/// </summary>\n");
43}
44
45/// Escape C# XML doc line: handle XML special characters.
46fn escape_csharp_doc_line(s: &str) -> String {
47    s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
48}
49
50/// Emit Elixir documentation comments (@doc)
51/// Used for Elixir modules and functions.
52pub fn emit_elixir_doc(out: &mut String, doc: &str) {
53    if doc.is_empty() {
54        return;
55    }
56    out.push_str("@doc \"\"\"\n");
57    for line in doc.lines() {
58        out.push_str(&escape_elixir_doc_line(line));
59        out.push('\n');
60    }
61    out.push_str("\"\"\"\n");
62}
63
64/// Emit Rust `///` documentation comments.
65///
66/// Used by alef backends that emit Rust source (e.g., the Rustler NIF crate,
67/// the swift-bridge wrapper crate, the FRB Dart bridge crate). Distinct from
68/// `emit_swift_doc` only by intent — the syntax is identical (`/// ` per line).
69pub fn emit_rustdoc(out: &mut String, doc: &str, indent: &str) {
70    if doc.is_empty() {
71        return;
72    }
73    for line in doc.lines() {
74        out.push_str(indent);
75        out.push_str("/// ");
76        out.push_str(line);
77        out.push('\n');
78    }
79}
80
81/// Escape Elixir doc line: handle triple-quote sequences that could close the heredoc early.
82fn escape_elixir_doc_line(s: &str) -> String {
83    s.replace("\"\"\"", "\"\" \"")
84}
85
86/// Emit R roxygen2-style documentation comments (#')
87/// Used for R functions.
88pub fn emit_roxygen(out: &mut String, doc: &str) {
89    if doc.is_empty() {
90        return;
91    }
92    for line in doc.lines() {
93        out.push_str("#' ");
94        out.push_str(line);
95        out.push('\n');
96    }
97}
98
99/// Emit Swift-style documentation comments (///)
100/// Used for Swift structs, enums, and functions.
101pub fn emit_swift_doc(out: &mut String, doc: &str, indent: &str) {
102    if doc.is_empty() {
103        return;
104    }
105    for line in doc.lines() {
106        out.push_str(indent);
107        out.push_str("/// ");
108        out.push_str(line);
109        out.push('\n');
110    }
111}
112
113/// Emit Javadoc-style documentation comments (/** ... */)
114/// Used for Java classes, methods, and fields.
115/// Handles XML escaping and Javadoc tag formatting.
116pub fn emit_javadoc(out: &mut String, doc: &str, indent: &str) {
117    if doc.is_empty() {
118        return;
119    }
120    out.push_str(indent);
121    out.push_str("/**\n");
122    for line in doc.lines() {
123        if line.is_empty() {
124            out.push_str(indent);
125            out.push_str(" *\n");
126        } else {
127            let escaped = escape_javadoc_line(line);
128            out.push_str(indent);
129            out.push_str(" * ");
130            out.push_str(&escaped);
131            out.push('\n');
132        }
133    }
134    out.push_str(indent);
135    out.push_str(" */\n");
136}
137
138/// Emit KDoc-style documentation comments (/** ... */)
139/// Used for Kotlin classes, methods, and properties.
140pub fn emit_kdoc(out: &mut String, doc: &str, indent: &str) {
141    if doc.is_empty() {
142        return;
143    }
144    out.push_str(indent);
145    out.push_str("/**\n");
146    for line in doc.lines() {
147        if line.is_empty() {
148            out.push_str(indent);
149            out.push_str(" *\n");
150        } else {
151            out.push_str(indent);
152            out.push_str(" * ");
153            out.push_str(line);
154            out.push('\n');
155        }
156    }
157    out.push_str(indent);
158    out.push_str(" */\n");
159}
160
161/// Emit Dartdoc-style documentation comments (///)
162/// Used for Dart classes, methods, and properties.
163pub fn emit_dartdoc(out: &mut String, doc: &str, indent: &str) {
164    if doc.is_empty() {
165        return;
166    }
167    for line in doc.lines() {
168        out.push_str(indent);
169        out.push_str("/// ");
170        out.push_str(line);
171        out.push('\n');
172    }
173}
174
175/// Emit Gleam documentation comments (///)
176/// Used for Gleam functions and types.
177pub fn emit_gleam_doc(out: &mut String, doc: &str, indent: &str) {
178    if doc.is_empty() {
179        return;
180    }
181    for line in doc.lines() {
182        out.push_str(indent);
183        out.push_str("/// ");
184        out.push_str(line);
185        out.push('\n');
186    }
187}
188
189/// Emit Zig documentation comments (///)
190/// Used for Zig functions, types, and declarations.
191pub fn emit_zig_doc(out: &mut String, doc: &str, indent: &str) {
192    if doc.is_empty() {
193        return;
194    }
195    for line in doc.lines() {
196        out.push_str(indent);
197        out.push_str("/// ");
198        out.push_str(line);
199        out.push('\n');
200    }
201}
202
203/// Escape Javadoc line: handle XML special chars and backtick code blocks.
204fn escape_javadoc_line(s: &str) -> String {
205    let mut result = String::with_capacity(s.len());
206    let mut chars = s.chars().peekable();
207    while let Some(ch) = chars.next() {
208        if ch == '`' {
209            let mut code = String::new();
210            for c in chars.by_ref() {
211                if c == '`' {
212                    break;
213                }
214                code.push(c);
215            }
216            result.push_str("{@code ");
217            result.push_str(&code);
218            result.push('}');
219        } else if ch == '<' {
220            result.push_str("&lt;");
221        } else if ch == '>' {
222            result.push_str("&gt;");
223        } else if ch == '&' {
224            result.push_str("&amp;");
225        } else {
226            result.push(ch);
227        }
228    }
229    result
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_emit_phpdoc() {
238        let mut out = String::new();
239        emit_phpdoc(&mut out, "Simple documentation", "    ");
240        assert!(out.contains("/**"));
241        assert!(out.contains("Simple documentation"));
242        assert!(out.contains("*/"));
243    }
244
245    #[test]
246    fn test_phpdoc_escaping() {
247        let mut out = String::new();
248        emit_phpdoc(&mut out, "Handle */ sequences", "");
249        assert!(out.contains("Handle * / sequences"));
250    }
251
252    #[test]
253    fn test_emit_csharp_doc() {
254        let mut out = String::new();
255        emit_csharp_doc(&mut out, "C# documentation", "    ");
256        assert!(out.contains("<summary>"));
257        assert!(out.contains("C# documentation"));
258        assert!(out.contains("</summary>"));
259    }
260
261    #[test]
262    fn test_csharp_xml_escaping() {
263        let mut out = String::new();
264        emit_csharp_doc(&mut out, "foo < bar & baz > qux", "");
265        assert!(out.contains("foo &lt; bar &amp; baz &gt; qux"));
266    }
267
268    #[test]
269    fn test_emit_elixir_doc() {
270        let mut out = String::new();
271        emit_elixir_doc(&mut out, "Elixir documentation");
272        assert!(out.contains("@doc \"\"\""));
273        assert!(out.contains("Elixir documentation"));
274        assert!(out.contains("\"\"\""));
275    }
276
277    #[test]
278    fn test_elixir_heredoc_escaping() {
279        let mut out = String::new();
280        emit_elixir_doc(&mut out, "Handle \"\"\" sequences");
281        assert!(out.contains("Handle \"\" \" sequences"));
282    }
283
284    #[test]
285    fn test_emit_roxygen() {
286        let mut out = String::new();
287        emit_roxygen(&mut out, "R documentation");
288        assert!(out.contains("#' R documentation"));
289    }
290
291    #[test]
292    fn test_emit_swift_doc() {
293        let mut out = String::new();
294        emit_swift_doc(&mut out, "Swift documentation", "    ");
295        assert!(out.contains("/// Swift documentation"));
296    }
297
298    #[test]
299    fn test_emit_javadoc() {
300        let mut out = String::new();
301        emit_javadoc(&mut out, "Java documentation", "    ");
302        assert!(out.contains("/**"));
303        assert!(out.contains("Java documentation"));
304        assert!(out.contains("*/"));
305    }
306
307    #[test]
308    fn test_emit_kdoc() {
309        let mut out = String::new();
310        emit_kdoc(&mut out, "Kotlin documentation", "    ");
311        assert!(out.contains("/**"));
312        assert!(out.contains("Kotlin documentation"));
313        assert!(out.contains("*/"));
314    }
315
316    #[test]
317    fn test_emit_dartdoc() {
318        let mut out = String::new();
319        emit_dartdoc(&mut out, "Dart documentation", "    ");
320        assert!(out.contains("/// Dart documentation"));
321    }
322
323    #[test]
324    fn test_emit_gleam_doc() {
325        let mut out = String::new();
326        emit_gleam_doc(&mut out, "Gleam documentation", "    ");
327        assert!(out.contains("/// Gleam documentation"));
328    }
329
330    #[test]
331    fn test_emit_zig_doc() {
332        let mut out = String::new();
333        emit_zig_doc(&mut out, "Zig documentation", "    ");
334        assert!(out.contains("/// Zig documentation"));
335    }
336
337    #[test]
338    fn test_empty_doc_skipped() {
339        let mut out = String::new();
340        emit_phpdoc(&mut out, "", "");
341        emit_csharp_doc(&mut out, "", "");
342        emit_elixir_doc(&mut out, "");
343        emit_roxygen(&mut out, "");
344        emit_kdoc(&mut out, "", "");
345        emit_dartdoc(&mut out, "", "");
346        emit_gleam_doc(&mut out, "", "");
347        emit_zig_doc(&mut out, "", "");
348        assert!(out.is_empty());
349    }
350}