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}