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
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use crate::core::moniker::MonikerBuilder;
80 use crate::lang::assert_conformance;
81
82 fn make_anchor() -> Moniker {
83 MonikerBuilder::new().project(b"app").build()
84 }
85
86 fn extract_default(uri: &str, source: &str, anchor: &Moniker, deep: bool) -> CodeGraph {
87 let g = extract(uri, source, anchor, deep, &Presets::default());
88 assert_conformance::<super::Lang>(&g, anchor);
89 g
90 }
91
92 #[test]
93 fn parse_empty_returns_module() {
94 let tree = parse("");
95 assert_eq!(tree.root_node().kind(), "module");
96 }
97
98 #[test]
99 fn extract_module_uses_path_segments() {
100 let g = extract_default("acme/util/text.py", "", &make_anchor(), false);
101 let expected = MonikerBuilder::new()
102 .project(b"app")
103 .segment(b"lang", b"python")
104 .segment(b"package", b"acme")
105 .segment(b"package", b"util")
106 .segment(b"module", b"text")
107 .build();
108 assert_eq!(g.root(), &expected);
109 }
110
111 #[test]
112 fn extract_module_root_is_filename_only() {
113 let g = extract_default("foo.py", "", &make_anchor(), false);
114 let expected = MonikerBuilder::new()
115 .project(b"app")
116 .segment(b"lang", b"python")
117 .segment(b"module", b"foo")
118 .build();
119 assert_eq!(g.root(), &expected);
120 }
121
122 #[test]
123 fn extract_function_with_typed_params_emits_full_signature() {
124 let src = "def make(x: int, y: str) -> int:\n return x\n";
125 let g = extract_default("m.py", src, &make_anchor(), false);
126 let f = g
127 .defs()
128 .find(|d| d.kind == b"function")
129 .expect("function def");
130 let last = f.moniker.as_view().segments().last().unwrap();
131 assert_eq!(last.kind, b"function");
132 assert_eq!(last.name, b"make(x:int,y:str)");
133 assert_eq!(f.signature, b"x:int,y:str".to_vec());
134 }
135
136 #[test]
137 fn extract_function_with_untyped_params_uses_name_only_slots() {
138 let src = "def f(a, b=1):\n return a\n";
139 let g = extract_default("m.py", src, &make_anchor(), false);
140 let f = g
141 .defs()
142 .find(|d| d.kind == b"function")
143 .expect("function def");
144 let last = f.moniker.as_view().segments().last().unwrap();
145 assert_eq!(last.name, b"f(a,b)");
146 assert_eq!(f.signature, b"a,b".to_vec());
147 }
148
149 #[test]
150 fn extract_classmethod_excludes_cls_from_signature() {
151 let src = "class Foo:\n @classmethod\n def make(cls, x: int) -> 'Foo':\n return cls()\n";
152 let g = extract_default("foo.py", src, &make_anchor(), false);
153 let m = g.defs().find(|d| d.kind == b"method").expect("method def");
154 assert_eq!(
155 m.moniker.as_view().segments().last().unwrap().name,
156 b"make(x:int)"
157 );
158 }
159
160 #[test]
161 fn extract_double_underscore_visibility_is_private() {
162 let src = "class Foo:\n def __secret(self):\n pass\n";
163 let g = extract_default("foo.py", src, &make_anchor(), false);
164 let m = g.defs().find(|d| d.kind == b"method").expect("method def");
165 assert_eq!(m.visibility, b"private".to_vec());
166 }
167
168 #[test]
169 fn extract_single_underscore_visibility_is_module() {
170 let src = "def _internal():\n pass\n";
171 let g = extract_default("m.py", src, &make_anchor(), false);
172 let f = g
173 .defs()
174 .find(|d| d.kind == b"function")
175 .expect("function def");
176 assert_eq!(f.visibility, b"module".to_vec());
177 }
178
179 #[test]
180 fn extract_import_module_emits_imports_module() {
181 let src = "import os\nimport acme.util as u\n";
182 let g = extract_default("m.py", src, &make_anchor(), false);
183 let kinds: Vec<&[u8]> = g.refs().map(|r| r.kind.as_ref()).collect();
184 assert_eq!(kinds.iter().filter(|k| **k == b"imports_module").count(), 2);
185 }
186
187 #[test]
188 fn extract_stdlib_import_marks_external() {
189 let g = extract_default("m.py", "import json\n", &make_anchor(), false);
190 let r = g
191 .refs()
192 .find(|r| r.kind == b"imports_module")
193 .expect("imports_module");
194 assert_eq!(r.confidence, b"external".to_vec());
195 }
196
197 #[test]
198 fn extract_project_import_marks_imported() {
199 let g = extract_default("m.py", "import acme.util\n", &make_anchor(), false);
200 let r = g
201 .refs()
202 .find(|r| r.kind == b"imports_module")
203 .expect("imports_module");
204 assert_eq!(r.confidence, b"imported".to_vec());
205 }
206
207 #[test]
208 fn extract_from_import_emits_one_imports_symbol_per_name() {
209 let src = "from acme.util import a, b as c\n";
210 let g = extract_default("m.py", src, &make_anchor(), false);
211 let names: Vec<&[u8]> = g
212 .refs()
213 .filter(|r| r.kind == b"imports_symbol")
214 .map(|r| r.target.as_view().segments().last().unwrap().name)
215 .collect();
216 assert_eq!(names, vec![&b"a"[..], &b"b"[..]]);
217 let segs: Vec<_> = g
218 .refs()
219 .find(|r| r.kind == b"imports_symbol")
220 .unwrap()
221 .target
222 .as_view()
223 .segments()
224 .collect();
225 let kinds: Vec<&[u8]> = segs.iter().map(|s| s.kind).collect();
226 assert_eq!(
227 kinds,
228 vec![&b"lang"[..], &b"package"[..], &b"module"[..], &b"path"[..]]
229 );
230 let aliased = g
231 .refs()
232 .find(|r| r.kind == b"imports_symbol" && r.alias == b"c")
233 .expect("aliased import");
234 assert_eq!(aliased.alias, b"c".to_vec());
235 }
236
237 #[test]
238 fn extract_future_annotations_does_not_emit_a_runtime_read() {
239 let g = extract_default(
240 "m.py",
241 "from __future__ import annotations\n",
242 &make_anchor(),
243 false,
244 );
245 assert!(!g.refs().any(|reference| {
246 reference.kind == b"reads"
247 && reference.target.as_view().segments().last().unwrap().name == b"annotations"
248 }));
249 }
250
251 #[test]
252 fn extract_runtime_module_globals_are_external() {
253 let g = extract_default(
254 "m.py",
255 "if __name__ == '__main__':\n pass\n",
256 &make_anchor(),
257 false,
258 );
259 let reference = g
260 .refs()
261 .find(|reference| reference.kind == b"reads")
262 .expect("reads __name__");
263 assert_eq!(reference.confidence, b"external".to_vec());
264 assert_eq!(
265 reference.target.as_view().segments().next().unwrap().name,
266 b"python"
267 );
268 assert_eq!(
269 reference.target.as_view().segments().nth(1).unwrap().name,
270 b"runtime"
271 );
272 }
273
274 #[test]
275 fn extract_wildcard_import_preserves_the_star_binding() {
276 let g = extract_default(
277 "acme/facade.py",
278 "from .implementation import *\n",
279 &make_anchor(),
280 false,
281 );
282 let reference = g
283 .refs()
284 .find(|reference| reference.kind == b"imports_module")
285 .expect("wildcard module import");
286
287 assert_eq!(reference.alias, b"*".to_vec());
288 }
289
290 #[test]
291 fn extract_static_all_emits_named_export_bindings() {
292 let src = "__all__ = [\n \"PublicClient\",\n # Kept public for compatibility.\n 'helper',\n]\nclass PublicClient:\n pass\ndef helper():\n pass\n";
293 let g = extract_default("acme/implementation.py", src, &make_anchor(), false);
294 let exports = g
295 .refs()
296 .filter(|reference| reference.kind == b"reexports" && !reference.alias.is_empty())
297 .map(|reference| reference.alias.clone())
298 .collect::<Vec<_>>();
299
300 assert_eq!(exports, vec![b"PublicClient".to_vec(), b"helper".to_vec()]);
301 }
302
303 #[test]
304 fn extract_all_assignments_preserve_empty_dynamic_and_extend_state() {
305 let src = "__all__ = []\n__all__ += ['Later']\n__all__ = build_exports()\n";
306 let g = extract_default("acme/facade.py", src, &make_anchor(), false);
307 let directives = g
308 .refs()
309 .filter(|reference| reference.kind == b"reexports" && reference.alias.is_empty())
310 .map(|reference| reference.receiver_hint.clone())
311 .collect::<Vec<_>>();
312
313 assert_eq!(
314 directives,
315 vec![
316 b"python_all_replace".to_vec(),
317 b"python_all_extend".to_vec(),
318 b"python_all_dynamic".to_vec(),
319 ]
320 );
321 }
322
323 #[test]
324 fn extract_conditional_import_marks_runtime_binding() {
325 let src = "if enabled:\n from acme.client import Client\n";
326 let g = extract_default("acme/facade.py", src, &make_anchor(), false);
327 let reference = g
328 .refs()
329 .find(|reference| reference.kind == b"imports_symbol")
330 .expect("conditional import");
331
332 assert_eq!(
333 reference.receiver_hint,
334 b"python_conditional_import".to_vec()
335 );
336 }
337
338 #[test]
339 fn extract_calls_through_conditional_imports_as_runtime_bindings() {
340 let src = "def build(enabled):\n if enabled:\n from acme.client import Client\n return Client()\n";
341 let g = extract_default("acme/facade.py", src, &make_anchor(), false);
342 let reference = g
343 .refs()
344 .find(|reference| reference.kind == b"calls")
345 .expect("call through conditional import");
346
347 assert_eq!(
348 reference.receiver_hint,
349 b"python_conditional_import".to_vec()
350 );
351 }
352
353 #[test]
354 fn extract_function_imports_do_not_leak_into_sibling_scopes() {
355 let src = "def configure():\n from acme.client import Client\n return Client()\n\ndef build():\n return Client()\n";
356 let g = extract_default("acme/facade.py", src, &make_anchor(), false);
357 let calls = g
358 .refs()
359 .filter(|reference| reference.kind == b"calls")
360 .map(|reference| {
361 let source = g
362 .def_at(reference.source)
363 .moniker
364 .as_view()
365 .segments()
366 .last()
367 .expect("call source")
368 .name
369 .to_vec();
370 (source, reference.confidence.to_vec())
371 })
372 .collect::<Vec<_>>();
373
374 assert!(
375 calls.contains(&(b"configure()".to_vec(), b"imported".to_vec())),
376 "{calls:?}"
377 );
378 assert!(
379 calls.contains(&(b"build()".to_vec(), b"name_match".to_vec())),
380 "{calls:?}"
381 );
382 }
383
384 #[test]
385 fn extract_local_import_shadows_conditional_module_binding() {
386 let src = "try:\n from acme.a import Client\nexcept ImportError:\n from acme.b import Client\n\ndef build():\n from acme.c import Client\n return Client()\n";
387 let g = extract_default("acme/facade.py", src, &make_anchor(), false);
388 let call = g
389 .refs()
390 .find(|reference| reference.kind == b"calls")
391 .expect("locally shadowed call");
392
393 assert_eq!(call.receiver_hint, b"");
394 assert_eq!(call.confidence, b"imported");
395 assert!(
396 call.target
397 .as_view()
398 .segments()
399 .any(|segment| { segment.kind == b"module" && segment.name == b"c" })
400 );
401 }
402
403 #[test]
404 fn extract_conditional_all_as_dynamic() {
405 let src = "if enabled:\n __all__ = ['Client']\nelse:\n __all__ = ['Fallback']\n";
406 let g = extract_default("acme/facade.py", src, &make_anchor(), false);
407 let directives = g
408 .refs()
409 .filter(|reference| reference.kind == b"reexports")
410 .map(|reference| (reference.alias.to_vec(), reference.receiver_hint.to_vec()))
411 .collect::<Vec<_>>();
412
413 assert_eq!(
414 directives,
415 vec![
416 (Vec::new(), b"python_all_dynamic".to_vec()),
417 (Vec::new(), b"python_all_dynamic".to_vec()),
418 ]
419 );
420 }
421
422 #[test]
423 fn extract_relative_import_resolves_against_importer() {
424 let src = "from .util import helper\n";
425 let g = extract_default("acme/m.py", src, &make_anchor(), false);
426 let r = g
427 .refs()
428 .find(|r| r.kind == b"imports_symbol")
429 .expect("imports_symbol");
430 let segs: Vec<_> = r.target.as_view().segments().collect();
431 let kinds: Vec<&[u8]> = segs.iter().map(|s| s.kind).collect();
432 let names: Vec<&[u8]> = segs.iter().map(|s| s.name).collect();
433 assert_eq!(
434 kinds,
435 vec![&b"lang"[..], &b"package"[..], &b"module"[..], &b"path"[..]]
436 );
437 assert_eq!(
438 names,
439 vec![&b"python"[..], &b"acme"[..], &b"util"[..], &b"helper"[..]]
440 );
441 }
442
443 #[test]
444 fn extract_relative_import_underflow_falls_back_to_external_pkg() {
445 let src = "from ...foo import bar\n";
446 let g = extract_default("m.py", src, &make_anchor(), false);
447 let r = g
448 .refs()
449 .find(|r| r.kind == b"imports_symbol")
450 .expect("imports_symbol");
451 let segs: Vec<_> = r.target.as_view().segments().collect();
452 assert_eq!(segs[0].kind, b"external_pkg");
453 assert_eq!(segs[0].name, b"...");
454 }
455
456 #[test]
457 fn extract_decorator_emits_annotates() {
458 let src = "import functools\n@functools.wraps(fn)\ndef g():\n pass\n";
459 let g = extract_default("m.py", src, &make_anchor(), false);
460 let ann = g
461 .refs()
462 .find(|r| r.kind == b"annotates")
463 .expect("annotates ref");
464 assert_eq!(
465 ann.target.as_view().segments().last().unwrap().name,
466 b"wraps"
467 );
468 }
469
470 #[test]
471 fn extract_param_read_marks_confidence_local() {
472 let src = "def f(x):\n return x\n";
473 let g = extract_default("m.py", src, &make_anchor(), true);
474 let r = g
475 .refs()
476 .find(|r| {
477 r.kind == b"reads" && r.target.as_view().segments().last().unwrap().name == b"x"
478 })
479 .expect("reads x");
480 assert_eq!(r.confidence, b"local".to_vec());
481 }
482
483 #[test]
484 fn extract_deep_emits_param_def() {
485 let src = "def f(x, y: int):\n return x\n";
486 let g = extract_default("m.py", src, &make_anchor(), true);
487 let params: Vec<&[u8]> = g
488 .defs()
489 .filter(|d| d.kind == b"param")
490 .map(|d| d.moniker.as_view().segments().last().unwrap().name)
491 .collect();
492 assert!(params.contains(&&b"x"[..]));
493 assert!(params.contains(&&b"y"[..]));
494 }
495
496 #[test]
497 fn extract_async_function_tracks_local_assignments() {
498 let src = "async def f():\n x = 1\n return x\n";
499 let g = extract_default("m.py", src, &make_anchor(), true);
500 let r = g
501 .refs()
502 .find(|r| {
503 r.kind == b"reads" && r.target.as_view().segments().last().unwrap().name == b"x"
504 })
505 .expect("reads x");
506 assert_eq!(r.confidence, b"local".to_vec());
507 }
508
509 #[test]
510 fn extract_imported_identifier_read_targets_import_moniker() {
511 let src = "import asyncio\nasync def f():\n return asyncio\n";
512 let g = extract_default("m.py", src, &make_anchor(), false);
513 let r = g
514 .refs()
515 .find(|r| r.kind == b"reads" && r.confidence == b"external")
516 .expect("external read");
517 let segs: Vec<_> = r.target.as_view().segments().collect();
518 assert_eq!(segs[0].kind, b"sdk");
519 assert_eq!(segs[0].name, b"python");
520 assert_eq!(segs[1].name, b"asyncio");
521 }
522
523 #[test]
524 fn extract_unknown_identifier_read_marks_incomplete_resolution() {
525 let src = "def f():\n return missing_name\n";
526 let g = extract_default("m.py", src, &make_anchor(), false);
527 let r = g
528 .refs()
529 .find(|r| {
530 r.kind == b"reads"
531 && r.target.as_view().segments().last().unwrap().name == b"missing_name"
532 })
533 .expect("reads missing_name");
534 assert_eq!(r.confidence, b"unresolved".to_vec());
535 }
536
537 #[test]
538 fn extract_same_module_callable_read_keeps_exact_resolution() {
539 let src = "def f():\n return callback\n\ndef callback():\n pass\n";
540 let g = extract_default("m.py", src, &make_anchor(), false);
541 let r = g
542 .refs()
543 .find(|r| {
544 r.kind == b"reads"
545 && r.target.as_view().segments().last().unwrap().name == b"callback()"
546 })
547 .expect("reads callback");
548 assert_eq!(r.confidence, b"resolved".to_vec());
549 }
550
551 #[test]
552 fn extract_module_callable_read_respects_local_shadowing() {
553 let src = "def callback():\n pass\n\ndef f(callback):\n return callback\n";
554 let g = extract_default("m.py", src, &make_anchor(), true);
555 let r = g
556 .refs()
557 .find(|r| {
558 r.kind == b"reads"
559 && r.target.as_view().segments().last().unwrap().name == b"callback"
560 })
561 .expect("reads local callback");
562 assert_eq!(r.confidence, b"local".to_vec());
563 assert_eq!(r.target.as_view().segments().last().unwrap().kind, b"local");
564 }
565
566 #[test]
567 fn extract_unknown_receiver_method_call_marks_incomplete_resolution() {
568 let src = "def f(value):\n return value.normalize()\n";
569 let g = extract_default("m.py", src, &make_anchor(), false);
570 let r = g
571 .refs()
572 .find(|r| {
573 r.kind == b"method_call"
574 && r.target.as_view().segments().last().unwrap().name == b"normalize"
575 })
576 .expect("method_call normalize");
577 assert_eq!(r.confidence, b"unresolved".to_vec());
578 }
579
580 #[test]
581 fn extract_self_member_call_resolves_via_typed_constructor_param() {
582 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";
583 let g = extract_default("m.py", src, &make_anchor(), false);
584 let r = g
585 .refs()
586 .find(|r| {
587 r.kind == b"method_call"
588 && r.target.as_view().segments().last().unwrap().name == b"reserve()"
589 })
590 .expect("method_call Store.reserve");
591 assert_eq!(r.confidence, b"resolved".to_vec());
592 let parent = r.target.parent().expect("method parent");
593 assert_eq!(parent.as_view().segments().last().unwrap().name, b"Store");
594 }
595
596 #[test]
597 fn extract_self_member_call_uses_constructor_type_independent_of_method_order() {
598 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";
599 let g = extract_default("m.py", src, &make_anchor(), false);
600 let targets: Vec<Moniker> = g
601 .refs()
602 .filter(|r| {
603 r.kind == b"method_call"
604 && r.target.as_view().segments().last().unwrap().name == b"reserve()"
605 })
606 .map(|r| r.target.clone())
607 .collect();
608 assert_eq!(targets.len(), 2);
609 for target in targets {
610 let parent = target.parent().expect("method parent");
611 assert_eq!(parent.as_view().segments().last().unwrap().name, b"Store");
612 }
613 }
614
615 #[test]
616 fn extract_self_callable_attr_call_targets_attribute_type_alias() {
617 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";
618 let g = extract_default("m.py", src, &make_anchor(), false);
619 let r = g
620 .refs()
621 .find(|r| {
622 r.kind == b"calls"
623 && r.target.as_view().segments().last().unwrap().name == b"Callback"
624 })
625 .expect("calls Callback type alias");
626 assert_eq!(r.confidence, b"resolved".to_vec());
627 assert!(!g.refs().any(|r| {
628 r.kind == b"method_call" && r.target.as_view().segments().last().unwrap().name == b"_cb"
629 }));
630 }
631
632 #[test]
633 fn extract_type_alias_emits_type_def_and_rhs_uses_type() {
634 let src = "from typing import TypeAlias\n\nclass User:\n pass\n\nUserMap: TypeAlias = dict[str, User]\ntype UserResult = User | None\n";
635 let g = extract_default("m.py", src, &make_anchor(), false);
636 let type_names: Vec<&[u8]> = g
637 .defs()
638 .filter(|d| d.kind == b"type")
639 .map(|d| d.moniker.as_view().segments().last().unwrap().name)
640 .collect();
641 assert!(type_names.contains(&&b"UserMap"[..]));
642 assert!(type_names.contains(&&b"UserResult"[..]));
643 assert!(g.refs().any(|r| {
644 r.kind == b"uses_type"
645 && r.target.as_view().segments().last().unwrap().name == b"User"
646 && r.confidence == b"resolved"
647 }));
648 assert!(!g.refs().any(|r| {
649 r.kind == b"uses_type"
650 && matches!(
651 r.target.as_view().segments().last().unwrap().name,
652 b"dict" | b"str" | b"None"
653 )
654 }));
655 }
656
657 #[test]
658 fn extract_local_class_call_emits_instantiates() {
659 let src = "class User:\n pass\n\ndef make() -> User:\n return User()\n";
660 let g = extract_default("m.py", src, &make_anchor(), false);
661 let r = g
662 .refs()
663 .find(|r| r.kind == b"instantiates")
664 .expect("instantiates User");
665 assert_eq!(r.confidence, b"resolved".to_vec());
666 assert_eq!(r.target.as_view().segments().last().unwrap().name, b"User");
667 }
668
669 #[test]
670 fn extract_callable_return_annotation_emits_returns_type() {
671 let src = "class User:\n pass\n\ndef make() -> User:\n return User()\n";
672 let g = extract_default("m.py", src, &make_anchor(), false);
673 let r = g
674 .refs()
675 .find(|r| r.kind == b"returns_type")
676 .expect("returns_type User");
677 assert_eq!(r.confidence, b"resolved".to_vec());
678 assert_eq!(r.target.as_view().segments().last().unwrap().kind, b"class");
679 assert_eq!(r.target.as_view().segments().last().unwrap().name, b"User");
680 }
681
682 #[test]
683 fn extract_local_assignment_uses_annotated_factory_return_type() {
684 let src = "class User:\n def label(self) -> str:\n return 'user'\n\ndef make() -> User:\n return User()\n\ndef render():\n value = make()\n return value.label()\n";
685 let g = extract_default("m.py", src, &make_anchor(), false);
686 let r = g
687 .refs()
688 .find(|r| {
689 r.kind == b"method_call"
690 && r.target.as_view().segments().last().unwrap().name == b"label()"
691 })
692 .expect("method_call User.label");
693 assert_eq!(r.confidence, b"resolved".to_vec());
694 assert_eq!(
695 r.target
696 .parent()
697 .expect("method parent")
698 .as_view()
699 .segments()
700 .last()
701 .unwrap()
702 .name,
703 b"User"
704 );
705 }
706
707 #[test]
708 fn extract_shadowed_factory_does_not_leak_module_return_type() {
709 let src = "class User:\n def label(self) -> str:\n return 'user'\n\ndef make() -> User:\n return User()\n\ndef render(make):\n value = make()\n return value.label()\n";
710 let g = extract_default("m.py", src, &make_anchor(), false);
711 let r = g
712 .refs()
713 .find(|r| {
714 r.kind == b"method_call"
715 && r.target.as_view().segments().last().unwrap().name == b"label"
716 })
717 .expect("method_call label");
718 assert_eq!(r.confidence, b"unresolved".to_vec());
719 }
720
721 #[test]
722 fn extract_later_assignment_still_shadows_module_factory() {
723 let src = "class User:\n def label(self) -> str:\n return 'user'\n\ndef make() -> User:\n return User()\n\ndef render():\n value = make()\n make = lambda: None\n return value.label()\n";
724 let g = extract_default("m.py", src, &make_anchor(), false);
725 let r = g
726 .refs()
727 .find(|r| {
728 r.kind == b"method_call"
729 && r.target.as_view().segments().last().unwrap().name == b"label"
730 })
731 .expect("method_call label");
732 assert_eq!(r.confidence, b"unresolved".to_vec());
733 }
734
735 #[test]
736 fn extract_factory_return_type_uses_defining_scope() {
737 let src = "class Product:\n def label(self) -> str:\n return 'product'\n\ndef make() -> Product:\n return Product()\n\nclass View:\n class Product:\n def label(self) -> str:\n return 'shadow'\n\n def render(self):\n value = make()\n return value.label()\n";
738 let g = extract_default("m.py", src, &make_anchor(), false);
739 let r = g
740 .refs()
741 .find(|r| {
742 r.kind == b"method_call"
743 && g.defs().nth(r.source).is_some_and(|source| {
744 source
745 .moniker
746 .as_view()
747 .segments()
748 .last()
749 .is_some_and(|segment| segment.name == b"render()")
750 }) && r.target.as_view().segments().last().unwrap().name == b"label()"
751 })
752 .expect("method_call Product.label");
753 let owner = r.target.parent().expect("method owner");
754 assert_eq!(owner.as_view().segments().last().unwrap().name, b"Product");
755 assert_eq!(owner.parent(), Some(g.root().clone()));
756 }
757
758 #[test]
759 fn extract_bare_call_in_method_does_not_bind_sibling_method() {
760 let src = "class Product:\n def label(self):\n pass\n\nclass Factory:\n def make(self) -> Product:\n return Product()\n\n def render(self):\n value = make()\n return value.label()\n";
761 let g = extract_default("m.py", src, &make_anchor(), false);
762 let r = g
763 .refs()
764 .find(|r| {
765 r.kind == b"method_call"
766 && r.target.as_view().segments().last().unwrap().name == b"label"
767 })
768 .expect("method_call label");
769 assert_eq!(r.confidence, b"unresolved".to_vec());
770 }
771
772 #[test]
773 fn extract_builtin_annotated_receiver_method_is_external() {
774 let src = "def normalize(value: str) -> str:\n return value.strip()\n";
775 let g = extract_default("m.py", src, &make_anchor(), false);
776 let r = g
777 .refs()
778 .find(|r| {
779 r.kind == b"method_call"
780 && r.target.as_view().segments().last().unwrap().name == b"strip"
781 })
782 .expect("method_call str.strip");
783 assert_eq!(r.confidence, b"external".to_vec());
784 let segments = r.target.as_view().segments().collect::<Vec<_>>();
785 assert_eq!(segments[0].kind, b"sdk");
786 assert_eq!(segments[0].name, b"python");
787 assert_eq!(segments[1].name, b"builtins");
788 assert_eq!(segments[2].name, b"str");
789 }
790
791 #[test]
792 fn extract_builtin_generic_receiver_uses_container_type() {
793 let src = "def add(values: list[str]):\n values.append('x')\n";
794 let g = extract_default("m.py", src, &make_anchor(), false);
795 let r = g
796 .refs()
797 .find(|reference| reference.kind == b"method_call")
798 .expect("method_call list.append");
799 assert_eq!(r.confidence, b"external".to_vec());
800 assert_eq!(r.target.as_view().segments().nth(2).unwrap().name, b"list");
801 }
802
803 #[test]
804 fn extract_variadic_parameters_use_runtime_container_types() {
805 let src = "def collect(*items: str, **options: int):\n items.count('x')\n options.get('limit')\n";
806 let g = extract_default("m.py", src, &make_anchor(), false);
807 let targets = g
808 .refs()
809 .filter(|reference| reference.kind == b"method_call")
810 .map(|reference| {
811 reference
812 .target
813 .as_view()
814 .segments()
815 .nth(2)
816 .unwrap()
817 .name
818 .to_vec()
819 })
820 .collect::<Vec<_>>();
821 assert!(targets.contains(&b"tuple".to_vec()), "{targets:?}");
822 assert!(targets.contains(&b"dict".to_vec()), "{targets:?}");
823 }
824
825 #[test]
826 fn extract_with_alias_preserves_constructed_receiver_type() {
827 let src = "class Session:\n def __enter__(self) -> Session:\n return self\n\n def __exit__(self, exc_type, exc, tb):\n pass\n\n def close(self):\n pass\n\ndef run():\n with Session() as session:\n session.close()\n";
828 let g = extract_default("m.py", src, &make_anchor(), false);
829 let r = g
830 .refs()
831 .find(|reference| reference.kind == b"method_call")
832 .expect("method_call Session.close");
833 assert_eq!(r.confidence, b"resolved".to_vec());
834 assert_eq!(
835 r.target
836 .parent()
837 .unwrap()
838 .as_view()
839 .segments()
840 .last()
841 .unwrap()
842 .name,
843 b"Session"
844 );
845 }
846
847 #[test]
848 fn extract_annotated_iterable_types_the_loop_binding() {
849 let src = "class Item:\n def label(self):\n pass\n\ndef render(items: list[Item]):\n for item in items:\n item.label()\n";
850 let g = extract_default("m.py", src, &make_anchor(), false);
851 let r = g
852 .refs()
853 .find(|reference| reference.kind == b"method_call")
854 .expect("method_call Item.label");
855 assert_eq!(r.confidence, b"resolved".to_vec());
856 assert_eq!(
857 r.target
858 .parent()
859 .unwrap()
860 .as_view()
861 .segments()
862 .last()
863 .unwrap()
864 .name,
865 b"Item"
866 );
867 }
868
869 #[test]
870 fn extract_heterogeneous_tuple_annotation_keeps_all_loop_element_types() {
871 let src = "class Alpha:\n pass\n\nclass Beta:\n pass\n\ndef render(values: tuple[Alpha, Beta]):\n for value in values:\n pass\n";
872 let g = extract_default("m.py", src, &make_anchor(), false);
873 let mut targets = g
874 .refs()
875 .filter(|reference| reference.kind == b"typed_as" && reference.alias == b"value")
876 .map(|reference| {
877 reference
878 .target
879 .as_view()
880 .segments()
881 .last()
882 .unwrap()
883 .name
884 .to_vec()
885 })
886 .collect::<Vec<_>>();
887 targets.sort();
888 assert_eq!(targets, vec![b"Alpha".to_vec(), b"Beta".to_vec()]);
889 }
890
891 #[test]
892 fn extract_except_alias_uses_the_exception_type() {
893 let src = "class Problem(Exception):\n def explain(self):\n pass\n\ndef run():\n try:\n pass\n except Problem as error:\n error.explain()\n";
894 let g = extract_default("m.py", src, &make_anchor(), false);
895 let r = g
896 .refs()
897 .find(|reference| reference.kind == b"method_call")
898 .expect("method_call Problem.explain");
899 assert_eq!(r.confidence, b"resolved".to_vec());
900 assert_eq!(
901 r.target
902 .parent()
903 .unwrap()
904 .as_view()
905 .segments()
906 .last()
907 .unwrap()
908 .name,
909 b"Problem"
910 );
911 }
912
913 #[test]
914 fn extract_exception_tuple_keeps_all_alias_types() {
915 let src = "class AlphaError(Exception):\n pass\n\nclass BetaError(Exception):\n pass\n\ndef run():\n try:\n pass\n except (AlphaError, BetaError) as error:\n pass\n";
916 let g = extract_default("m.py", src, &make_anchor(), false);
917 let mut targets = g
918 .refs()
919 .filter(|reference| reference.kind == b"typed_as" && reference.alias == b"error")
920 .map(|reference| {
921 reference
922 .target
923 .as_view()
924 .segments()
925 .last()
926 .unwrap()
927 .name
928 .to_vec()
929 })
930 .collect::<Vec<_>>();
931 targets.sort();
932 assert_eq!(targets, vec![b"AlphaError".to_vec(), b"BetaError".to_vec()]);
933 }
934
935 #[test]
936 fn extract_distinct_builtin_union_does_not_invent_receiver_type() {
937 let src = "def normalize(value: str | bytes):\n return value.strip()\n";
938 let g = extract_default("m.py", src, &make_anchor(), false);
939 let r = g
940 .refs()
941 .find(|r| {
942 r.kind == b"method_call"
943 && r.target.as_view().segments().last().unwrap().name == b"strip"
944 })
945 .expect("method_call strip");
946 assert_eq!(r.confidence, b"unresolved".to_vec());
947 }
948
949 #[test]
950 fn extract_workspace_union_parameter_emits_each_local_type_fact() {
951 let src = "class Alpha:\n pass\n\nclass Beta:\n pass\n\ndef render(value: Alpha | Beta):\n return value\n";
952 let g = extract_default("m.py", src, &make_anchor(), false);
953 let mut targets = g
954 .refs()
955 .filter(|reference| reference.kind == b"typed_as" && reference.alias == b"value")
956 .map(|reference| {
957 reference
958 .target
959 .as_view()
960 .segments()
961 .last()
962 .unwrap()
963 .name
964 .to_vec()
965 })
966 .collect::<Vec<_>>();
967 targets.sort();
968 assert_eq!(targets, vec![b"Alpha".to_vec(), b"Beta".to_vec()]);
969 }
970
971 #[test]
972 fn extract_optional_builtin_does_not_invent_receiver_type() {
973 let src = "from typing import Optional\n\ndef normalize(value: Optional[str]):\n return value.strip()\n";
974 let g = extract_default("m.py", src, &make_anchor(), false);
975 let r = g
976 .refs()
977 .find(|r| {
978 r.kind == b"method_call"
979 && r.target.as_view().segments().last().unwrap().name == b"strip"
980 })
981 .expect("method_call strip");
982 assert_eq!(r.confidence, b"unresolved".to_vec());
983 }
984
985 #[test]
986 fn extract_workspace_type_shadows_builtin_name() {
987 let src = "class str:\n def custom(self):\n pass\n\ndef use(value: str):\n value.custom()\n";
988 let g = extract_default("m.py", src, &make_anchor(), false);
989 let r = g
990 .refs()
991 .find(|r| {
992 r.kind == b"method_call"
993 && r.target.as_view().segments().last().unwrap().name == b"custom()"
994 })
995 .expect("method_call workspace str.custom");
996 assert_eq!(r.confidence, b"resolved".to_vec());
997 assert_eq!(r.target.parent().unwrap().parent(), Some(g.root().clone()));
998 }
999
1000 #[test]
1001 fn extract_literal_assignment_infers_builtin_receiver_type() {
1002 let src = "def normalize():\n value = ' user '\n return value.strip()\n";
1003 let g = extract_default("m.py", src, &make_anchor(), false);
1004 let r = g
1005 .refs()
1006 .find(|r| {
1007 r.kind == b"method_call"
1008 && r.target.as_view().segments().last().unwrap().name == b"strip"
1009 })
1010 .expect("method_call str.strip");
1011 assert_eq!(r.confidence, b"external".to_vec());
1012 assert_eq!(r.target.as_view().segments().nth(2).unwrap().name, b"str");
1013 }
1014
1015 #[test]
1016 fn extract_raw_bytes_literal_infers_bytes_receiver_type() {
1017 let src = "def decode():\n value = rb'user'\n return value.decode()\n";
1018 let g = extract_default("m.py", src, &make_anchor(), false);
1019 let r = g
1020 .refs()
1021 .find(|r| {
1022 r.kind == b"method_call"
1023 && r.target.as_view().segments().last().unwrap().name == b"decode"
1024 })
1025 .expect("method_call bytes.decode");
1026 assert_eq!(r.confidence, b"external".to_vec());
1027 assert_eq!(r.target.as_view().segments().nth(2).unwrap().name, b"bytes");
1028 }
1029
1030 #[test]
1031 fn extract_builtin_return_annotation_emits_external_returns_type() {
1032 let src = "def label() -> str:\n return 'user'\n";
1033 let g = extract_default("m.py", src, &make_anchor(), false);
1034 let r = g
1035 .refs()
1036 .find(|r| r.kind == b"returns_type")
1037 .expect("returns_type str");
1038 assert_eq!(r.confidence, b"external".to_vec());
1039 assert_eq!(r.target.as_view().segments().nth(2).unwrap().name, b"str");
1040 }
1041
1042 #[test]
1043 fn extract_async_return_type_requires_await_for_local_inference() {
1044 let src = "class User:\n def label(self) -> str:\n return 'user'\n\nasync def make() -> User:\n return User()\n\nasync def direct():\n value = make()\n return value.label()\n\nasync def awaited():\n value = await make()\n return value.label()\n";
1045 let g = extract_default("m.py", src, &make_anchor(), false);
1046 assert!(!g.refs().any(|r| r.kind == b"returns_type" && {
1047 g.defs().nth(r.source).is_some_and(|source| {
1048 source
1049 .moniker
1050 .as_view()
1051 .segments()
1052 .last()
1053 .is_some_and(|segment| segment.name == b"make()")
1054 })
1055 }));
1056 let method_calls = g
1057 .refs()
1058 .filter(|r| r.kind == b"method_call")
1059 .map(|r| {
1060 let source = g.defs().nth(r.source).unwrap();
1061 (
1062 source
1063 .moniker
1064 .as_view()
1065 .segments()
1066 .last()
1067 .unwrap()
1068 .name
1069 .to_vec(),
1070 r.confidence.to_vec(),
1071 )
1072 })
1073 .collect::<Vec<_>>();
1074 assert!(method_calls.contains(&(b"direct()".to_vec(), b"unresolved".to_vec())));
1075 assert!(method_calls.contains(&(b"awaited()".to_vec(), b"resolved".to_vec())));
1076 }
1077
1078 #[test]
1079 fn extract_async_method_does_not_emit_direct_return_type() {
1080 let src = "class User:\n pass\n\nclass Factory:\n async def make(self) -> User:\n return User()\n";
1081 let g = extract_default("m.py", src, &make_anchor(), false);
1082 assert!(!g.refs().any(|r| r.kind == b"returns_type"));
1083 }
1084
1085 #[test]
1086 fn extract_conflicting_factory_returns_do_not_type_local_assignment() {
1087 let src = "class First:\n def marker(self):\n pass\n\nclass Second:\n def marker(self):\n pass\n\ndef make() -> First:\n return First()\n\ndef make() -> Second:\n return Second()\n\ndef render():\n value = make()\n return value.marker()\n";
1088 let g = extract_default("m.py", src, &make_anchor(), false);
1089 let r = g
1090 .refs()
1091 .find(|r| {
1092 r.kind == b"method_call"
1093 && r.target.as_view().segments().last().unwrap().name == b"marker"
1094 })
1095 .expect("method_call marker");
1096 assert_eq!(r.confidence, b"unresolved".to_vec());
1097 assert!(!g.refs().any(|r| r.kind == b"returns_type"));
1098 }
1099
1100 #[test]
1101 fn extract_local_import_shadows_module_factory_return() {
1102 let src = "class Local:\n def marker(self):\n pass\n\ndef make() -> Local:\n return Local()\n\ndef render():\n from other import make\n value = make()\n return value.marker()\n";
1103 let g = extract_default("m.py", src, &make_anchor(), false);
1104 let r = g
1105 .refs()
1106 .find(|r| {
1107 r.kind == b"method_call"
1108 && r.target.as_view().segments().last().unwrap().name == b"marker"
1109 })
1110 .expect("method_call marker");
1111 assert_ne!(
1112 r.target
1113 .parent()
1114 .unwrap()
1115 .as_view()
1116 .segments()
1117 .last()
1118 .unwrap()
1119 .name,
1120 b"Local"
1121 );
1122 }
1123
1124 #[test]
1125 fn extract_aliased_optional_does_not_become_receiver_type() {
1126 let src = "from typing import Optional as Maybe\n\ndef normalize(value: Maybe[str]):\n return value.strip()\n";
1127 let g = extract_default("m.py", src, &make_anchor(), false);
1128 let r = g
1129 .refs()
1130 .find(|r| {
1131 r.kind == b"method_call"
1132 && r.target.as_view().segments().last().unwrap().name == b"strip"
1133 })
1134 .expect("method_call strip");
1135 assert_eq!(r.confidence, b"unresolved".to_vec());
1136 }
1137
1138 #[test]
1139 fn extract_typing_any_remains_dynamic() {
1140 let src = "from typing import Any\n\ndef make() -> Any:\n raise RuntimeError\n\ndef use(value: Any):\n return value.dynamic()\n";
1141 let g = extract_default("m.py", src, &make_anchor(), false);
1142 assert!(!g.refs().any(|r| r.kind == b"returns_type"));
1143 let r = g
1144 .refs()
1145 .find(|r| {
1146 r.kind == b"method_call"
1147 && r.target.as_view().segments().last().unwrap().name == b"dynamic"
1148 })
1149 .expect("method_call dynamic");
1150 assert_eq!(r.confidence, b"unresolved".to_vec());
1151 }
1152
1153 #[test]
1154 fn extract_conflicting_local_assignment_types_remain_ambiguous() {
1155 let src = "def mutate(flag):\n if flag:\n value = []\n else:\n value = {}\n value.append(1)\n";
1156 let g = extract_default("m.py", src, &make_anchor(), false);
1157 let r = g
1158 .refs()
1159 .find(|r| {
1160 r.kind == b"method_call"
1161 && r.target.as_view().segments().last().unwrap().name == b"append"
1162 })
1163 .expect("method_call append");
1164 assert_eq!(r.confidence, b"unresolved".to_vec());
1165 }
1166
1167 #[test]
1168 fn extract_unknown_reassignment_invalidates_known_local_type() {
1169 let src = "def mutate():\n value = []\n value = unknown()\n value.append(1)\n";
1170 let g = extract_default("m.py", src, &make_anchor(), false);
1171 let r = g
1172 .refs()
1173 .find(|r| {
1174 r.kind == b"method_call"
1175 && r.target.as_view().segments().last().unwrap().name == b"append"
1176 })
1177 .expect("method_call append");
1178 assert_eq!(r.confidence, b"unresolved".to_vec());
1179 }
1180
1181 #[test]
1182 fn extract_conflicting_instance_attribute_types_remain_ambiguous() {
1183 let src = "class Holder:\n def mutate(self, flag):\n if flag:\n self.value = []\n else:\n self.value = {}\n self.value.append(1)\n";
1184 let g = extract_default("m.py", src, &make_anchor(), false);
1185 let r = g
1186 .refs()
1187 .find(|r| {
1188 r.kind == b"method_call"
1189 && r.target.as_view().segments().last().unwrap().name == b"append"
1190 })
1191 .expect("method_call append");
1192 assert_eq!(r.confidence, b"unresolved".to_vec());
1193 }
1194
1195 #[test]
1196 fn extract_qualified_typing_any_does_not_bind_workspace_homonym() {
1197 let src = "import typing\n\nclass Any:\n def dynamic(self):\n pass\n\ndef use(value: typing.Any):\n value.dynamic()\n";
1198 let g = extract_default("m.py", src, &make_anchor(), false);
1199 let r = g
1200 .refs()
1201 .find(|r| {
1202 r.kind == b"method_call"
1203 && r.target.as_view().segments().last().unwrap().name == b"dynamic"
1204 })
1205 .expect("method_call dynamic");
1206 assert_eq!(r.confidence, b"unresolved".to_vec());
1207 }
1208
1209 #[test]
1210 fn extract_match_capture_shadows_module_factory() {
1211 let src = "class Local:\n def marker(self):\n pass\n\ndef make() -> Local:\n return Local()\n\ndef render(subject):\n value = make()\n match subject:\n case make:\n pass\n return value.marker()\n";
1212 let g = extract_default("m.py", src, &make_anchor(), false);
1213 let r = g
1214 .refs()
1215 .find(|r| {
1216 r.kind == b"method_call"
1217 && r.target.as_view().segments().last().unwrap().name == b"marker"
1218 })
1219 .expect("method_call marker");
1220 assert_eq!(r.confidence, b"unresolved".to_vec());
1221 }
1222
1223 #[test]
1224 fn extract_comprehension_walrus_shadows_module_factory() {
1225 let src = "class Local:\n def marker(self):\n pass\n\ndef make() -> Local:\n return Local()\n\ndef render(items):\n value = make()\n selected = [item for item in items if (make := item)]\n return value.marker()\n";
1226 let g = extract_default("m.py", src, &make_anchor(), false);
1227 let r = g
1228 .refs()
1229 .find(|r| {
1230 r.kind == b"method_call"
1231 && r.target.as_view().segments().last().unwrap().name == b"marker"
1232 })
1233 .expect("method_call marker");
1234 assert_eq!(r.confidence, b"unresolved".to_vec());
1235 }
1236
1237 #[test]
1238 fn extract_inner_param_shadows_outer_inferred_type() {
1239 let src =
1240 "def outer():\n value = []\n\n def inner(value):\n value.append(1)\n";
1241 let g = extract_default("m.py", src, &make_anchor(), false);
1242 let r = g
1243 .refs()
1244 .find(|r| {
1245 r.kind == b"method_call"
1246 && r.target.as_view().segments().last().unwrap().name == b"append"
1247 })
1248 .expect("method_call append");
1249 assert_eq!(r.confidence, b"unresolved".to_vec());
1250 }
1251
1252 #[test]
1253 fn extract_open_union_return_keeps_known_type_with_open_marker() {
1254 let src = "class User:\n pass\n\ndef maybe_make() -> User | None:\n return None\n";
1255 let g = extract_default("m.py", src, &make_anchor(), false);
1256 let fact = g
1257 .refs()
1258 .find(|reference| reference.kind == b"returns_type")
1259 .expect("known User return candidate");
1260 assert_eq!(
1261 fact.target.as_view().segments().last().unwrap().name,
1262 b"User"
1263 );
1264 assert_eq!(fact.receiver_hint, b"python_open_type_set".to_vec());
1265 }
1266
1267 #[test]
1268 fn extract_closed_union_return_emits_each_type_fact() {
1269 let src = "class Alpha:\n pass\n\nclass Beta:\n pass\n\ndef make() -> Alpha | Beta:\n return Alpha()\n";
1270 let g = extract_default("m.py", src, &make_anchor(), false);
1271 let mut targets = g
1272 .refs()
1273 .filter(|reference| reference.kind == b"returns_type")
1274 .map(|reference| {
1275 reference
1276 .target
1277 .as_view()
1278 .segments()
1279 .last()
1280 .unwrap()
1281 .name
1282 .to_vec()
1283 })
1284 .collect::<Vec<_>>();
1285 targets.sort();
1286 assert_eq!(targets, vec![b"Alpha".to_vec(), b"Beta".to_vec()]);
1287 }
1288
1289 #[test]
1290 fn extract_local_class_call_prefers_function_scoped_type() {
1291 let src = "class Local:\n pass\n\ndef make():\n class Local:\n pass\n return Local()\n";
1292 let g = extract_default("m.py", src, &make_anchor(), false);
1293 let r = g
1294 .refs()
1295 .find(|r| r.kind == b"instantiates")
1296 .expect("instantiates local Local");
1297 let target_last = r.target.as_view().segments().last().unwrap();
1298 assert_eq!(target_last.kind, b"class");
1299 assert_eq!(target_last.name, b"Local");
1300 let parent = r.target.parent().expect("class parent");
1301 let parent_last = parent.as_view().segments().last().unwrap();
1302 assert_eq!(parent_last.kind, b"function");
1303 assert_eq!(parent_last.name, b"make()");
1304 }
1305
1306 #[test]
1307 fn extract_keyword_argument_names_are_not_reads() {
1308 let src = "def save(value):\n return dict(id=value)\n";
1309 let g = extract_default("m.py", src, &make_anchor(), false);
1310 assert!(!g.refs().any(|r| {
1311 r.kind == b"reads" && r.target.as_view().segments().last().unwrap().name == b"id"
1312 }));
1313 }
1314
1315 #[test]
1316 fn extract_attribute_tail_is_not_a_bare_read() {
1317 let src = "def f(payment):\n return payment.id\n";
1318 let g = extract_default("m.py", src, &make_anchor(), false);
1319 assert!(!g.refs().any(|r| {
1320 r.kind == b"reads" && r.target.as_view().segments().last().unwrap().name == b"id"
1321 }));
1322 }
1323
1324 #[test]
1325 fn extract_function_docstring_emits_comment_def_parented_on_function() {
1326 let src = "def f():\n \"\"\"docstring\"\"\"\n return 0\n";
1327 let g = extract_default("m.py", src, &make_anchor(), false);
1328 let fn_moniker = MonikerBuilder::new()
1329 .project(b"app")
1330 .segment(b"lang", b"python")
1331 .segment(b"module", b"m")
1332 .segment(b"function", b"f()")
1333 .build();
1334 let docstring_count = g
1335 .defs()
1336 .filter(|d| d.kind == b"comment")
1337 .filter(|d| {
1338 d.parent
1339 .and_then(|i| g.defs().nth(i))
1340 .is_some_and(|p| p.moniker == fn_moniker)
1341 })
1342 .count();
1343 assert_eq!(
1344 docstring_count,
1345 1,
1346 "function docstring must emit one comment def parented on the function. defs: {:?}",
1347 g.def_monikers()
1348 );
1349 }
1350
1351 #[test]
1352 fn extract_class_and_module_docstring_each_emit_one_comment() {
1353 let src = "\"\"\"module doc\"\"\"\nclass A:\n \"\"\"class doc\"\"\"\n pass\n";
1354 let g = extract_default("m.py", src, &make_anchor(), false);
1355 assert_eq!(
1356 g.defs().filter(|d| d.kind == b"comment").count(),
1357 2,
1358 "module-level and class docstrings should each yield one comment def. defs: {:?}",
1359 g.def_monikers()
1360 );
1361 }
1362
1363 #[test]
1364 fn extract_non_docstring_string_at_start_is_not_a_comment() {
1365 let src = "x = \"hello\"\n";
1366 let g = extract_default("m.py", src, &make_anchor(), false);
1367 assert_eq!(
1368 g.defs().filter(|d| d.kind == b"comment").count(),
1369 0,
1370 "string literals that aren't bare expression-statement-strings must NOT be treated as docstrings"
1371 );
1372 }
1373}