reference_query/lang/python/
mod.rs1use tree_sitter::{Node, Parser};
6
7use crate::core::{Kind, Symbol};
8use crate::lang::LanguagePlugin;
9
10const LANGUAGE: &str = "python";
11
12pub struct Python;
13
14impl LanguagePlugin for Python {
15 fn language(&self) -> &'static str {
16 LANGUAGE
17 }
18
19 fn extensions(&self) -> &[&str] {
20 &["py"]
21 }
22
23 fn extract(&self, file: &str, source: &str) -> Vec<Symbol> {
24 let mut parser = Parser::new();
25 if parser
26 .set_language(&tree_sitter_python::LANGUAGE.into())
27 .is_err()
28 {
29 return Vec::new();
30 }
31 let Some(tree) = parser.parse(source, None) else {
32 return Vec::new();
33 };
34 let mut out = Vec::new();
35 let ctx = Ctx {
36 src: source.as_bytes(),
37 file,
38 };
39 ctx.walk(tree.root_node(), None, false, &mut out);
40 out
41 }
42}
43
44struct Ctx<'a> {
45 src: &'a [u8],
46 file: &'a str,
47}
48
49impl Ctx<'_> {
50 fn walk(&self, node: Node, parent: Option<&str>, in_class: bool, out: &mut Vec<Symbol>) {
53 let mut cursor = node.walk();
54 for child in node.children(&mut cursor) {
55 match child.kind() {
56 "class_definition" => {
57 if let Some(name) = self.field_text(child, "name") {
58 out.push(self.symbol(&name, Kind::Class, child, parent));
59 let qualified = qualify(parent, &name);
60 self.walk(child, Some(&qualified), true, out);
61 }
62 }
63 "function_definition" => {
64 if let Some(name) = self.field_text(child, "name") {
65 let kind = if in_class {
66 Kind::Method
67 } else {
68 Kind::Function
69 };
70 out.push(self.symbol(&name, kind, child, parent));
71 }
72 }
74 _ => self.walk(child, parent, in_class, out),
77 }
78 }
79 }
80
81 fn field_text(&self, node: Node, field: &str) -> Option<String> {
82 node.child_by_field_name(field)
83 .and_then(|n| n.utf8_text(self.src).ok())
84 .map(str::to_string)
85 }
86
87 fn symbol(&self, name: &str, kind: Kind, node: Node, parent: Option<&str>) -> Symbol {
88 Symbol {
89 name: name.to_string(),
90 kind,
91 language: LANGUAGE.to_string(),
92 file: self.file.to_string(),
93 line: node.start_position().row as u32 + 1,
94 parent: parent.map(str::to_string),
95 }
96 }
97}
98
99fn qualify(parent: Option<&str>, name: &str) -> String {
100 match parent {
101 Some(p) => format!("{p}.{name}"),
102 None => name.to_string(),
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 fn extract(source: &str) -> Vec<Symbol> {
111 Python.extract("test.py", source)
112 }
113
114 fn find<'a>(syms: &'a [Symbol], name: &str) -> &'a Symbol {
115 syms.iter()
116 .find(|s| s.name == name)
117 .unwrap_or_else(|| panic!("no symbol named {name} in {syms:?}"))
118 }
119
120 #[test]
121 fn extracts_classes_methods_and_functions() {
122 let src = r#"
123class Account:
124 def deposit(self, amount):
125 pass
126
127 @property
128 def balance(self):
129 return 0
130
131def build():
132 return Account()
133"#;
134 let syms = extract(src);
135
136 let account = find(&syms, "Account");
137 assert_eq!(account.kind, Kind::Class);
138 assert_eq!(account.parent, None);
139
140 let deposit = find(&syms, "deposit");
141 assert_eq!(deposit.kind, Kind::Method);
142 assert_eq!(deposit.parent.as_deref(), Some("Account"));
143
144 assert_eq!(find(&syms, "balance").kind, Kind::Method);
146
147 let build = find(&syms, "build");
149 assert_eq!(build.kind, Kind::Function);
150 assert_eq!(build.parent, None);
151
152 assert_eq!(account.language, "python");
153 }
154
155 #[test]
156 fn empty_and_unparseable_yield_no_symbols() {
157 assert!(extract("").is_empty());
158 assert!(extract("# just a comment\n").is_empty());
159 }
160}