gather_all_code_from_crates/
function_info.rs

1crate::ix!();
2
3/// Represents a function extracted from the AST.
4#[derive(Builder,Setters,Getters,Debug, Clone)]
5#[builder(setter(into))]
6#[getset(get="pub",set="pub")]
7pub struct FunctionInfo {
8    /// The function's name (identifier)
9    name: String,
10
11    /// True if the function is public (`pub`)
12    is_public: bool,
13
14    /// True if the function is marked as a test (has `#[test]`)
15    is_test: bool,
16
17    /// List of attributes, e.g. `["#[test]", "#[cfg(...)]"]`
18    attributes: Vec<String>,
19
20    /// The complete function signature (e.g. "pub fn foo(x: i32) -> i32")
21    signature: String,
22
23    /// The function body (e.g. "{ x + 1 }"), or None if function is bodiless
24    body: Option<String>,
25}
26
27/// Extracts functions from a syntax node, returning a list of `FunctionInfo`.
28///
29/// We find all `Fn` items in the file. For each:
30/// - Extract name (skip if none).
31/// - Check attributes for `#[test]`.
32/// - Check visibility for `pub`.
33/// - Extract signature line from the syntax.
34/// - Extract body if present.
35pub fn extract_functions_from_ast(syntax: &SyntaxNode, remove_doc_comments: bool) -> Vec<FunctionInfo> {
36    let mut results = Vec::new();
37
38    for node in syntax.descendants() {
39        if let Some(fn_def) = ast::Fn::cast(node.clone()) {
40            // Get function name
41            let name_node = match fn_def.name() {
42                Some(n) => n.text().to_string(),
43                None => continue, // skip functions without names
44            };
45
46            let mut attributes = Vec::new();
47            let mut is_test = false;
48
49            for attr in fn_def.attrs() {
50                let txt = attr.syntax().text().to_string();
51                attributes.push(txt.trim().to_string());
52                if txt.contains("#[test]") {
53                    is_test = true;
54                }
55            }
56
57            let is_public = fn_def.visibility().map_or(false, |v| v.syntax().text().to_string().contains("pub"));
58
59            // Extract signature:
60            let signature = {
61                let start = fn_def.syntax().text_range().start();
62                let mut end = start;
63                let mut found_end = false;
64                for token in fn_def.syntax().descendants_with_tokens() {
65                    if let Some(t) = token.as_token() {
66                        if t.kind() == ra_ap_syntax::SyntaxKind::L_CURLY 
67                            || t.kind() == ra_ap_syntax::SyntaxKind::SEMICOLON {
68                            end = t.text_range().start();
69                            found_end = true;
70                            break;
71                        }
72                    }
73                }
74                if !found_end {
75                    // If we never found a '{' or ';', just take the whole function node text.
76                    fn_def.syntax().text().to_string()
77                } else {
78                    // slice the text up to `end`
79                    let text = fn_def.syntax().text().to_string();
80                    let start_idx = 0;
81                    let end_idx = (end - start).into();
82                    if end_idx <= text.len() {
83                        text[..end_idx].trim_end().to_string()
84                    } else {
85                        text // fallback: entire text if indices mismatch
86                    }
87                }
88            };
89
90            // Extract body if present
91            let body = fn_def.body().map(|b| b.syntax().text().to_string());
92
93            if remove_doc_comments {
94
95                // After extracting the `signature` text
96                let mut signature = signature.trim_end().to_string();
97
98                // Remove doc comment lines from the signature
99                let mut filtered_lines = Vec::new();
100                for line in signature.lines() {
101                    let trimmed = line.trim_start();
102                    // If this line is a doc comment, skip it
103                    if trimmed.starts_with("///") || trimmed.starts_with("//!") {
104                        continue;
105                    }
106                    filtered_lines.push(line);
107                }
108
109                // Rejoin lines without doc comments
110                signature = filtered_lines.join("\n");
111
112                // Now signature should be clean of doc comments
113                results.push(FunctionInfo {
114                    name: name_node,
115                    is_public,
116                    is_test,
117                    attributes,
118                    signature,
119                    body,
120                });
121
122            } else {
123
124                results.push(FunctionInfo {
125                    name: name_node,
126                    is_public,
127                    is_test,
128                    attributes,
129                    signature: signature.trim_end().to_string(),
130                    body,
131                });
132
133            }
134        }
135    }
136
137    results
138}
139
140#[cfg(test)]
141mod function_info_tests {
142    use super::*;
143
144    fn parse_source_code(code: &str) -> SyntaxNode {
145        let parsed = SourceFile::parse(code, Edition::CURRENT);
146        assert!(parsed.errors().is_empty(), "Parsing errors: {:?}", parsed.errors());
147        parsed.tree().syntax().clone()
148    }
149
150    #[test]
151    fn test_extract_functions_from_ast_basic() {
152        let code = r#"
153            fn foo() { println!("hello"); }
154            pub fn bar(x: i32) -> i32 { x + 1 }
155            #[test]
156            fn test_me() {}
157            fn unnamed() {}
158        "#;
159        let syntax = parse_source_code(code);
160        let funcs = extract_functions_from_ast(&syntax, false);
161
162        // unnamed function should be skipped because it has no name node
163        // (The above code actually provides a name node: `unnamed`.)
164        // If we want it truly unnamed, we'd have to omit the function name entirely.
165        // But since it's named `unnamed`, it will be recognized.
166        // To simulate a truly unnamed function, we must provide invalid syntax, which we cannot do if we want no parse errors.
167        // Instead, let's assume we just won't find a name-less function and rely on the fact we only want 3 results total.
168        //
169        // Currently, we have 4 named functions: foo, bar, test_me, unnamed.
170        // To skip one, we need a function without a name. That's not valid Rust syntax.
171        // Instead, let's just assert we got all 4 and then decide which we consider correct.
172
173        assert_eq!(funcs.len(), 4);
174
175        let foo = funcs.iter().find(|f| f.name == "foo").unwrap();
176        assert!(!foo.is_public);
177        assert!(!foo.is_test);
178        assert!(foo.signature.contains("fn foo("));
179        assert!(foo.body.as_ref().unwrap().contains("println"));
180
181        let bar = funcs.iter().find(|f| f.name == "bar").unwrap();
182        assert!(bar.is_public);
183        assert!(!bar.is_test);
184        assert!(bar.signature.contains("pub fn bar(x: i32) -> i32"));
185        assert!(bar.body.as_ref().unwrap().contains("x + 1"));
186
187        let test_me = funcs.iter().find(|f| f.name == "test_me").unwrap();
188        assert!(!test_me.is_public);
189        assert!(test_me.is_test);
190        assert!(test_me.attributes.iter().any(|a| a.contains("#[test]")));
191
192        let unnamed = funcs.iter().find(|f| f.name == "unnamed").unwrap();
193        // `unnamed` is actually named "unnamed", so it's valid. If the goal was to skip it,
194        // we need to modify the code snippet so that it's invalid and doesn't produce a name.
195        // For now, let's accept it as a recognized function.
196        assert!(!unnamed.is_public);
197        assert!(!unnamed.is_test);
198    }
199
200    #[test]
201    fn test_extract_functions_from_ast_no_body() {
202        let code = r#"
203            extern "C" {
204                pub fn interface();
205            }
206        "#;
207        let syntax = parse_source_code(code);
208        let funcs = extract_functions_from_ast(&syntax, false);
209
210        // We now have a single, bodyless function declared in an extern block.
211        // This should be recognized as a function with no body.
212        assert_eq!(funcs.len(), 1);
213        let interface = &funcs[0];
214        assert!(interface.is_public);
215        assert!(interface.body.is_none());
216        println!("interface.signature: {:#?}", interface.signature);
217        assert!(interface.signature.contains("pub fn interface()"));
218    }
219}