workspacer_syntax/
extract_docs_from_ast_node.rs

1// ---------------- [ File: workspacer-syntax/src/extract_docs_from_ast_node.rs ]
2crate::ix!();
3
4/// Extract doc lines from `///` or `/** ... */`. We do NOT gather `#[doc="..."]` attributes here.
5/// Those can be handled separately if you want to unify them.
6pub fn extract_docs(node: &SyntaxNode) -> Option<String> {
7    let mut doc_lines = Vec::new();
8
9    for child in node.children_with_tokens() {
10        if let Some(token) = child.into_token() {
11            if token.kind() == SyntaxKind::COMMENT {
12                let text = token.text();
13                // We only consider `///` and `/**...*/` doc comments
14                if text.starts_with("///") || text.starts_with("/**") {
15                    debug!(?text, "Found doc-comment token");
16                    // Keep lines as-is, so block docs remain `/** ... */`
17                    for line in text.lines() {
18                        // Trim leading indentation if you like:
19                        doc_lines.push(line.trim_start().to_string());
20                    }
21                }
22            }
23        }
24    }
25
26    if doc_lines.is_empty() {
27        None
28    } else {
29        Some(doc_lines.join("\n"))
30    }
31}
32
33#[cfg(test)]
34mod test_extract_docs_exhaustive {
35    use super::*;
36
37    /// Helper to parse code and return the first top-level item node.
38    fn parse_first_item_node(code: &str) -> SyntaxNode {
39        let file = SourceFile::parse(code, Edition::Edition2021);
40        let syntax = file.syntax_node();
41        syntax
42            .children()
43            .find(|child| {
44                // Skip whitespace or other trivial tokens
45                !matches!(child.kind(), SyntaxKind::WHITESPACE | SyntaxKind::COMMENT)
46            })
47            .unwrap_or_else(|| panic!("No top-level item node found in snippet:\n{}", code))
48    }
49
50    #[traced_test]
51    fn test_extract_docs_none_when_no_doc_comments() {
52        info!("Testing no doc comments exist.");
53        let code = r#"
54            fn example() {}
55        "#;
56        let node = parse_first_item_node(code);
57        let docs = extract_docs(&node);
58        assert!(docs.is_none(), "Expected no doc comments, got: {:?}", docs);
59    }
60
61    #[traced_test]
62    fn test_extract_docs_none_with_only_normal_comment() {
63        info!("Testing only normal comments appear, no doc comment style recognized.");
64        let code = r#"
65            // This is a regular comment, not a doc comment.
66            fn example() {}
67        "#;
68        let node = parse_first_item_node(code);
69        let docs = extract_docs(&node);
70        assert!(docs.is_none(), "Expected None because only normal // comment was present.");
71    }
72
73    #[traced_test]
74    fn test_extract_docs_single_line_doc_comment() {
75        info!("Testing single line triple-slash doc comment recognized.");
76        let code = r#"
77            /// This is a doc comment
78            fn example() {}
79        "#;
80        let node = parse_first_item_node(code);
81        let docs = extract_docs(&node);
82        assert!(docs.is_some(), "Expected Some(doc) for triple-slash comment.");
83        let doc_text = docs.unwrap();
84        assert_eq!(doc_text.trim(), "/// This is a doc comment");
85    }
86
87    #[traced_test]
88    fn test_extract_docs_multiple_line_doc_comments() {
89        info!("Testing multiple triple-slash doc comments recognized.");
90        let code = r#"
91            /// Line one
92            /// Line two
93            fn example() {}
94        "#;
95        let node = parse_first_item_node(code);
96        let docs = extract_docs(&node).expect("Expected multiple doc lines");
97        let lines: Vec<&str> = docs.lines().collect();
98        assert_eq!(lines.len(), 2, "Should have two doc comment lines");
99        assert_eq!(lines[0].trim(), "/// Line one");
100        assert_eq!(lines[1].trim(), "/// Line two");
101    }
102
103    #[traced_test]
104    fn test_extract_docs_block_doc_comment() {
105        info!("Testing block doc comment recognized as multiple lines if needed.");
106        let code = r#"
107            /** 
108             * This is a block doc comment
109             */
110            fn example() {}
111        "#;
112        let node = parse_first_item_node(code);
113        let docs = extract_docs(&node);
114        assert!(docs.is_some(), "Expected Some(doc) for block doc comment.");
115        let doc_text = docs.unwrap();
116        assert!(
117            doc_text.contains("/**"),
118            "Should contain block doc syntax in the collected lines:\n{doc_text}"
119        );
120    }
121
122    #[traced_test]
123    fn test_extract_docs_combination_block_and_line() {
124        info!("Testing combination of block doc and triple-slash doc.");
125        let code = r#"
126            /** Block doc */
127            /// Line doc
128            fn example() {}
129        "#;
130        let node = parse_first_item_node(code);
131        let docs = extract_docs(&node)
132            .expect("Expected doc lines since doc comments are present.");
133        let lines: Vec<&str> = docs.lines().collect();
134        assert_eq!(lines.len(), 2, "Should have two doc lines total.");
135        assert!(lines[0].starts_with("/** Block doc"));
136        assert!(lines[1].starts_with("/// Line doc"));
137    }
138
139    #[traced_test]
140    fn test_extract_docs_with_mixed_normal_comments_ignored() {
141        info!("Testing normal // comments are ignored, doc comments extracted.");
142        let code = r#"
143            // normal comment
144            /// doc comment
145            // another normal comment
146            fn example() {}
147        "#;
148        let node = parse_first_item_node(code);
149        let docs = extract_docs(&node)
150            .expect("Expected doc comment to be extracted despite normal comments.");
151        let doc_text = docs.trim();
152        assert_eq!(doc_text, "/// doc comment");
153    }
154
155    #[traced_test]
156    fn test_extract_docs_returns_all_doc_comments_in_joined_string() {
157        info!("Testing we join all doc lines in order.");
158        let code = r#"
159            /// first line
160            /** second line */
161            /// third line
162            fn example() {}
163        "#;
164        let node = parse_first_item_node(code);
165        let docs = extract_docs(&node).expect("Expected doc comments");
166        let parts: Vec<&str> = docs.lines().collect();
167        assert_eq!(parts.len(), 3, "Should collect three doc comment lines.");
168        assert!(parts[0].starts_with("/// first line"));
169        assert!(parts[1].starts_with("/** second line"));
170        assert!(parts[2].starts_with("/// third line"));
171    }
172
173    #[traced_test]
174    fn test_extract_docs_on_struct_with_no_docs() {
175        info!("Testing no doc lines on a doc-less struct.");
176        let code = r#"
177            struct MyStruct {
178                field: i32
179            }
180        "#;
181        let node = parse_first_item_node(code);
182        let docs = extract_docs(&node);
183        assert!(docs.is_none(), "Should return None for a struct with no doc comments.");
184    }
185
186    #[traced_test]
187    fn test_extract_docs_on_struct_with_doc_comment() {
188        info!("Testing triple-slash doc lines on a struct.");
189        let code = r#"
190            /// A structure for demonstration
191            struct MyStruct {
192                field: i32
193            }
194        "#;
195        let node = parse_first_item_node(code);
196        let docs = extract_docs(&node).expect("Expected doc string on struct");
197        assert!(docs.contains("A structure for demonstration"));
198    }
199}