reference_query/lang/python/
mod.rs1use tree_sitter::Node;
6
7use crate::core::{Kind, Symbol};
8use crate::lang::{Ctx, LanguagePlugin, extract_with, qualify};
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 extract_with(
25 LANGUAGE,
26 tree_sitter_python::LANGUAGE.into(),
27 file,
28 source,
29 |ctx, root, out| walk(ctx, root, None, false, out),
30 )
31 }
32}
33
34fn walk(ctx: &Ctx, node: Node, parent: Option<&str>, in_class: bool, out: &mut Vec<Symbol>) {
37 let mut cursor = node.walk();
38 for child in node.children(&mut cursor) {
39 match child.kind() {
40 "class_definition" => {
41 if let Some(name) = ctx.field_text(child, "name") {
42 let mut s = ctx.symbol(&name, Kind::Class, child, parent);
43 s.visibility = Some(name_visibility(&name));
44 out.push(s);
45 let qualified = qualify(parent, &name, ".");
46 walk(ctx, child, Some(&qualified), true, out);
47 }
48 }
49 "function_definition" => {
50 if let Some(name) = ctx.field_text(child, "name") {
51 let kind = if in_class {
52 Kind::Method
53 } else {
54 Kind::Function
55 };
56 let mut s = ctx.symbol(&name, kind, child, parent);
57 s.visibility = Some(name_visibility(&name));
58 out.push(s);
59 }
60 }
62 _ => walk(ctx, child, parent, in_class, out),
65 }
66 }
67}
68
69fn name_visibility(name: &str) -> &'static str {
72 let dunder = name.starts_with("__") && name.ends_with("__");
73 if name.starts_with('_') && !dunder {
74 "private"
75 } else {
76 "public"
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 fn extract(source: &str) -> Vec<Symbol> {
85 Python.extract("test.py", source)
86 }
87
88 fn find<'a>(syms: &'a [Symbol], name: &str) -> &'a Symbol {
89 syms.iter()
90 .find(|s| s.name == name)
91 .unwrap_or_else(|| panic!("no symbol named {name} in {syms:?}"))
92 }
93
94 #[test]
95 fn extracts_classes_methods_and_functions() {
96 let src = r#"
97class Account:
98 def deposit(self, amount):
99 pass
100
101 @property
102 def balance(self):
103 return 0
104
105def build():
106 return Account()
107"#;
108 let syms = extract(src);
109
110 let account = find(&syms, "Account");
111 assert_eq!(account.kind, Kind::Class);
112 assert_eq!(account.parent, None);
113
114 let deposit = find(&syms, "deposit");
115 assert_eq!(deposit.kind, Kind::Method);
116 assert_eq!(deposit.parent.as_deref(), Some("Account"));
117
118 assert_eq!(find(&syms, "balance").kind, Kind::Method);
120
121 let build = find(&syms, "build");
123 assert_eq!(build.kind, Kind::Function);
124 assert_eq!(build.parent, None);
125
126 assert_eq!(account.language, "python");
127 }
128
129 #[test]
130 fn empty_and_unparseable_yield_no_symbols() {
131 assert!(extract("").is_empty());
132 assert!(extract("# just a comment\n").is_empty());
133 }
134
135 #[test]
136 fn underscore_names_read_as_private_except_dunders() {
137 let src = "class Account:\n def _internal(self):\n pass\n def __init__(self):\n pass\n\ndef fetch():\n pass\n";
138 let syms = extract(src);
139 assert_eq!(find(&syms, "_internal").visibility, Some("private"));
140 assert_eq!(find(&syms, "__init__").visibility, Some("public"));
141 assert_eq!(find(&syms, "fetch").visibility, Some("public"));
142 }
143}