gitcortex_indexer/parser/
python.rs1use std::{
2 collections::HashMap,
3 path::{Path, PathBuf},
4};
5
6use gitcortex_core::{
7 error::{GitCortexError, Result},
8 graph::{Edge, Node, NodeId, NodeMetadata, Span},
9 schema::{EdgeKind, NodeKind, Visibility},
10};
11use tree_sitter::{Node as TsNode, Parser};
12
13use super::{LanguageParser, ParseResult};
14
15pub struct PythonParser {
16 language: tree_sitter::Language,
17}
18
19impl PythonParser {
20 pub fn new() -> Self {
21 Self {
22 language: tree_sitter_python::LANGUAGE.into(),
23 }
24 }
25}
26
27impl Default for PythonParser {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33impl LanguageParser for PythonParser {
34 fn extensions(&self) -> &[&str] {
35 &["py"]
36 }
37
38 fn parse(&self, path: &Path, source: &str) -> Result<ParseResult> {
39 let mut parser = Parser::new();
40 parser
41 .set_language(&self.language)
42 .map_err(|e| GitCortexError::Parse {
43 file: path.to_owned(),
44 message: e.to_string(),
45 })?;
46
47 let tree = parser
48 .parse(source, None)
49 .ok_or_else(|| GitCortexError::Parse {
50 file: path.to_owned(),
51 message: "tree-sitter returned no parse tree".into(),
52 })?;
53
54 let mut visitor = FileVisitor::new(path, source);
55 visitor.collect_names(tree.root_node());
56 visitor.visit_module(tree.root_node());
57
58 Ok(ParseResult {
59 nodes: visitor.nodes,
60 edges: visitor.edges,
61 deferred_calls: visitor.deferred_calls,
62 deferred_uses: Vec::new(),
63 deferred_implements: Vec::new(),
64 deferred_imports: Vec::new(),
65 })
66 }
67}
68
69struct FileVisitor<'src> {
72 source: &'src [u8],
73 file: PathBuf,
74 nodes: Vec<Node>,
75 edges: Vec<Edge>,
76 class_index: HashMap<String, NodeId>,
78 fn_index: HashMap<String, NodeId>,
80 deferred_calls: Vec<(NodeId, String)>,
81}
82
83impl<'src> FileVisitor<'src> {
84 fn new(file: &Path, source: &'src str) -> Self {
85 Self {
86 source: source.as_bytes(),
87 file: file.to_owned(),
88 nodes: Vec::new(),
89 edges: Vec::new(),
90 class_index: HashMap::new(),
91 fn_index: HashMap::new(),
92 deferred_calls: Vec::new(),
93 }
94 }
95
96 fn text<'t>(&self, node: TsNode<'t>) -> &'src str {
97 node.utf8_text(self.source).unwrap_or("")
98 }
99
100 fn span(node: TsNode<'_>) -> Span {
101 Span {
102 start_line: node.start_position().row as u32 + 1,
103 end_line: node.end_position().row as u32 + 1,
104 }
105 }
106
107 fn visibility(name: &str) -> Visibility {
109 if name.starts_with('_') {
110 Visibility::Private
111 } else {
112 Visibility::Pub
113 }
114 }
115
116 fn qualified(scope: &[String], name: &str) -> String {
117 if scope.is_empty() {
118 name.to_owned()
119 } else {
120 format!("{}.{name}", scope.join("."))
121 }
122 }
123
124 fn make_node(
125 &self,
126 id: NodeId,
127 kind: NodeKind,
128 name: String,
129 scope: &[String],
130 ts_node: TsNode<'_>,
131 is_async: bool,
132 ) -> Node {
133 let vis = Self::visibility(&name);
134 Node {
135 id,
136 qualified_name: Self::qualified(scope, &name),
137 kind,
138 name,
139 file: self.file.clone(),
140 span: Self::span(ts_node),
141 metadata: NodeMetadata {
142 loc: (ts_node.end_position().row - ts_node.start_position().row + 1) as u32,
143 visibility: vis,
144 is_async,
145 is_unsafe: false,
146 ..Default::default()
147 },
148 }
149 }
150
151 fn collect_names(&mut self, node: TsNode<'_>) {
154 let mut cursor = node.walk();
155 let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
156 for child in children {
157 match child.kind() {
158 "class_definition" => {
159 if let Some(name_node) = child.child_by_field_name("name") {
160 let name = self.text(name_node).to_owned();
161 self.class_index.entry(name).or_default();
162 }
163 }
164 "function_definition" | "decorated_definition" => {
165 let fn_node = if child.kind() == "decorated_definition" {
166 child.child_by_field_name("definition")
167 } else {
168 Some(child)
169 };
170 if let Some(fn_node) = fn_node {
171 if let Some(name_node) = fn_node.child_by_field_name("name") {
172 let name = self.text(name_node).to_owned();
173 self.fn_index.entry(name).or_default();
174 }
175 }
176 }
177 _ => {}
178 }
179 }
180 }
181
182 fn visit_module(&mut self, node: TsNode<'_>) {
185 let mut cursor = node.walk();
186 let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
187 for child in children {
188 self.visit_top_level(child, &[]);
189 }
190 }
191
192 fn visit_top_level(&mut self, node: TsNode<'_>, scope: &[String]) {
193 match node.kind() {
194 "function_definition" => {
195 self.visit_function(node, scope, None, false);
196 }
197 "decorated_definition" => {
198 let is_async = {
199 let mut c = node.walk();
200 let result = node.named_children(&mut c).any(|n| n.kind() == "async");
201 result
202 };
203 if let Some(def) = node.child_by_field_name("definition") {
204 match def.kind() {
205 "function_definition" => self.visit_function(def, scope, None, is_async),
206 "class_definition" => self.visit_class(def, scope),
207 _ => {}
208 }
209 }
210 }
211 "class_definition" => self.visit_class(node, scope),
212 "expression_statement" => self.maybe_visit_constant(node, scope),
213 _ => {}
214 }
215 }
216
217 fn visit_function(
218 &mut self,
219 node: TsNode<'_>,
220 scope: &[String],
221 container_id: Option<NodeId>,
222 is_async: bool,
223 ) {
224 let Some(name_node) = node.child_by_field_name("name") else {
225 return;
226 };
227 let name = self.text(name_node).to_owned();
228 let id = self
229 .fn_index
230 .get(&name)
231 .cloned()
232 .unwrap_or_else(NodeId::new);
233 let kind = if container_id.is_some() {
234 NodeKind::Method
235 } else {
236 NodeKind::Function
237 };
238 let graph_node = self.make_node(id.clone(), kind, name, scope, node, is_async);
239
240 if let Some(cid) = container_id {
241 self.edges.push(Edge {
242 src: cid,
243 dst: id.clone(),
244 kind: EdgeKind::Contains,
245 });
246 }
247 self.nodes.push(graph_node);
248
249 if let Some(body) = node.child_by_field_name("body") {
250 self.collect_calls(body, &id);
251 }
252 }
253
254 fn visit_class(&mut self, node: TsNode<'_>, scope: &[String]) {
255 let Some(name_node) = node.child_by_field_name("name") else {
256 return;
257 };
258 let name = self.text(name_node).to_owned();
259 let id = self
260 .class_index
261 .get(&name)
262 .cloned()
263 .unwrap_or_else(NodeId::new);
264 let graph_node = self.make_node(
265 id.clone(),
266 NodeKind::Struct,
267 name.clone(),
268 scope,
269 node,
270 false,
271 );
272 self.nodes.push(graph_node);
273
274 let mut class_scope = scope.to_vec();
275 class_scope.push(name.clone());
276
277 if let Some(body) = node.child_by_field_name("body") {
278 let mut cursor = body.walk();
279 let children: Vec<TsNode<'_>> = body.named_children(&mut cursor).collect();
280 for child in children {
281 match child.kind() {
282 "function_definition" => {
283 self.visit_function(child, &class_scope, Some(id.clone()), false);
284 }
285 "decorated_definition" => {
286 if let Some(def) = child.child_by_field_name("definition") {
287 if def.kind() == "function_definition" {
288 self.visit_function(def, &class_scope, Some(id.clone()), false);
289 }
290 }
291 }
292 _ => {}
293 }
294 }
295 }
296 }
297
298 fn maybe_visit_constant(&mut self, node: TsNode<'_>, scope: &[String]) {
299 let mut cursor = node.walk();
301 for child in node.named_children(&mut cursor) {
302 if child.kind() == "assignment" {
303 if let Some(left) = child.child_by_field_name("left") {
304 if left.kind() == "identifier" {
305 let name = self.text(left).to_owned();
306 if name
307 .chars()
308 .all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit())
309 && name.len() > 1
310 && !name.starts_with('_')
311 {
312 let id = NodeId::new();
313 let graph_node =
314 self.make_node(id, NodeKind::Constant, name, scope, node, false);
315 self.nodes.push(graph_node);
316 }
317 }
318 }
319 }
320 }
321 }
322
323 fn collect_calls(&mut self, node: TsNode<'_>, caller_id: &NodeId) {
324 let mut cursor = node.walk();
325 let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
326 for child in children {
327 if child.kind() == "call" {
328 if let Some(callee) = self.callee_name(child) {
329 self.record_call(caller_id.clone(), callee);
330 }
331 if let Some(args) = child.child_by_field_name("arguments") {
333 self.collect_calls(args, caller_id);
334 }
335 } else {
336 self.collect_calls(child, caller_id);
337 }
338 }
339 }
340
341 fn callee_name(&self, call_node: TsNode<'_>) -> Option<String> {
342 let func = call_node.child_by_field_name("function")?;
343 match func.kind() {
344 "identifier" => Some(self.text(func).to_owned()),
345 "attribute" => func
346 .child_by_field_name("attribute")
347 .map(|n| self.text(n).to_owned()),
348 _ => None,
349 }
350 }
351
352 fn record_call(&mut self, caller_id: NodeId, callee_name: String) {
353 if callee_name.is_empty() {
354 return;
355 }
356 if let Some(callee_id) = self.fn_index.get(&callee_name).cloned() {
357 let edge = Edge {
358 src: caller_id,
359 dst: callee_id,
360 kind: EdgeKind::Calls,
361 };
362 if !self.edges.contains(&edge) {
363 self.edges.push(edge);
364 }
365 } else if !self
366 .deferred_calls
367 .iter()
368 .any(|(c, n)| c == &caller_id && n == &callee_name)
369 {
370 self.deferred_calls.push((caller_id, callee_name));
371 }
372 }
373}
374
375#[cfg(test)]
378mod tests {
379 use super::PythonParser;
380 use crate::parser::LanguageParser;
381 use gitcortex_core::schema::{EdgeKind, NodeKind};
382 use std::path::Path;
383
384 fn parse(
385 src: &str,
386 ) -> (
387 Vec<gitcortex_core::graph::Node>,
388 Vec<gitcortex_core::graph::Edge>,
389 ) {
390 let r = PythonParser::new()
391 .parse(Path::new("test.py"), src)
392 .unwrap();
393 (r.nodes, r.edges)
394 }
395
396 #[test]
397 fn parses_free_function() {
398 let (nodes, _) = parse("def greet(name):\n return name\n");
399 assert_eq!(nodes.len(), 1);
400 assert_eq!(nodes[0].kind, NodeKind::Function);
401 assert_eq!(nodes[0].name, "greet");
402 }
403
404 #[test]
405 fn parses_class_and_method() {
406 let src = "class Person:\n def greet(self):\n pass\n";
407 let (nodes, edges) = parse(src);
408 let classes: Vec<_> = nodes
409 .iter()
410 .filter(|n| n.kind == NodeKind::Struct)
411 .collect();
412 let methods: Vec<_> = nodes
413 .iter()
414 .filter(|n| n.kind == NodeKind::Method)
415 .collect();
416 assert_eq!(classes.len(), 1);
417 assert_eq!(methods.len(), 1);
418 let contains: Vec<_> = edges
419 .iter()
420 .filter(|e| e.kind == EdgeKind::Contains)
421 .collect();
422 assert!(!contains.is_empty());
423 }
424
425 #[test]
426 fn detects_call_edges() {
427 let src = "def caller():\n callee()\ndef callee():\n pass\n";
428 let (_, edges) = parse(src);
429 let calls: Vec<_> = edges.iter().filter(|e| e.kind == EdgeKind::Calls).collect();
430 assert_eq!(calls.len(), 1);
431 }
432}