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        let escaped = escape_javadoc_line(line);
124        let trimmed = escaped.trim_end();
125        if trimmed.is_empty() {
126            out.push_str(indent);
127            out.push_str(" *\n");
128        } else {
129            out.push_str(indent);
130            out.push_str(" * ");
131            out.push_str(trimmed);
132            out.push('\n');
133        }
134    }
135    out.push_str(indent);
136    out.push_str(" */\n");
137}
138
139/// Emit KDoc-style documentation comments (/** ... */)
140/// Used for Kotlin classes, methods, and properties.
141pub fn emit_kdoc(out: &mut String, doc: &str, indent: &str) {
142    if doc.is_empty() {
143        return;
144    }
145    out.push_str(indent);
146    out.push_str("/**\n");
147    for line in doc.lines() {
148        let trimmed = line.trim_end();
149        if trimmed.is_empty() {
150            out.push_str(indent);
151            out.push_str(" *\n");
152        } else {
153            out.push_str(indent);
154            out.push_str(" * ");
155            out.push_str(trimmed);
156            out.push('\n');
157        }
158    }
159    out.push_str(indent);
160    out.push_str(" */\n");
161}
162
163/// Emit Dartdoc-style documentation comments (///)
164/// Used for Dart classes, methods, and properties.
165pub fn emit_dartdoc(out: &mut String, doc: &str, indent: &str) {
166    if doc.is_empty() {
167        return;
168    }
169    for line in doc.lines() {
170        out.push_str(indent);
171        out.push_str("/// ");
172        out.push_str(line);
173        out.push('\n');
174    }
175}
176
177/// Emit Gleam documentation comments (///)
178/// Used for Gleam functions and types.
179pub fn emit_gleam_doc(out: &mut String, doc: &str, indent: &str) {
180    if doc.is_empty() {
181        return;
182    }
183    for line in doc.lines() {
184        out.push_str(indent);
185        out.push_str("/// ");
186        out.push_str(line);
187        out.push('\n');
188    }
189}
190
191/// Emit Zig documentation comments (///)
192/// Used for Zig functions, types, and declarations.
193pub fn emit_zig_doc(out: &mut String, doc: &str, indent: &str) {
194    if doc.is_empty() {
195        return;
196    }
197    for line in doc.lines() {
198        out.push_str(indent);
199        out.push_str("/// ");
200        out.push_str(line);
201        out.push('\n');
202    }
203}
204
205/// Escape Javadoc line: handle XML special chars and backtick code blocks.
206///
207/// HTML entities (`<`, `>`, `&`) are also escaped *inside* `{@code …}` blocks.
208/// Without that, content like `` `<pre><code>` `` would emit raw `<pre>`
209/// inside the Javadoc tag — Eclipse-formatter Spotless then treats it as a
210/// real `<pre>` block element and shatters the line across multiple `* `
211/// rows, breaking `alef-verify`'s embedded hash. Escaped content is
212/// rendered identically by Javadoc readers (the `{@code}` tag shows literal
213/// characters) and is stable under any post-formatter pass.
214fn escape_javadoc_line(s: &str) -> String {
215    let mut result = String::with_capacity(s.len());
216    let mut chars = s.chars().peekable();
217    while let Some(ch) = chars.next() {
218        if ch == '`' {
219            let mut code = String::new();
220            for c in chars.by_ref() {
221                if c == '`' {
222                    break;
223                }
224                code.push(c);
225            }
226            result.push_str("{@code ");
227            result.push_str(&escape_javadoc_html_entities(&code));
228            result.push('}');
229        } else if ch == '<' {
230            result.push_str("&lt;");
231        } else if ch == '>' {
232            result.push_str("&gt;");
233        } else if ch == '&' {
234            result.push_str("&amp;");
235        } else {
236            result.push(ch);
237        }
238    }
239    result
240}
241
242/// Escape only the HTML special characters that would otherwise be parsed by
243/// downstream Javadoc/Eclipse formatters as block-level HTML (e.g. `<pre>`).
244fn escape_javadoc_html_entities(s: &str) -> String {
245    let mut out = String::with_capacity(s.len());
246    for ch in s.chars() {
247        match ch {
248            '<' => out.push_str("&lt;"),
249            '>' => out.push_str("&gt;"),
250            '&' => out.push_str("&amp;"),
251            other => out.push(other),
252        }
253    }
254    out
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_emit_phpdoc() {
263        let mut out = String::new();
264        emit_phpdoc(&mut out, "Simple documentation", "    ");
265        assert!(out.contains("/**"));
266        assert!(out.contains("Simple documentation"));
267        assert!(out.contains("*/"));
268    }
269
270    #[test]
271    fn test_phpdoc_escaping() {
272        let mut out = String::new();
273        emit_phpdoc(&mut out, "Handle */ sequences", "");
274        assert!(out.contains("Handle * / sequences"));
275    }
276
277    #[test]
278    fn test_emit_csharp_doc() {
279        let mut out = String::new();
280        emit_csharp_doc(&mut out, "C# documentation", "    ");
281        assert!(out.contains("<summary>"));
282        assert!(out.contains("C# documentation"));
283        assert!(out.contains("</summary>"));
284    }
285
286    #[test]
287    fn test_csharp_xml_escaping() {
288        let mut out = String::new();
289        emit_csharp_doc(&mut out, "foo < bar & baz > qux", "");
290        assert!(out.contains("foo &lt; bar &amp; baz &gt; qux"));
291    }
292
293    #[test]
294    fn test_emit_elixir_doc() {
295        let mut out = String::new();
296        emit_elixir_doc(&mut out, "Elixir documentation");
297        assert!(out.contains("@doc \"\"\""));
298        assert!(out.contains("Elixir documentation"));
299        assert!(out.contains("\"\"\""));
300    }
301
302    #[test]
303    fn test_elixir_heredoc_escaping() {
304        let mut out = String::new();
305        emit_elixir_doc(&mut out, "Handle \"\"\" sequences");
306        assert!(out.contains("Handle \"\" \" sequences"));
307    }
308
309    #[test]
310    fn test_emit_roxygen() {
311        let mut out = String::new();
312        emit_roxygen(&mut out, "R documentation");
313        assert!(out.contains("#' R documentation"));
314    }
315
316    #[test]
317    fn test_emit_swift_doc() {
318        let mut out = String::new();
319        emit_swift_doc(&mut out, "Swift documentation", "    ");
320        assert!(out.contains("/// Swift documentation"));
321    }
322
323    #[test]
324    fn test_emit_javadoc() {
325        let mut out = String::new();
326        emit_javadoc(&mut out, "Java documentation", "    ");
327        assert!(out.contains("/**"));
328        assert!(out.contains("Java documentation"));
329        assert!(out.contains("*/"));
330    }
331
332    #[test]
333    fn test_emit_kdoc() {
334        let mut out = String::new();
335        emit_kdoc(&mut out, "Kotlin documentation", "    ");
336        assert!(out.contains("/**"));
337        assert!(out.contains("Kotlin documentation"));
338        assert!(out.contains("*/"));
339    }
340
341    #[test]
342    fn test_emit_dartdoc() {
343        let mut out = String::new();
344        emit_dartdoc(&mut out, "Dart documentation", "    ");
345        assert!(out.contains("/// Dart documentation"));
346    }
347
348    #[test]
349    fn test_emit_gleam_doc() {
350        let mut out = String::new();
351        emit_gleam_doc(&mut out, "Gleam documentation", "    ");
352        assert!(out.contains("/// Gleam documentation"));
353    }
354
355    #[test]
356    fn test_emit_zig_doc() {
357        let mut out = String::new();
358        emit_zig_doc(&mut out, "Zig documentation", "    ");
359        assert!(out.contains("/// Zig documentation"));
360    }
361
362    #[test]
363    fn test_empty_doc_skipped() {
364        let mut out = String::new();
365        emit_phpdoc(&mut out, "", "");
366        emit_csharp_doc(&mut out, "", "");
367        emit_elixir_doc(&mut out, "");
368        emit_roxygen(&mut out, "");
369        emit_kdoc(&mut out, "", "");
370        emit_dartdoc(&mut out, "", "");
371        emit_gleam_doc(&mut out, "", "");
372        emit_zig_doc(&mut out, "", "");
373        assert!(out.is_empty());
374    }
375}