1use 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}