Skip to main content

imp_core/tools/scan/
rust.rs

1//! Rust tree-sitter extraction — structs, enums, traits, impls, functions.
2//! Ported from uu-manifest's Rust language adapter.
3
4use tree_sitter::{Node, Parser};
5
6use super::types::*;
7
8pub fn parse(source: &str, file: &str, result: &mut ScanResult) {
9    let mut parser = Parser::new();
10    if parser
11        .set_language(&tree_sitter_rust::LANGUAGE.into())
12        .is_err()
13    {
14        return;
15    }
16    let tree = match parser.parse(source, None) {
17        Some(t) => t,
18        None => return,
19    };
20    extract_rust(&tree.root_node(), source, file, result);
21}
22
23fn extract_rust(root: &Node, source: &str, file: &str, result: &mut ScanResult) {
24    let mut cursor = root.walk();
25    for child in root.named_children(&mut cursor) {
26        match child.kind() {
27            "struct_item" => extract_struct(&child, source, file, result),
28            "enum_item" => extract_enum(&child, source, file, result),
29            "trait_item" => extract_trait(&child, source, file, result),
30            "impl_item" => extract_impl(&child, source, file, result),
31            "function_item" => extract_function(&child, source, file, result),
32            _ => {}
33        }
34    }
35}
36
37fn extract_struct(node: &Node, source: &str, file: &str, result: &mut ScanResult) {
38    let name = match get_name(node, source) {
39        Some(n) => n,
40        None => return,
41    };
42    let vis = get_visibility(node, source);
43    let fields = extract_fields(node, source);
44
45    result.types.insert(
46        name.clone(),
47        TypeInfo {
48            name,
49            source: source_loc(file, node),
50            kind: TypeKind::Struct,
51            fields,
52            visibility: vis,
53            ..Default::default()
54        },
55    );
56}
57
58fn extract_fields(node: &Node, source: &str) -> Vec<Field> {
59    let mut fields = Vec::new();
60    if let Some(body) = node.child_by_field_name("body") {
61        let mut cursor = body.walk();
62        for child in body.named_children(&mut cursor) {
63            if child.kind() == "field_declaration" {
64                if let Some(name_node) = child.child_by_field_name("name") {
65                    let name = node_text(&name_node, source).to_string();
66                    let type_name = child
67                        .child_by_field_name("type")
68                        .map(|t| node_text(&t, source).to_string())
69                        .unwrap_or_default();
70                    let optional = type_name.starts_with("Option<");
71                    fields.push(Field {
72                        name,
73                        type_name,
74                        optional,
75                    });
76                }
77            }
78        }
79    }
80    fields
81}
82
83fn extract_enum(node: &Node, source: &str, file: &str, result: &mut ScanResult) {
84    let name = match get_name(node, source) {
85        Some(n) => n,
86        None => return,
87    };
88    let vis = get_visibility(node, source);
89
90    let mut variants = Vec::new();
91    if let Some(body) = node.child_by_field_name("body") {
92        let mut cursor = body.walk();
93        for child in body.named_children(&mut cursor) {
94            if child.kind() == "enum_variant" {
95                if let Some(name_node) = child.child_by_field_name("name") {
96                    variants.push(node_text(&name_node, source).to_string());
97                }
98            }
99        }
100    }
101
102    result.types.insert(
103        name.clone(),
104        TypeInfo {
105            name,
106            source: source_loc(file, node),
107            kind: TypeKind::Enum,
108            variants,
109            visibility: vis,
110            ..Default::default()
111        },
112    );
113}
114
115fn extract_trait(node: &Node, source: &str, file: &str, result: &mut ScanResult) {
116    let name = match get_name(node, source) {
117        Some(n) => n,
118        None => return,
119    };
120    let vis = get_visibility(node, source);
121
122    let mut methods = Vec::new();
123    if let Some(body) = node.child_by_field_name("body") {
124        let mut cursor = body.walk();
125        for child in body.named_children(&mut cursor) {
126            if child.kind() == "function_signature_item" || child.kind() == "function_item" {
127                if let Some(name_node) = child.child_by_field_name("name") {
128                    methods.push(node_text(&name_node, source).to_string());
129                }
130            }
131        }
132    }
133
134    result.types.insert(
135        name.clone(),
136        TypeInfo {
137            name,
138            source: source_loc(file, node),
139            kind: TypeKind::Trait,
140            methods,
141            visibility: vis,
142            ..Default::default()
143        },
144    );
145}
146
147fn extract_impl(node: &Node, source: &str, file: &str, result: &mut ScanResult) {
148    let type_node = match node.child_by_field_name("type") {
149        Some(n) => n,
150        None => return,
151    };
152    let type_name = node_text(&type_node, source).to_string();
153
154    let trait_name = node
155        .child_by_field_name("trait")
156        .map(|t| node_text(&t, source).to_string());
157
158    let mut methods = Vec::new();
159    if let Some(body) = node.child_by_field_name("body") {
160        let mut cursor = body.walk();
161        for child in body.named_children(&mut cursor) {
162            if child.kind() == "function_item" {
163                let vis = get_visibility(&child, source);
164                if let Some(name_node) = child.child_by_field_name("name") {
165                    let method_name = node_text(&name_node, source).to_string();
166                    methods.push(method_name.clone());
167
168                    if matches!(vis, Visibility::Public) {
169                        let sig = build_fn_signature(&child, source);
170                        let is_async = has_async(&child, source);
171                        let is_test = has_test_attr(&child, source);
172                        let qualified = format!("{}::{}", type_name, method_name);
173                        result.functions.insert(
174                            qualified,
175                            FunctionInfo {
176                                name: method_name,
177                                source: source_loc(file, &child),
178                                signature: sig,
179                                visibility: vis,
180                                is_async,
181                                is_test,
182                            },
183                        );
184                    }
185                }
186            }
187        }
188    }
189
190    if let Some(typedef) = result.types.get_mut(&type_name) {
191        for m in &methods {
192            if !typedef.methods.contains(m) {
193                typedef.methods.push(m.clone());
194            }
195        }
196        if let Some(trait_name) = &trait_name {
197            if !typedef.implements.contains(trait_name) {
198                typedef.implements.push(trait_name.clone());
199            }
200        }
201    }
202}
203
204fn extract_function(node: &Node, source: &str, file: &str, result: &mut ScanResult) {
205    let name = match node.child_by_field_name("name") {
206        Some(n) => node_text(&n, source).to_string(),
207        None => return,
208    };
209    let vis = get_visibility(node, source);
210    let sig = build_fn_signature(node, source);
211    let is_async = has_async(node, source);
212    let is_test = has_test_attr(node, source);
213
214    result.functions.insert(
215        name.clone(),
216        FunctionInfo {
217            name,
218            source: source_loc(file, node),
219            signature: sig,
220            visibility: vis,
221            is_async,
222            is_test,
223        },
224    );
225}
226
227// ── helpers ─────────────────────────────────────────────────────────
228
229fn get_visibility(node: &Node, source: &str) -> Visibility {
230    let mut cursor = node.walk();
231    for child in node.named_children(&mut cursor) {
232        if child.kind() == "visibility_modifier" {
233            let text = node_text(&child, source);
234            return if text.contains("pub(crate)") || text.contains("pub(super)") {
235                Visibility::Internal
236            } else {
237                Visibility::Public
238            };
239        }
240    }
241    Visibility::Private
242}
243
244fn node_text<'a>(node: &Node, source: &'a str) -> &'a str {
245    &source[node.byte_range()]
246}
247
248fn get_name(node: &Node, source: &str) -> Option<String> {
249    for field_name in &["name", "type"] {
250        if let Some(name_node) = node.child_by_field_name(field_name) {
251            return Some(node_text(&name_node, source).to_string());
252        }
253    }
254    None
255}
256
257fn source_loc(file: &str, node: &Node) -> String {
258    format!("{}:{}", file, node.start_position().row + 1)
259}
260
261fn build_fn_signature(node: &Node, source: &str) -> String {
262    let name = node
263        .child_by_field_name("name")
264        .map(|n| node_text(&n, source))
265        .unwrap_or("?");
266    let params = node
267        .child_by_field_name("parameters")
268        .map(|n| node_text(&n, source))
269        .unwrap_or("()");
270    let ret = node
271        .child_by_field_name("return_type")
272        .map(|n| format!(" -> {}", node_text(&n, source)))
273        .unwrap_or_default();
274    let async_prefix = if has_async(node, source) {
275        "async "
276    } else {
277        ""
278    };
279    format!("{async_prefix}fn {name}{params}{ret}")
280}
281
282fn has_async(node: &Node, source: &str) -> bool {
283    let text = node_text(node, source);
284    text.starts_with("async ") || text.starts_with("pub async ") || text.contains(" async fn ")
285}
286
287fn has_test_attr(node: &Node, source: &str) -> bool {
288    if let Some(parent) = node.parent() {
289        let idx = node.start_byte();
290        let mut cursor = parent.walk();
291        for child in parent.named_children(&mut cursor) {
292            if child.start_byte() >= idx {
293                break;
294            }
295            if child.kind() == "attribute_item" || child.kind() == "inner_attribute_item" {
296                let text = node_text(&child, source);
297                if text.contains("test") {
298                    return true;
299                }
300            }
301        }
302    }
303    false
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    fn parse_rust_str(source: &str) -> ScanResult {
311        let mut result = ScanResult::default();
312        parse(source, "src/lib.rs", &mut result);
313        result
314    }
315
316    #[test]
317    fn pub_struct_with_fields() {
318        let r = parse_rust_str(
319            r#"
320pub struct User {
321    pub name: String,
322    pub age: u32,
323    pub email: Option<String>,
324}
325"#,
326        );
327        let t = &r.types["User"];
328        assert_eq!(t.kind, TypeKind::Struct);
329        assert_eq!(t.visibility, Visibility::Public);
330        assert_eq!(t.fields.len(), 3);
331        assert_eq!(t.fields[0].name, "name");
332        assert!(!t.fields[0].optional);
333        assert!(t.fields[2].optional);
334    }
335
336    #[test]
337    fn private_struct() {
338        let r = parse_rust_str("struct Internal { x: i32 }");
339        assert_eq!(r.types["Internal"].visibility, Visibility::Private);
340    }
341
342    #[test]
343    fn enum_variants() {
344        let r = parse_rust_str("pub enum Color { Red, Green, Blue }");
345        let t = &r.types["Color"];
346        assert_eq!(t.kind, TypeKind::Enum);
347        assert_eq!(t.variants, vec!["Red", "Green", "Blue"]);
348    }
349
350    #[test]
351    fn trait_methods() {
352        let r = parse_rust_str(
353            r#"
354pub trait Drawable {
355    fn draw(&self);
356    fn resize(&mut self, w: u32, h: u32);
357}
358"#,
359        );
360        let t = &r.types["Drawable"];
361        assert_eq!(t.kind, TypeKind::Trait);
362        assert!(t.methods.contains(&"draw".to_string()));
363        assert!(t.methods.contains(&"resize".to_string()));
364    }
365
366    #[test]
367    fn function_with_signature() {
368        let r = parse_rust_str("pub fn process(input: &str) -> Result<String> { todo!() }");
369        let f = &r.functions["process"];
370        assert_eq!(f.visibility, Visibility::Public);
371        assert!(f.signature.contains("-> Result<String>"));
372    }
373
374    #[test]
375    fn async_function() {
376        let r = parse_rust_str("pub async fn fetch(url: &str) -> Vec<u8> { todo!() }");
377        let f = &r.functions["fetch"];
378        assert!(f.is_async);
379        assert!(f.signature.starts_with("async fn"));
380    }
381
382    #[test]
383    fn impl_adds_methods_and_traits() {
384        let r = parse_rust_str(
385            r#"
386pub struct Foo { val: i32 }
387impl Foo {
388    pub fn new(val: i32) -> Self { Self { val } }
389    fn internal(&self) {}
390}
391impl Display for Foo {
392    fn fmt(&self, f: &mut Formatter) -> Result { todo!() }
393}
394"#,
395        );
396        let t = &r.types["Foo"];
397        assert!(t.methods.contains(&"new".to_string()));
398        assert!(t.methods.contains(&"internal".to_string()));
399        assert!(t.implements.contains(&"Display".to_string()));
400        assert!(r.functions.contains_key("Foo::new"));
401    }
402
403    #[test]
404    fn source_location() {
405        let r = parse_rust_str("\npub struct Pos;");
406        assert_eq!(r.types["Pos"].source, "src/lib.rs:2");
407    }
408}