Skip to main content

gitcortex_indexer/parser/
python.rs

1use 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
69// ── Internal visitor ──────────────────────────────────────────────────────────
70
71struct FileVisitor<'src> {
72    source: &'src [u8],
73    file: PathBuf,
74    nodes: Vec<Node>,
75    edges: Vec<Edge>,
76    /// class name → NodeId (pass 1)
77    class_index: HashMap<String, NodeId>,
78    /// function/method name → NodeId (pass 1)
79    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    /// In Python, public = not starting with `_`.
108    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    // ── Pass 1 ────────────────────────────────────────────────────────────────
152
153    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    // ── Pass 2 ────────────────────────────────────────────────────────────────
183
184    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        // Only capture module-level UPPER_CASE = ... assignments.
300        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                // Recurse into arguments.
332                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// ── Tests ─────────────────────────────────────────────────────────────────────
376
377#[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}