Skip to main content

imp_core/tools/scan/
python.rs

1//! Python tree-sitter extraction — classes, functions, decorators.
2//! Ported from uu-manifest's Python 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_python::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    let is_test = is_test_file(file);
21    extract_python(&tree.root_node(), source, file, is_test, result);
22}
23
24fn is_test_file(path: &str) -> bool {
25    let filename = path.rsplit('/').next().unwrap_or(path);
26    filename.starts_with("test_") || filename.ends_with("_test.py")
27}
28
29fn extract_python(root: &Node, source: &str, file: &str, is_test: bool, result: &mut ScanResult) {
30    let mut cursor = root.walk();
31    for child in root.named_children(&mut cursor) {
32        match child.kind() {
33            "class_definition" => extract_class(&child, source, file, result),
34            "function_definition" => {
35                extract_function(&child, source, file, is_test, &[], result);
36            }
37            "decorated_definition" => {
38                extract_decorated(&child, source, file, is_test, result);
39            }
40            _ => {}
41        }
42    }
43}
44
45fn extract_class(node: &Node, source: &str, file: &str, result: &mut ScanResult) {
46    let name = match node.child_by_field_name("name") {
47        Some(n) => node_text(&n, source),
48        None => return,
49    };
50
51    let mut implements = Vec::new();
52    if let Some(superclasses) = node.child_by_field_name("superclasses") {
53        let mut cursor = superclasses.walk();
54        for child in superclasses.named_children(&mut cursor) {
55            let text = node_text(&child, source);
56            if !text.contains('=') && !text.is_empty() {
57                implements.push(text);
58            }
59        }
60    }
61
62    let visibility = if name.starts_with('_') {
63        Visibility::Private
64    } else {
65        Visibility::Public
66    };
67
68    let mut methods = Vec::new();
69    if let Some(body) = node.child_by_field_name("body") {
70        extract_class_methods(&body, source, file, &name, result, &mut methods);
71    }
72
73    result.types.insert(
74        name.clone(),
75        TypeInfo {
76            name,
77            source: file.to_string(),
78            kind: TypeKind::Class,
79            visibility,
80            implements,
81            methods,
82            ..Default::default()
83        },
84    );
85}
86
87fn extract_class_methods(
88    body: &Node,
89    source: &str,
90    file: &str,
91    class_name: &str,
92    result: &mut ScanResult,
93    methods: &mut Vec<String>,
94) {
95    let mut cursor = body.walk();
96    for child in body.named_children(&mut cursor) {
97        match child.kind() {
98            "function_definition" => {
99                if let Some(name) = extract_method(&child, source, file, class_name, &[], result) {
100                    methods.push(name);
101                }
102            }
103            "decorated_definition" => {
104                let decorators = collect_decorators(&child, source);
105                if let Some(func_node) = child.child_by_field_name("definition") {
106                    if func_node.kind() == "function_definition" {
107                        if let Some(name) = extract_method(
108                            &func_node,
109                            source,
110                            file,
111                            class_name,
112                            &decorators,
113                            result,
114                        ) {
115                            methods.push(name);
116                        }
117                    }
118                }
119            }
120            _ => {}
121        }
122    }
123}
124
125fn extract_method(
126    node: &Node,
127    source: &str,
128    file: &str,
129    class_name: &str,
130    decorators: &[String],
131    result: &mut ScanResult,
132) -> Option<String> {
133    let name = node_text(&node.child_by_field_name("name")?, source);
134
135    let visibility = if name.starts_with('_') && !name.starts_with("__") {
136        Visibility::Private
137    } else {
138        Visibility::Public
139    };
140
141    let is_async = source[node.byte_range()].starts_with("async ");
142    let params = node
143        .child_by_field_name("parameters")
144        .map(|p| node_text(&p, source))
145        .unwrap_or_default();
146
147    let decorator_prefix = decorators
148        .iter()
149        .map(|d| format!("@{d} "))
150        .collect::<String>();
151    let async_prefix = if is_async { "async " } else { "" };
152    let signature = format!("{decorator_prefix}{async_prefix}def {name}{params}");
153
154    let qualified = format!("{class_name}::{name}");
155    result.functions.insert(
156        qualified,
157        FunctionInfo {
158            name: name.clone(),
159            source: file.to_string(),
160            signature,
161            visibility,
162            is_async,
163            ..Default::default()
164        },
165    );
166    Some(name)
167}
168
169fn extract_function(
170    node: &Node,
171    source: &str,
172    file: &str,
173    is_test: bool,
174    decorators: &[String],
175    result: &mut ScanResult,
176) {
177    let name = match node.child_by_field_name("name") {
178        Some(n) => node_text(&n, source),
179        None => return,
180    };
181
182    let visibility = if name.starts_with('_') {
183        Visibility::Private
184    } else {
185        Visibility::Public
186    };
187
188    let is_async = source[node.byte_range()].starts_with("async ");
189    let func_is_test = is_test || name.starts_with("test_");
190
191    let params = node
192        .child_by_field_name("parameters")
193        .map(|p| node_text(&p, source))
194        .unwrap_or_default();
195
196    let decorator_prefix = decorators
197        .iter()
198        .map(|d| format!("@{d} "))
199        .collect::<String>();
200    let async_prefix = if is_async { "async " } else { "" };
201    let signature = format!("{decorator_prefix}{async_prefix}def {name}{params}");
202
203    result.functions.insert(
204        name.clone(),
205        FunctionInfo {
206            name,
207            source: file.to_string(),
208            signature,
209            visibility,
210            is_async,
211            is_test: func_is_test,
212        },
213    );
214}
215
216fn extract_decorated(
217    node: &Node,
218    source: &str,
219    file: &str,
220    is_test: bool,
221    result: &mut ScanResult,
222) {
223    let decorators = collect_decorators(node, source);
224    if let Some(definition) = node.child_by_field_name("definition") {
225        match definition.kind() {
226            "function_definition" => {
227                extract_function(&definition, source, file, is_test, &decorators, result);
228            }
229            "class_definition" => {
230                extract_class(&definition, source, file, result);
231            }
232            _ => {}
233        }
234    }
235}
236
237fn collect_decorators(node: &Node, source: &str) -> Vec<String> {
238    let mut decorators = Vec::new();
239    let mut cursor = node.walk();
240    for child in node.named_children(&mut cursor) {
241        if child.kind() == "decorator" {
242            let text = node_text(&child, source);
243            let name = text.strip_prefix('@').unwrap_or(&text).trim().to_string();
244            decorators.push(name);
245        }
246    }
247    decorators
248}
249
250fn node_text(node: &Node, source: &str) -> String {
251    source[node.byte_range()].to_string()
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    fn parse_py(source: &str) -> ScanResult {
259        let mut result = ScanResult::default();
260        parse(source, "models.py", &mut result);
261        result
262    }
263
264    #[test]
265    fn class_with_inheritance() {
266        let r = parse_py(
267            r#"
268class Admin(User, Serializable):
269    pass
270"#,
271        );
272        let t = &r.types["Admin"];
273        assert_eq!(t.kind, TypeKind::Class);
274        assert!(t.implements.contains(&"User".to_string()));
275        assert!(t.implements.contains(&"Serializable".to_string()));
276    }
277
278    #[test]
279    fn function_visibility() {
280        let r = parse_py("def create_user(name): pass\ndef _internal(): pass");
281        assert_eq!(r.functions["create_user"].visibility, Visibility::Public);
282        assert_eq!(r.functions["_internal"].visibility, Visibility::Private);
283    }
284
285    #[test]
286    fn async_function() {
287        let r = parse_py("async def fetch(url): pass");
288        assert!(r.functions["fetch"].is_async);
289    }
290
291    #[test]
292    fn class_methods() {
293        let r = parse_py(
294            r#"
295class Service:
296    def process(self): pass
297    def _internal(self): pass
298"#,
299        );
300        let t = &r.types["Service"];
301        assert!(t.methods.contains(&"process".to_string()));
302        assert!(t.methods.contains(&"_internal".to_string()));
303    }
304
305    #[test]
306    fn decorated_function() {
307        let r = parse_py(
308            r#"
309class Config:
310    @property
311    def name(self):
312        return self._name
313"#,
314        );
315        let f = &r.functions["Config::name"];
316        assert!(f.signature.contains("@property"));
317    }
318}