Skip to main content

code_moniker_core/lang/python/
mod.rs

1use tree_sitter::{Language, Parser, Tree};
2
3use crate::core::code_graph::CodeGraph;
4use crate::core::moniker::Moniker;
5use crate::core::shape::Shape;
6
7use crate::lang::KindSpec;
8
9pub mod build;
10mod canonicalize;
11mod kinds;
12mod sdk_pipeline;
13
14#[derive(Clone, Debug, Default)]
15pub struct Presets {}
16
17pub fn parse(source: &str) -> Tree {
18	let mut parser = Parser::new();
19	let language: Language = tree_sitter_python::LANGUAGE.into();
20	parser.set_language(&language).unwrap_or_else(|err| {
21		panic!("failed to load tree-sitter Python grammar: {err}");
22	});
23	parser.parse(source, None).unwrap_or_else(|| {
24		panic!("tree-sitter parse returned None on a non-cancelled call");
25	})
26}
27
28pub fn extract(
29	uri: &str,
30	source: &str,
31	anchor: &Moniker,
32	deep: bool,
33	presets: &Presets,
34) -> CodeGraph {
35	sdk_pipeline::extract(uri, source, anchor, deep, presets)
36}
37
38pub struct Lang;
39
40const DEF_KINDS: &[&str] = &[
41	"class",
42	"type",
43	"function",
44	"method",
45	"async_function",
46	"path",
47];
48
49const DEF_KIND_SPECS: &[KindSpec] = &[
50	KindSpec::new("class", Shape::Type, 20, "class"),
51	KindSpec::new("type", Shape::Type, 21, "type"),
52	KindSpec::new("function", Shape::Callable, 40, "function"),
53	KindSpec::new("async_function", Shape::Callable, 41, "async_function"),
54	KindSpec::new("method", Shape::Callable, 42, "method"),
55	KindSpec::new("path", Shape::Value, 63, "path"),
56];
57
58impl crate::lang::LangExtractor for Lang {
59	type Presets = Presets;
60	const LANG_TAG: &'static str = "python";
61	const ALLOWED_KINDS: &'static [&'static str] = DEF_KINDS;
62	const KIND_SPECS: &'static [KindSpec] = DEF_KIND_SPECS;
63	const ALLOWED_VISIBILITIES: &'static [&'static str] = &["public", "private", "module"];
64
65	fn extract(
66		uri: &str,
67		source: &str,
68		anchor: &Moniker,
69		deep: bool,
70		presets: &Self::Presets,
71	) -> CodeGraph {
72		extract(uri, source, anchor, deep, presets)
73	}
74}
75
76pub fn builtin_external_root(root: &str) -> bool {
77	root == "builtins" || sdk_pipeline::STDLIB_PACKAGES.binary_search(&root).is_ok()
78}
79
80#[cfg(test)]
81mod tests {
82	use super::*;
83	use crate::core::moniker::MonikerBuilder;
84	use crate::lang::assert_conformance;
85
86	fn make_anchor() -> Moniker {
87		MonikerBuilder::new().project(b"app").build()
88	}
89
90	fn extract_default(uri: &str, source: &str, anchor: &Moniker, deep: bool) -> CodeGraph {
91		let g = extract(uri, source, anchor, deep, &Presets::default());
92		assert_conformance::<super::Lang>(&g, anchor);
93		g
94	}
95
96	#[test]
97	fn parse_empty_returns_module() {
98		let tree = parse("");
99		assert_eq!(tree.root_node().kind(), "module");
100	}
101
102	#[test]
103	fn extract_module_uses_path_segments() {
104		let g = extract_default("acme/util/text.py", "", &make_anchor(), false);
105		let expected = MonikerBuilder::new()
106			.project(b"app")
107			.segment(b"lang", b"python")
108			.segment(b"package", b"acme")
109			.segment(b"package", b"util")
110			.segment(b"module", b"text")
111			.build();
112		assert_eq!(g.root(), &expected);
113	}
114
115	#[test]
116	fn extract_module_root_is_filename_only() {
117		let g = extract_default("foo.py", "", &make_anchor(), false);
118		let expected = MonikerBuilder::new()
119			.project(b"app")
120			.segment(b"lang", b"python")
121			.segment(b"module", b"foo")
122			.build();
123		assert_eq!(g.root(), &expected);
124	}
125
126	#[test]
127	fn extract_function_with_typed_params_emits_full_signature() {
128		let src = "def make(x: int, y: str) -> int:\n    return x\n";
129		let g = extract_default("m.py", src, &make_anchor(), false);
130		let f = g
131			.defs()
132			.find(|d| d.kind == b"function")
133			.expect("function def");
134		let last = f.moniker.as_view().segments().last().unwrap();
135		assert_eq!(last.kind, b"function");
136		assert_eq!(last.name, b"make(x:int,y:str)");
137		assert_eq!(f.signature, b"x:int,y:str".to_vec());
138	}
139
140	#[test]
141	fn extract_function_with_untyped_params_uses_name_only_slots() {
142		let src = "def f(a, b=1):\n    return a\n";
143		let g = extract_default("m.py", src, &make_anchor(), false);
144		let f = g
145			.defs()
146			.find(|d| d.kind == b"function")
147			.expect("function def");
148		let last = f.moniker.as_view().segments().last().unwrap();
149		assert_eq!(last.name, b"f(a,b)");
150		assert_eq!(f.signature, b"a,b".to_vec());
151	}
152
153	#[test]
154	fn extract_classmethod_excludes_cls_from_signature() {
155		let src = "class Foo:\n    @classmethod\n    def make(cls, x: int) -> 'Foo':\n        return cls()\n";
156		let g = extract_default("foo.py", src, &make_anchor(), false);
157		let m = g.defs().find(|d| d.kind == b"method").expect("method def");
158		assert_eq!(
159			m.moniker.as_view().segments().last().unwrap().name,
160			b"make(x:int)"
161		);
162	}
163
164	#[test]
165	fn extract_double_underscore_visibility_is_private() {
166		let src = "class Foo:\n    def __secret(self):\n        pass\n";
167		let g = extract_default("foo.py", src, &make_anchor(), false);
168		let m = g.defs().find(|d| d.kind == b"method").expect("method def");
169		assert_eq!(m.visibility, b"private".to_vec());
170	}
171
172	#[test]
173	fn extract_single_underscore_visibility_is_module() {
174		let src = "def _internal():\n    pass\n";
175		let g = extract_default("m.py", src, &make_anchor(), false);
176		let f = g
177			.defs()
178			.find(|d| d.kind == b"function")
179			.expect("function def");
180		assert_eq!(f.visibility, b"module".to_vec());
181	}
182
183	#[test]
184	fn extract_import_module_emits_imports_module() {
185		let src = "import os\nimport acme.util as u\n";
186		let g = extract_default("m.py", src, &make_anchor(), false);
187		let kinds: Vec<&[u8]> = g.refs().map(|r| r.kind.as_ref()).collect();
188		assert_eq!(kinds.iter().filter(|k| **k == b"imports_module").count(), 2);
189	}
190
191	#[test]
192	fn extract_stdlib_import_marks_external() {
193		let g = extract_default("m.py", "import json\n", &make_anchor(), false);
194		let r = g
195			.refs()
196			.find(|r| r.kind == b"imports_module")
197			.expect("imports_module");
198		assert_eq!(r.confidence, b"external".to_vec());
199	}
200
201	#[test]
202	fn extract_project_import_marks_imported() {
203		let g = extract_default("m.py", "import acme.util\n", &make_anchor(), false);
204		let r = g
205			.refs()
206			.find(|r| r.kind == b"imports_module")
207			.expect("imports_module");
208		assert_eq!(r.confidence, b"imported".to_vec());
209	}
210
211	#[test]
212	fn extract_from_import_emits_one_imports_symbol_per_name() {
213		let src = "from acme.util import a, b as c\n";
214		let g = extract_default("m.py", src, &make_anchor(), false);
215		let names: Vec<&[u8]> = g
216			.refs()
217			.filter(|r| r.kind == b"imports_symbol")
218			.map(|r| r.target.as_view().segments().last().unwrap().name)
219			.collect();
220		assert_eq!(names, vec![&b"a"[..], &b"b"[..]]);
221		let segs: Vec<_> = g
222			.refs()
223			.find(|r| r.kind == b"imports_symbol")
224			.unwrap()
225			.target
226			.as_view()
227			.segments()
228			.collect();
229		let kinds: Vec<&[u8]> = segs.iter().map(|s| s.kind).collect();
230		assert_eq!(
231			kinds,
232			vec![&b"lang"[..], &b"package"[..], &b"module"[..], &b"path"[..]]
233		);
234		let aliased = g
235			.refs()
236			.find(|r| r.kind == b"imports_symbol" && r.alias == b"c")
237			.expect("aliased import");
238		assert_eq!(aliased.alias, b"c".to_vec());
239	}
240
241	#[test]
242	fn extract_relative_import_resolves_against_importer() {
243		let src = "from .util import helper\n";
244		let g = extract_default("acme/m.py", src, &make_anchor(), false);
245		let r = g
246			.refs()
247			.find(|r| r.kind == b"imports_symbol")
248			.expect("imports_symbol");
249		let segs: Vec<_> = r.target.as_view().segments().collect();
250		let kinds: Vec<&[u8]> = segs.iter().map(|s| s.kind).collect();
251		let names: Vec<&[u8]> = segs.iter().map(|s| s.name).collect();
252		assert_eq!(
253			kinds,
254			vec![&b"lang"[..], &b"package"[..], &b"module"[..], &b"path"[..]]
255		);
256		assert_eq!(
257			names,
258			vec![&b"python"[..], &b"acme"[..], &b"util"[..], &b"helper"[..]]
259		);
260	}
261
262	#[test]
263	fn extract_relative_import_underflow_falls_back_to_external_pkg() {
264		let src = "from ...foo import bar\n";
265		let g = extract_default("m.py", src, &make_anchor(), false);
266		let r = g
267			.refs()
268			.find(|r| r.kind == b"imports_symbol")
269			.expect("imports_symbol");
270		let segs: Vec<_> = r.target.as_view().segments().collect();
271		assert_eq!(segs[0].kind, b"external_pkg");
272		assert_eq!(segs[0].name, b"...");
273	}
274
275	#[test]
276	fn extract_decorator_emits_annotates() {
277		let src = "import functools\n@functools.wraps(fn)\ndef g():\n    pass\n";
278		let g = extract_default("m.py", src, &make_anchor(), false);
279		let ann = g
280			.refs()
281			.find(|r| r.kind == b"annotates")
282			.expect("annotates ref");
283		assert_eq!(
284			ann.target.as_view().segments().last().unwrap().name,
285			b"wraps"
286		);
287	}
288
289	#[test]
290	fn extract_param_read_marks_confidence_local() {
291		let src = "def f(x):\n    return x\n";
292		let g = extract_default("m.py", src, &make_anchor(), true);
293		let r = g
294			.refs()
295			.find(|r| {
296				r.kind == b"reads" && r.target.as_view().segments().last().unwrap().name == b"x"
297			})
298			.expect("reads x");
299		assert_eq!(r.confidence, b"local".to_vec());
300	}
301
302	#[test]
303	fn extract_deep_emits_param_def() {
304		let src = "def f(x, y: int):\n    return x\n";
305		let g = extract_default("m.py", src, &make_anchor(), true);
306		let params: Vec<&[u8]> = g
307			.defs()
308			.filter(|d| d.kind == b"param")
309			.map(|d| d.moniker.as_view().segments().last().unwrap().name)
310			.collect();
311		assert!(params.contains(&&b"x"[..]));
312		assert!(params.contains(&&b"y"[..]));
313	}
314
315	#[test]
316	fn extract_async_function_tracks_local_assignments() {
317		let src = "async def f():\n    x = 1\n    return x\n";
318		let g = extract_default("m.py", src, &make_anchor(), true);
319		let r = g
320			.refs()
321			.find(|r| {
322				r.kind == b"reads" && r.target.as_view().segments().last().unwrap().name == b"x"
323			})
324			.expect("reads x");
325		assert_eq!(r.confidence, b"local".to_vec());
326	}
327
328	#[test]
329	fn extract_imported_identifier_read_targets_import_moniker() {
330		let src = "import asyncio\nasync def f():\n    return asyncio\n";
331		let g = extract_default("m.py", src, &make_anchor(), false);
332		let r = g
333			.refs()
334			.find(|r| r.kind == b"reads" && r.confidence == b"external")
335			.expect("external read");
336		let segs: Vec<_> = r.target.as_view().segments().collect();
337		assert_eq!(segs[0].kind, b"external_pkg");
338		assert_eq!(segs[0].name, b"asyncio");
339	}
340
341	#[test]
342	fn extract_self_member_call_resolves_via_typed_constructor_param() {
343		let src = "class Store:\n    def reserve(self) -> None:\n        pass\n\nclass Worker:\n    def __init__(self, store: Store) -> None:\n        self._store = store\n\n    def run(self) -> None:\n        self._store.reserve()\n";
344		let g = extract_default("m.py", src, &make_anchor(), false);
345		let r = g
346			.refs()
347			.find(|r| {
348				r.kind == b"method_call"
349					&& r.target.as_view().segments().last().unwrap().name == b"reserve()"
350			})
351			.expect("method_call Store.reserve");
352		assert_eq!(r.confidence, b"resolved".to_vec());
353		let parent = r.target.parent().expect("method parent");
354		assert_eq!(parent.as_view().segments().last().unwrap().name, b"Store");
355	}
356
357	#[test]
358	fn extract_self_member_call_uses_constructor_type_independent_of_method_order() {
359		let src = "class Store:\n    def reserve(self) -> None:\n        pass\n\nclass Other:\n    def reserve(self) -> None:\n        pass\n\nclass Worker:\n    def before_init(self) -> None:\n        self._store.reserve()\n\n    def __init__(self, store: Store) -> None:\n        self._store = store\n\n    def retarget(self, other: Other) -> None:\n        self._store = other\n\n    def after_retarget(self) -> None:\n        self._store.reserve()\n";
360		let g = extract_default("m.py", src, &make_anchor(), false);
361		let targets: Vec<Moniker> = g
362			.refs()
363			.filter(|r| {
364				r.kind == b"method_call"
365					&& r.target.as_view().segments().last().unwrap().name == b"reserve()"
366			})
367			.map(|r| r.target.clone())
368			.collect();
369		assert_eq!(targets.len(), 2);
370		for target in targets {
371			let parent = target.parent().expect("method parent");
372			assert_eq!(parent.as_view().segments().last().unwrap().name, b"Store");
373		}
374	}
375
376	#[test]
377	fn extract_self_callable_attr_call_targets_attribute_type_alias() {
378		let src = "from collections.abc import Callable\nfrom typing import TypeAlias\n\nCallback: TypeAlias = Callable[[], None]\n\nclass Worker:\n    def __init__(self, cb: Callback) -> None:\n        self._cb = cb\n\n    def run(self) -> None:\n        self._cb()\n";
379		let g = extract_default("m.py", src, &make_anchor(), false);
380		let r = g
381			.refs()
382			.find(|r| {
383				r.kind == b"calls"
384					&& r.target.as_view().segments().last().unwrap().name == b"Callback"
385			})
386			.expect("calls Callback type alias");
387		assert_eq!(r.confidence, b"resolved".to_vec());
388		assert!(!g.refs().any(|r| {
389			r.kind == b"method_call" && r.target.as_view().segments().last().unwrap().name == b"_cb"
390		}));
391	}
392
393	#[test]
394	fn extract_type_alias_emits_type_def_and_rhs_uses_type() {
395		let src = "from typing import TypeAlias\n\nclass User:\n    pass\n\nUserMap: TypeAlias = dict[str, User]\ntype UserResult = User | None\n";
396		let g = extract_default("m.py", src, &make_anchor(), false);
397		let type_names: Vec<&[u8]> = g
398			.defs()
399			.filter(|d| d.kind == b"type")
400			.map(|d| d.moniker.as_view().segments().last().unwrap().name)
401			.collect();
402		assert!(type_names.contains(&&b"UserMap"[..]));
403		assert!(type_names.contains(&&b"UserResult"[..]));
404		assert!(g.refs().any(|r| {
405			r.kind == b"uses_type"
406				&& r.target.as_view().segments().last().unwrap().name == b"User"
407				&& r.confidence == b"resolved"
408		}));
409		assert!(!g.refs().any(|r| {
410			r.kind == b"uses_type"
411				&& matches!(
412					r.target.as_view().segments().last().unwrap().name,
413					b"dict" | b"str" | b"None"
414				)
415		}));
416	}
417
418	#[test]
419	fn extract_local_class_call_emits_instantiates() {
420		let src = "class User:\n    pass\n\ndef make() -> User:\n    return User()\n";
421		let g = extract_default("m.py", src, &make_anchor(), false);
422		let r = g
423			.refs()
424			.find(|r| r.kind == b"instantiates")
425			.expect("instantiates User");
426		assert_eq!(r.confidence, b"resolved".to_vec());
427		assert_eq!(r.target.as_view().segments().last().unwrap().name, b"User");
428	}
429
430	#[test]
431	fn extract_local_class_call_prefers_function_scoped_type() {
432		let src = "class Local:\n    pass\n\ndef make():\n    class Local:\n        pass\n    return Local()\n";
433		let g = extract_default("m.py", src, &make_anchor(), false);
434		let r = g
435			.refs()
436			.find(|r| r.kind == b"instantiates")
437			.expect("instantiates local Local");
438		let target_last = r.target.as_view().segments().last().unwrap();
439		assert_eq!(target_last.kind, b"class");
440		assert_eq!(target_last.name, b"Local");
441		let parent = r.target.parent().expect("class parent");
442		let parent_last = parent.as_view().segments().last().unwrap();
443		assert_eq!(parent_last.kind, b"function");
444		assert_eq!(parent_last.name, b"make()");
445	}
446
447	#[test]
448	fn extract_keyword_argument_names_are_not_reads() {
449		let src = "def save(value):\n    return dict(id=value)\n";
450		let g = extract_default("m.py", src, &make_anchor(), false);
451		assert!(!g.refs().any(|r| {
452			r.kind == b"reads" && r.target.as_view().segments().last().unwrap().name == b"id"
453		}));
454	}
455
456	#[test]
457	fn extract_attribute_tail_is_not_a_bare_read() {
458		let src = "def f(payment):\n    return payment.id\n";
459		let g = extract_default("m.py", src, &make_anchor(), false);
460		assert!(!g.refs().any(|r| {
461			r.kind == b"reads" && r.target.as_view().segments().last().unwrap().name == b"id"
462		}));
463	}
464
465	#[test]
466	fn extract_function_docstring_emits_comment_def_parented_on_function() {
467		let src = "def f():\n    \"\"\"docstring\"\"\"\n    return 0\n";
468		let g = extract_default("m.py", src, &make_anchor(), false);
469		let fn_moniker = MonikerBuilder::new()
470			.project(b"app")
471			.segment(b"lang", b"python")
472			.segment(b"module", b"m")
473			.segment(b"function", b"f()")
474			.build();
475		let docstring_count = g
476			.defs()
477			.filter(|d| d.kind == b"comment")
478			.filter(|d| {
479				d.parent
480					.and_then(|i| g.defs().nth(i))
481					.is_some_and(|p| p.moniker == fn_moniker)
482			})
483			.count();
484		assert_eq!(
485			docstring_count,
486			1,
487			"function docstring must emit one comment def parented on the function. defs: {:?}",
488			g.def_monikers()
489		);
490	}
491
492	#[test]
493	fn extract_class_and_module_docstring_each_emit_one_comment() {
494		let src = "\"\"\"module doc\"\"\"\nclass A:\n    \"\"\"class doc\"\"\"\n    pass\n";
495		let g = extract_default("m.py", src, &make_anchor(), false);
496		assert_eq!(
497			g.defs().filter(|d| d.kind == b"comment").count(),
498			2,
499			"module-level and class docstrings should each yield one comment def. defs: {:?}",
500			g.def_monikers()
501		);
502	}
503
504	#[test]
505	fn extract_non_docstring_string_at_start_is_not_a_comment() {
506		let src = "x = \"hello\"\n";
507		let g = extract_default("m.py", src, &make_anchor(), false);
508		assert_eq!(
509			g.defs().filter(|d| d.kind == b"comment").count(),
510			0,
511			"string literals that aren't bare expression-statement-strings must NOT be treated as docstrings"
512		);
513	}
514}