1use crate::graph::PythonGraphSource;
6use crate::graph::hits::{
7 record_hit, record_import_hit, record_self_receiver_hit, record_unproven_hit,
8};
9use crate::graph::resolver::{
10 annotation_class_qualifier_site, annotation_reference_candidates, member_name,
11 normalized_receiver_type, receiver_annotation_matches_target,
12 resolve_callable_parameter_default_types, resolve_constructor_types, resolve_receiver_type,
13 target_owner_code_unit, top_level_identifier,
14};
15use crate::graph_support::{PythonSource, PythonUsageSource};
16use crate::imports::{PythonImportBinding, parse_python_import_bindings, resolve_fqn_candidates};
17use crate::usage_index::{
18 ModuleBindingEvent, ModuleBindingEventKind, ModuleBindingTimeline, PythonScopeFacts,
19 usage_matching_edges, usage_module_binding_timeline, usage_resolve_module_files,
20 usage_scope_facts,
21};
22use brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path;
23use brokk_bifrost_core::analyzer::usages::local_inference::{
24 LocalBindingsSnapshot, LocalInferenceConfig, LocalInferenceEngine, SymbolResolution,
25};
26use brokk_bifrost_core::analyzer::usages::model::{ImportKind, UsageHit};
27use brokk_bifrost_core::analyzer::usages::{ImportEdge, ImportEdgeKind};
28use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, Language, ProjectFile, Range};
29use brokk_bifrost_core::cancellation::CancellationToken;
30use brokk_bifrost_core::hash::{HashMap, HashSet};
31use brokk_bifrost_core::text_utils::compute_line_starts;
32use rayon::prelude::*;
33use std::collections::BTreeSet;
34use std::sync::{Arc, Mutex};
35use tree_sitter::{Node, Parser, Tree};
36
37pub struct ParsedFile {
38 pub source: Arc<String>,
39 pub tree: Tree,
40}
41
42pub struct PythonProjectGraph {
43 parsed: HashMap<ProjectFile, ParsedFile>,
44}
45
46impl PythonProjectGraph {
47 pub fn scan_files(
48 &self,
49 candidate_files: &HashSet<ProjectFile>,
50 target_file: &ProjectFile,
51 ) -> HashSet<ProjectFile> {
52 candidate_files
53 .iter()
54 .cloned()
55 .chain(std::iter::once(target_file.clone()))
56 .collect()
57 }
58}
59
60pub fn build_python_graph(
61 candidate_files: &HashSet<ProjectFile>,
62 target_file: &ProjectFile,
63 cancellation: Option<&CancellationToken>,
64) -> PythonProjectGraph {
65 let parser_language = tree_sitter_python::LANGUAGE.into();
66 let files: HashSet<ProjectFile> = candidate_files
67 .iter()
68 .cloned()
69 .chain(std::iter::once(target_file.clone()))
70 .collect();
71 let mut parsed = HashMap::default();
72
73 for file in files {
74 if cancellation.is_some_and(CancellationToken::is_cancelled) {
75 break;
76 }
77 let Ok(source) = file.read_to_string() else {
78 continue;
79 };
80 if cancellation.is_some_and(CancellationToken::is_cancelled) {
81 break;
82 }
83 if source.is_empty() {
84 continue;
85 }
86 let mut parser = Parser::new();
87 if parser.set_language(&parser_language).is_err() {
88 continue;
89 }
90 let Some(tree) = parser.parse(source.as_str(), None) else {
91 continue;
92 };
93 if cancellation.is_some_and(CancellationToken::is_cancelled) {
94 break;
95 }
96 parsed.insert(
97 file,
98 ParsedFile {
99 source: Arc::new(source),
100 tree,
101 },
102 );
103 }
104
105 PythonProjectGraph { parsed }
106}
107
108pub fn scan_files_for_seeds(
109 graph: &PythonGraphSource<'_>,
110 python: &dyn PythonUsageSource,
111 project_graph: &PythonProjectGraph,
112 files: &HashSet<ProjectFile>,
113 target: &CodeUnit,
114 seeds: &BTreeSet<(ProjectFile, String)>,
115 cancellation: Option<&CancellationToken>,
116) -> ScanResult {
117 let collected: Mutex<BTreeSet<UsageHit>> = Mutex::new(BTreeSet::new());
118 let unproven_collected: Mutex<BTreeSet<UsageHit>> = Mutex::new(BTreeSet::new());
119 let target_short = top_level_identifier(graph.index, target);
120 let target_member = member_name(graph.index, target);
121 let target_owner = target_owner_code_unit(graph.index, target);
122 let member_unique_in_target_file = target_member.as_deref().is_some_and(|member| {
126 let owners: HashSet<CodeUnit> = graph
127 .index
128 .declarations(target.source())
129 .into_iter()
130 .filter(|decl| {
131 decl.identifier() == member && target_owner_code_unit(graph.index, decl).is_some()
132 })
133 .filter_map(|decl| target_owner_code_unit(graph.index, &decl))
134 .collect();
135 owners.len() == 1
136 });
137 let files_vec: Vec<&ProjectFile> = files.iter().collect();
138 let parser_language = tree_sitter_python::LANGUAGE.into();
139
140 files_vec.par_iter().for_each(|file| {
141 if cancellation.is_some_and(CancellationToken::is_cancelled) {
142 return;
143 }
144 let owned_source: Option<Arc<String>>;
145 let owned_tree: Option<Tree>;
146 let (source_str, tree_ref) = if let Some(parsed) = project_graph.parsed.get(*file) {
147 (parsed.source.as_str(), &parsed.tree)
148 } else {
149 let Ok(source) = file.read_to_string() else {
150 return;
151 };
152 if source.is_empty() {
153 return;
154 }
155 let mut parser = Parser::new();
156 if parser.set_language(&parser_language).is_err() {
157 return;
158 }
159 let Some(tree) = parser.parse(source.as_str(), None) else {
160 return;
161 };
162 owned_source = Some(Arc::new(source));
163 owned_tree = Some(tree);
164 (
165 owned_source.as_deref().unwrap().as_str(),
166 owned_tree.as_ref().unwrap(),
167 )
168 };
169 if cancellation.is_some_and(CancellationToken::is_cancelled) {
170 return;
171 }
172
173 let edges = {
174 let _scope = brokk_bifrost_core::profiling::scope("python_graph::matching_edges");
175 usage_matching_edges(python, file, seeds)
176 };
177 if !file_may_reference_target(
182 tree_ref.root_node(),
183 source_str,
184 target,
185 target_short.as_str(),
186 target_member.as_deref(),
187 &edges,
188 ) {
189 return;
190 }
191 let raw_module_bindings = {
192 let _scope =
193 brokk_bifrost_core::profiling::scope("python_graph::module_binding_timeline");
194 usage_module_binding_timeline(python, file, || {
195 collect_module_binding_timeline(tree_ref.root_node(), source_str)
196 })
197 };
198 let module_bindings = classify_module_binding_timeline(
199 python,
200 file,
201 raw_module_bindings.as_ref(),
202 seeds,
203 &edges,
204 );
205 let scoped_import_bindings = parse_python_import_bindings(source_str);
206 let target_self_file = *file == target.source();
207 let scope_facts = {
208 let _scope = brokk_bifrost_core::profiling::scope("python_graph::scope_facts");
209 usage_scope_facts(python, file, || {
210 collect_scope_facts_from_parsed_source(
211 graph,
212 python,
213 file,
214 source_str,
215 tree_ref.root_node(),
216 )
217 })
218 };
219 let scope_range_index = build_scope_range_index(graph, scope_facts.as_ref());
220
221 let mut local_hits = BTreeSet::new();
222 let mut local_unproven_hits = BTreeSet::new();
223 let line_starts = compute_line_starts(source_str);
224
225 let mut scan_ctx = ScanCtx {
226 python,
227 file,
228 source: source_str,
229 line_starts: &line_starts,
230 graph,
231 target,
232 target_short: &target_short,
233 target_member: target_member.as_deref(),
234 target_owner: target_owner.clone(),
235 target_is_module: target.is_module(),
236 target_source: target.source(),
237 seeds,
238 edges: &edges,
239 target_self_file,
240 member_best_effort_unique: target_self_file && member_unique_in_target_file,
241 raw_module_bindings: raw_module_bindings.as_ref(),
242 module_bindings: &module_bindings,
243 scoped_import_bindings: &scoped_import_bindings,
244 scope_facts: scope_facts.as_ref(),
245 scope_range_index: &scope_range_index,
246 hits: &mut local_hits,
247 unproven_hits: &mut local_unproven_hits,
248 };
249
250 {
251 let _scope = brokk_bifrost_core::profiling::scope("python_graph::scan_tree");
252 scan_node(tree_ref.root_node(), &mut scan_ctx);
253 }
254
255 if !local_hits.is_empty() {
256 let mut sink = collected
257 .lock()
258 .expect("usage hit collector mutex poisoned");
259 sink.extend(local_hits);
260 }
261 if !local_unproven_hits.is_empty() {
262 let mut sink = unproven_collected
263 .lock()
264 .expect("usage unproven hit collector mutex poisoned");
265 sink.extend(local_unproven_hits);
266 }
267 });
268
269 ScanResult {
270 hits: collected
271 .into_inner()
272 .expect("usage hit collector mutex poisoned"),
273 unproven_hits: unproven_collected
274 .into_inner()
275 .expect("usage unproven hit collector mutex poisoned"),
276 }
277}
278
279fn file_may_reference_target(
280 root: Node<'_>,
281 source: &str,
282 target: &CodeUnit,
283 target_short: &str,
284 target_member: Option<&str>,
285 edges: &[ImportEdge],
286) -> bool {
287 if target.is_module() {
288 return true;
289 }
290
291 let mut stack = vec![root];
292 while let Some(node) = stack.pop() {
293 if node.kind() == "string" {
294 return true;
298 }
299 if node.kind() == "identifier" {
300 let name = slice(node, source);
301 if name == target_short
302 || target_member.is_some_and(|member| name == member)
303 || edges.iter().any(|edge| edge.local_name == name)
304 {
305 return true;
306 }
307 }
308
309 let mut cursor = node.walk();
310 stack.extend(node.named_children(&mut cursor));
311 }
312 false
313}
314
315pub struct ScanResult {
316 pub hits: BTreeSet<UsageHit>,
317 pub unproven_hits: BTreeSet<UsageHit>,
318}
319
320pub struct ScanCtx<'a> {
321 python: &'a dyn PythonUsageSource,
322 pub file: &'a ProjectFile,
323 pub source: &'a str,
324 pub line_starts: &'a [usize],
325 pub graph: &'a PythonGraphSource<'a>,
326 target: &'a CodeUnit,
327 target_short: &'a str,
328 target_member: Option<&'a str>,
329 target_owner: Option<CodeUnit>,
330 target_is_module: bool,
331 target_source: &'a ProjectFile,
332 seeds: &'a BTreeSet<(ProjectFile, String)>,
333 edges: &'a [ImportEdge],
334 target_self_file: bool,
335 member_best_effort_unique: bool,
341 raw_module_bindings: &'a ModuleBindingTimeline,
342 module_bindings: &'a HashMap<String, Vec<ClassifiedModuleBindingEvent>>,
343 scoped_import_bindings: &'a [PythonImportBinding],
344 scope_facts: &'a HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
345 scope_range_index: &'a [ScopeRangeEntry],
346 pub hits: &'a mut BTreeSet<UsageHit>,
347 pub unproven_hits: &'a mut BTreeSet<UsageHit>,
348}
349
350struct ScopeRangeEntry {
351 range: Range,
352 scope: CodeUnit,
353 prefix_max_end: usize,
354}
355
356fn build_scope_range_index(
357 graph: &PythonGraphSource<'_>,
358 scope_facts: &HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
359) -> Vec<ScopeRangeEntry> {
360 let mut entries = scope_facts
361 .keys()
362 .flat_map(|scope| {
363 graph
364 .index
365 .ranges(scope)
366 .into_iter()
367 .map(|range| ScopeRangeEntry {
368 range,
369 scope: scope.clone(),
370 prefix_max_end: 0,
371 })
372 })
373 .collect::<Vec<_>>();
374 entries.sort_by(|left, right| {
375 left.range
376 .start_byte
377 .cmp(&right.range.start_byte)
378 .then_with(|| right.range.end_byte.cmp(&left.range.end_byte))
379 .then_with(|| left.scope.cmp(&right.scope))
380 });
381 let mut max_end = 0;
382 for entry in &mut entries {
383 max_end = max_end.max(entry.range.end_byte);
384 entry.prefix_max_end = max_end;
385 }
386 entries
387}
388
389fn indexed_scope_entry<'entry, 'facts>(
390 scope_range_index: &'entry [ScopeRangeEntry],
391 scope_facts: &'facts HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
392 node: Node<'_>,
393 mut skip_innermost: usize,
394) -> Option<(&'entry CodeUnit, &'facts LocalBindingsSnapshot<String>)> {
395 let mut cursor =
396 scope_range_index.partition_point(|entry| entry.range.start_byte <= node.start_byte());
397 while cursor > 0 {
398 cursor -= 1;
399 let entry = &scope_range_index[cursor];
400 if entry.prefix_max_end < node.end_byte() {
401 return None;
402 }
403 if entry.range.end_byte >= node.end_byte() {
404 if skip_innermost > 0 {
405 skip_innermost -= 1;
406 continue;
407 }
408 return scope_facts
409 .get(&entry.scope)
410 .map(|facts| (&entry.scope, facts));
411 }
412 }
413 None
414}
415
416pub fn enclosing_scope_facts<'a>(
420 index: &dyn CodeUnitIndex,
421 file: &ProjectFile,
422 scope_facts: &'a HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
423 node: Node<'_>,
424) -> Option<&'a LocalBindingsSnapshot<String>> {
425 let range = Range {
426 start_byte: node.start_byte(),
427 end_byte: node.end_byte(),
428 start_line: 0,
429 end_line: 0,
430 };
431 let enclosing = index.enclosing_code_unit(file, &range)?;
432 scope_facts.get(&enclosing)
433}
434
435impl ScanCtx<'_> {
436 fn scope_entry_for_node(
437 &self,
438 node: Node<'_>,
439 ) -> Option<(&CodeUnit, &LocalBindingsSnapshot<String>)> {
440 indexed_scope_entry(
441 self.scope_range_index,
442 self.scope_facts,
443 node,
444 usize::from(function_declaration_expression_is_outer_scoped(node)),
445 )
446 }
447
448 fn scope_facts_for_node(&self, node: Node<'_>) -> Option<&LocalBindingsSnapshot<String>> {
449 self.scope_entry_for_node(node).map(|(_, facts)| facts)
450 }
451
452 fn binds_target(&self, ident: &str, node: Node<'_>) -> bool {
453 let scope_entry = self.scope_entry_for_node(node);
454 if self.target_self_file
455 && ident == self.target_short
456 && scope_entry
457 .is_none_or(|(scope, facts)| scope.is_module() || !facts.is_shadowed(ident))
458 {
459 return true;
460 }
461 if scope_entry.is_some_and(|(scope, facts)| !scope.is_module() && facts.is_shadowed(ident))
462 {
463 return false;
464 }
465 self.module_binding_targets_query(ident, node)
466 }
467
468 fn receiver_binds_target(&self, expr: &str, node: Node<'_>) -> bool {
469 if self.binds_target(expr, node) {
470 return true;
471 }
472
473 if self.target_member.is_some() && self.import_edge_visible_for(expr, node) {
474 return true;
475 }
476
477 if matches!(expr, "self" | "cls") && self.self_receiver_matches_target(node) {
481 return true;
482 }
483
484 match enclosing_runtime_parameter_type(expr, node, self.source) {
485 EnclosingParameterType::Typed(raw_type) => {
486 return self.receiver_type_matches_target(&raw_type);
487 }
488 EnclosingParameterType::Untyped => return false,
489 EnclosingParameterType::NotDeclared => {}
490 }
491
492 let Some(scope_facts) = self.scope_facts_for_node(node) else {
493 return false;
494 };
495 let resolution = scope_facts.resolution_for(expr);
496 let Some(raw_type) = resolution
497 .as_precise()
498 .and_then(|targets| targets.iter().next())
499 else {
500 return false;
501 };
502 self.receiver_type_matches_target(raw_type)
503 }
504
505 fn node_directly_in_owner_class_body(&self, node: Node<'_>) -> bool {
511 let Some(target_owner) = self.target_owner.as_ref() else {
512 return false;
513 };
514 let range = Range {
515 start_byte: node.start_byte(),
516 end_byte: node.end_byte(),
517 start_line: 0,
518 end_line: 0,
519 };
520 let Some(enclosing) = self.graph.index.enclosing_code_unit(self.file, &range) else {
521 return false;
522 };
523 if &enclosing == target_owner {
524 return true;
525 }
526 if enclosing.is_function() {
527 return target_owner_code_unit(self.graph.index, &enclosing).as_ref()
528 == Some(target_owner)
529 && function_declaration_expression_is_outer_scoped(node);
530 }
531 target_owner_code_unit(self.graph.index, &enclosing).as_ref() == Some(target_owner)
532 }
533
534 fn receiver_type_is_unknown(&self, expr: &str, node: Node<'_>) -> bool {
538 match enclosing_runtime_parameter_type(expr, node, self.source) {
539 EnclosingParameterType::Typed(_) => return false,
540 EnclosingParameterType::Untyped => return true,
541 EnclosingParameterType::NotDeclared => {}
542 }
543 match self.scope_facts_for_node(node) {
544 Some(facts) => facts.resolution_for(expr).is_unknown(),
545 None => true,
546 }
547 }
548
549 fn import_edge_visible_for(&self, ident: &str, node: Node<'_>) -> bool {
550 if let Some(scope_facts) = self.scope_facts_for_node(node)
551 && scope_facts.is_shadowed(ident)
552 {
553 return false;
554 }
555 self.module_binding_targets_query(ident, node)
556 }
557
558 fn module_binding_targets_query(&self, ident: &str, node: Node<'_>) -> bool {
559 if self.target_member.is_some() {
565 return self.module_binding_targets_symbol(ident, node);
566 }
567 if let Some(matches) = self.function_import_binding_targets_query(ident, node) {
568 return matches;
569 }
570 self.module_binding_matches_query(ident, node, true, |kind| {
571 kind != ModuleBindingKind::Other
572 })
573 }
574
575 fn module_binding_targets_symbol(&self, ident: &str, node: Node<'_>) -> bool {
576 if let Some(matches) = self.function_import_binding_targets_query(ident, node) {
577 return matches;
578 }
579 let unclassified_named_import = self.edges.iter().any(|edge| {
580 edge.local_name == ident && !matches!(edge.kind, ImportEdgeKind::Namespace)
581 });
582 self.module_binding_matches_query(ident, node, unclassified_named_import, |kind| {
583 kind == ModuleBindingKind::TargetSymbolImport
584 })
585 }
586
587 fn function_import_binding_targets_query(&self, ident: &str, node: Node<'_>) -> Option<bool> {
592 let binding = self.scoped_import_bindings.iter().rev().find(|binding| {
593 binding.is_function_scoped()
594 && binding.start_byte <= node.start_byte()
595 && binding.scope_start_byte <= node.start_byte()
596 && node.end_byte() <= binding.scope_end_byte
597 && binding.local_name == ident
598 })?;
599 let candidates = resolve_fqn_candidates(self.python, &binding.qualified_name, |name| {
600 self.graph.index.definitions(name).collect()
601 });
602 let imported_target = self.target_owner.as_ref().unwrap_or(self.target);
603 Some(
604 candidates
605 .iter()
606 .any(|candidate| candidate == imported_target),
607 )
608 }
609
610 fn module_binding_matches_query(
611 &self,
612 ident: &str,
613 node: Node<'_>,
614 unclassified: bool,
615 matches: impl Fn(ModuleBindingKind) -> bool,
616 ) -> bool {
617 if !self.edges.iter().any(|edge| edge.local_name == ident) {
618 return false;
619 }
620 let Some(events) = self.module_bindings.get(ident) else {
621 return unclassified;
622 };
623 let cutoff = if reference_is_deferred_function_body(node) {
624 usize::MAX
625 } else {
626 node.start_byte()
627 };
628 let visible: Vec<_> = events
629 .iter()
630 .filter(|event| event.visible_from <= cutoff)
631 .collect();
632 let start = visible
633 .iter()
634 .rposition(|event| !event.conditional)
635 .unwrap_or(0);
636 visible[start..].iter().any(|event| matches(event.kind))
637 }
638
639 fn self_receiver_matches_target(&self, node: Node<'_>) -> bool {
643 let Some(target_owner) = self.target_owner.as_ref() else {
644 return false;
645 };
646 let range = Range {
647 start_byte: node.start_byte(),
648 end_byte: node.end_byte(),
649 start_line: 0,
650 end_line: 0,
651 };
652 let Some(enclosing) = self.graph.index.enclosing_code_unit(self.file, &range) else {
653 return false;
654 };
655 let enclosing_class = if enclosing.is_class() {
656 enclosing
657 } else {
658 match target_owner_code_unit(self.graph.index, &enclosing) {
659 Some(class) => class,
660 None => return false,
661 }
662 };
663 if &enclosing_class == target_owner {
664 return true;
665 }
666 self.graph
667 .hierarchy
668 .map(|provider| provider.get_ancestors(&enclosing_class))
669 .unwrap_or_default()
670 .into_iter()
671 .any(|ancestor| ancestor == *target_owner)
672 }
673
674 fn receiver_type_matches_target(&self, raw_type: &str) -> bool {
675 let Some(target_owner) = self.target_owner.as_ref() else {
676 return false;
677 };
678 if let Some(receiver_type) = resolve_receiver_type(
679 self.graph,
680 self.python,
681 self.file,
682 raw_type,
683 self.target_self_file,
684 ) {
685 if &receiver_type == target_owner {
686 return true;
687 }
688 return self
689 .graph
690 .hierarchy
691 .map(|provider| provider.get_ancestors(&receiver_type))
692 .unwrap_or_default()
693 .into_iter()
694 .any(|ancestor| ancestor == *target_owner);
695 }
696
697 receiver_annotation_matches_target(
702 raw_type,
703 self.edges,
704 self.target_short,
705 self.target_self_file,
706 )
707 }
708}
709
710pub(crate) fn function_declaration_expression_is_outer_scoped(node: Node<'_>) -> bool {
711 let site_start = node.start_byte();
712 let site_end = node.end_byte();
713 let mut current = node;
714 while let Some(parent) = current.parent() {
715 if parent.kind() == "function_definition" {
716 if parent
717 .child_by_field_name("body")
718 .is_some_and(|body| body.start_byte() <= site_start && site_end <= body.end_byte())
719 {
720 return false;
721 }
722 if parent
723 .child_by_field_name("name")
724 .is_some_and(|name| name.id() == node.id())
725 {
726 return false;
727 }
728 if let Some(parameters) = parent.child_by_field_name("parameters")
729 && parameters.start_byte() <= site_start
730 && site_end <= parameters.end_byte()
731 {
732 let mut parameter = node;
733 while parameter.parent() != Some(parameters) {
734 let Some(next) = parameter.parent() else {
735 return false;
736 };
737 parameter = next;
738 }
739 let binder = if parameter.kind() == "identifier" {
740 Some(parameter)
741 } else {
742 parameter.child_by_field_name("name").or_else(|| {
743 parameter
744 .named_child(0)
745 .filter(|child| child.kind() == "identifier")
746 })
747 };
748 return binder.is_none_or(|binder| binder.id() != node.id());
749 }
750 return true;
751 }
752 if parent.kind() == "decorated_definition" {
753 return current.kind() == "decorator";
754 }
755 if parent.kind() == "class_definition" {
756 break;
757 }
758 current = parent;
759 }
760 false
761}
762
763fn scan_node(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
764 let mut stack = vec![node];
765 while let Some(node) = stack.pop() {
766 match node.kind() {
767 "import_statement" | "import_from_statement" => {
768 handle_import_candidate(node, ctx);
769 continue;
770 }
771 "identifier" => {
772 if handle_annotation_reference_candidate(node, ctx) {
773 continue;
774 }
775 handle_identifier_candidate(node, ctx);
776 }
777 "attribute" => {
778 if handle_annotation_reference_candidate(node, ctx) {
779 continue;
780 }
781 handle_attribute_candidate(node, ctx);
782 }
783 "string_content" => {
784 handle_annotation_reference_candidate(node, ctx);
785 }
786 "keyword_argument" => {
787 handle_keyword_argument_candidate(node, ctx);
788 if let Some(value) = node.child_by_field_name("value") {
789 stack.push(value);
790 }
791 continue;
792 }
793 _ => {}
794 }
795
796 let mut cursor = node.walk();
797 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
798 children.reverse();
799 stack.extend(children);
800 }
801}
802
803fn handle_annotation_reference_candidate(node: Node<'_>, ctx: &mut ScanCtx<'_>) -> bool {
804 let Some(candidates) = annotation_reference_candidates(
805 ctx.graph,
806 ctx.python,
807 ctx.file,
808 ctx.source,
809 node,
810 ctx.target_self_file,
811 ) else {
812 return false;
813 };
814
815 if node.kind() == "attribute"
820 && ctx.target.is_field()
821 && let Some(root) = leftmost_identifier(node)
822 && ctx.target_member == Some(slice(root, ctx.source))
823 && ctx.node_directly_in_owner_class_body(root)
824 {
825 record_hit(root, ctx);
826 }
827
828 if (ctx.target.is_class() || ctx.target.is_field() || ctx.target_member.is_none())
829 && candidates.iter().all(|candidate| *candidate == *ctx.target)
830 && candidates.iter().any(|candidate| *candidate == *ctx.target)
831 {
832 let site = if node.kind() == "attribute" {
833 node.child_by_field_name("attribute").unwrap_or(node)
834 } else {
835 node
836 };
837 record_hit(site, ctx);
838 }
839
840 if let Some(site) = annotation_class_qualifier_site(
841 ctx.graph, ctx.python, ctx.file, ctx.source, node, ctx.target,
842 ) {
843 record_hit(site, ctx);
844 }
845
846 if node.kind() == "attribute" && (candidates.is_empty() || ctx.target_is_module) {
852 return false;
853 }
854
855 true
856}
857
858fn handle_keyword_argument_candidate(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
859 let (Some(target_member), Some(name), Some(arguments)) = (
860 ctx.target_member,
861 node.child_by_field_name("name"),
862 node.parent(),
863 ) else {
864 return;
865 };
866 if name.kind() != "identifier"
867 || slice(name, ctx.source) != target_member
868 || arguments.kind() != "argument_list"
869 {
870 return;
871 }
872 let Some(call) = arguments.parent().filter(|parent| parent.kind() == "call") else {
873 return;
874 };
875 let Some(function) = call.child_by_field_name("function") else {
876 return;
877 };
878 if function.kind() == "identifier" && slice(function, ctx.source) == "cls" {
879 if ctx.self_receiver_matches_target(function) {
880 record_hit(name, ctx);
881 }
882 return;
883 }
884 let Some(target_owner) = ctx.target_owner.as_ref() else {
885 return;
886 };
887 let scoped_callee_matches = if function.kind() == "identifier" {
888 ctx.scope_facts_for_node(function)
889 .and_then(|facts| {
890 facts
891 .resolution_for(slice(function, ctx.source))
892 .as_precise()
893 .and_then(|targets| targets.iter().next().cloned())
894 })
895 .is_some_and(|raw_type| ctx.receiver_type_matches_target(&raw_type))
896 } else {
897 false
898 };
899 let default_callee_matches = if function.kind() == "identifier" {
900 resolve_callable_parameter_default_types(
901 ctx.graph,
902 ctx.python,
903 ctx.file,
904 ctx.source,
905 function,
906 slice(function, ctx.source),
907 )
908 .into_iter()
909 .any(|class| {
910 &class == target_owner
911 || ctx
912 .graph
913 .hierarchy
914 .map(|provider| provider.get_ancestors(&class))
915 .unwrap_or_default()
916 .into_iter()
917 .any(|ancestor| &ancestor == target_owner)
918 })
919 } else {
920 false
921 };
922 let root_shadowed = leftmost_identifier(function).is_some_and(|root| {
923 ctx.scope_facts_for_node(function)
924 .is_some_and(|facts| facts.is_shadowed(slice(root, ctx.source)))
925 });
926 if root_shadowed && !scoped_callee_matches && !default_callee_matches {
927 return;
928 }
929 let matches = scoped_callee_matches
930 || default_callee_matches
931 || (!root_shadowed
932 && resolve_constructor_types(ctx.graph, ctx.python, ctx.file, ctx.source, function)
933 .into_iter()
934 .any(|class| {
935 &class == target_owner
936 || ctx
937 .graph
938 .hierarchy
939 .map(|provider| provider.get_ancestors(&class))
940 .unwrap_or_default()
941 .into_iter()
942 .any(|ancestor| &ancestor == target_owner)
943 }));
944 if matches {
945 record_hit(name, ctx);
946 }
947}
948
949fn leftmost_identifier(mut node: Node<'_>) -> Option<Node<'_>> {
950 loop {
951 match node.kind() {
952 "identifier" => return Some(node),
953 "attribute" => node = node.child_by_field_name("object")?,
954 _ => return None,
955 }
956 }
957}
958
959fn handle_import_candidate(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
965 if ctx.target_member.is_some() {
966 return;
967 }
968 if !ctx
969 .edges
970 .iter()
971 .any(|edge| edge.local_name == ctx.target_short)
972 {
973 return;
974 }
975 let mut stack = vec![node];
976 while let Some(node) = stack.pop() {
977 if node.kind() == "identifier" && slice(node, ctx.source) == ctx.target_short {
978 record_import_hit(node, ctx);
979 return;
980 }
981 let mut cursor = node.walk();
982 for child in node.named_children(&mut cursor) {
983 stack.push(child);
984 }
985 }
986}
987
988fn handle_identifier_candidate(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
989 if node
990 .parent()
991 .is_some_and(|parent| parent.kind() == "attribute")
992 {
993 return;
994 }
995 let text = slice(node, ctx.source);
996 if text.is_empty() || is_declaration_identifier(node) || decorates_the_target(node, ctx) {
997 return;
998 }
999 if let Some(member) = ctx.target_member {
1000 if member == "__init__" && is_call_callee(node) && ctx.binds_target(text, node) {
1003 record_hit(node, ctx);
1004 return;
1005 }
1006 if text == member && ctx.node_directly_in_owner_class_body(node) {
1009 record_hit(node, ctx);
1010 }
1011 return;
1012 }
1013 if !ctx.binds_target(text, node) {
1014 return;
1015 }
1016 if !ctx.target_is_module
1017 && ctx.edges.iter().any(|edge| edge.local_name == text)
1018 && !ctx.module_binding_targets_symbol(text, node)
1019 {
1020 return;
1021 }
1022 record_hit(node, ctx);
1023}
1024
1025fn decorates_the_target(node: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
1033 if ctx.file != ctx.target_source {
1034 return false;
1035 }
1036 let mut current = node;
1037 while let Some(parent) = current.parent() {
1038 if parent.kind() == "decorated_definition" && current.kind() == "decorator" {
1039 let Some(definition) = parent.child_by_field_name("definition") else {
1040 return false;
1041 };
1042 return ctx.graph.index.ranges(ctx.target).iter().any(|range| {
1043 range.start_byte <= definition.start_byte()
1044 && definition.end_byte() <= range.end_byte
1045 });
1046 }
1047 current = parent;
1048 }
1049 false
1050}
1051
1052fn handle_attribute_candidate(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
1053 let Some(object) = node.child_by_field_name("object") else {
1054 return;
1055 };
1056 let Some(attribute) = node.child_by_field_name("attribute") else {
1057 return;
1058 };
1059 let object_text = slice(object, ctx.source);
1060 let attribute_text = slice(attribute, ctx.source);
1061 if let Some(member) = ctx.target_member
1062 && attribute_text == member
1063 {
1064 let is_same_owner_receiver =
1069 matches!(object_text, "self" | "cls") && ctx.self_receiver_matches_target(node);
1070 if is_same_owner_receiver {
1071 record_self_receiver_hit(attribute, ctx);
1072 } else if ctx.receiver_binds_target(object_text, node)
1073 || (object.kind() == "call" && call_result_matches_target(object, ctx))
1074 {
1075 record_hit(attribute, ctx);
1076 } else if member_receiver_match_is_unproven(object, object_text, node, ctx) {
1077 record_unproven_hit(attribute, ctx);
1078 }
1079 }
1080
1081 let object_binds_target = if ctx.target_is_module {
1082 imported_root_targets_module(ctx, object, node)
1083 } else {
1084 ctx.binds_target(object_text, node)
1085 };
1086 if object.kind() == "identifier"
1087 && object_binds_target
1088 && (ctx.target_is_module
1089 || (ctx.target_member.is_none()
1090 && !ctx.edges.iter().any(|edge| {
1091 matches!(edge.kind, ImportEdgeKind::Namespace) && edge.local_name == object_text
1092 })))
1093 {
1094 record_hit(object, ctx);
1095 }
1096
1097 if ctx.target_is_module
1098 && let Some(module_qualifier) = module_attribute_target_hit(node, ctx)
1099 {
1100 record_hit(module_qualifier, ctx);
1101 }
1102
1103 if let Some(member) = ctx.target_member
1107 && object.kind() == "identifier"
1108 && object_text == member
1109 && ctx.node_directly_in_owner_class_body(object)
1110 {
1111 record_hit(object, ctx);
1112 }
1113
1114 if ctx.member_best_effort_unique
1120 && let Some(member) = ctx.target_member
1121 && attribute_text == member
1122 && object.kind() == "identifier"
1123 && !matches!(object_text, "self" | "cls")
1124 && !ctx.receiver_binds_target(object_text, node)
1125 && ctx.receiver_type_is_unknown(object_text, node)
1126 {
1127 record_hit(attribute, ctx);
1128 }
1129
1130 if let Some(module_binding_target) = module_binding_attribute_target_hit(node, ctx) {
1131 record_hit(module_binding_target, ctx);
1132 }
1133}
1134
1135fn module_binding_attribute_target_hit<'a>(node: Node<'a>, ctx: &ScanCtx<'_>) -> Option<Node<'a>> {
1147 if ctx.target_member.is_some() {
1148 return None;
1149 }
1150 let (root, attributes) = attribute_chain(node)?;
1151 let terminal = *attributes.last()?;
1152 let terminal_name = slice(terminal, ctx.source);
1153 if terminal_name.is_empty()
1154 || !ctx
1155 .seeds
1156 .iter()
1157 .any(|(_, seed_name)| seed_name == terminal_name)
1158 {
1159 return None;
1160 }
1161
1162 for binding in imported_module_bindings(ctx, root, node) {
1163 let mut written_module = binding.module.clone();
1164 for attribute in attributes
1165 [binding.consumed_attributes.min(attributes.len() - 1)..attributes.len() - 1]
1166 .iter()
1167 {
1168 let segment = slice(*attribute, ctx.source);
1169 if segment.is_empty() {
1170 return None;
1171 }
1172 written_module.push('.');
1173 written_module.push_str(segment);
1174 }
1175 if usage_resolve_module_files(ctx.python, ctx.file, &written_module)
1176 .iter()
1177 .any(|resolved| {
1178 ctx.seeds
1179 .contains(&(resolved.clone(), terminal_name.to_string()))
1180 })
1181 {
1182 return Some(terminal);
1183 }
1184
1185 let mut written_fqn = binding.module;
1186 for attribute in attributes.iter().skip(binding.consumed_attributes) {
1187 let segment = slice(*attribute, ctx.source);
1188 if segment.is_empty() {
1189 return None;
1190 }
1191 written_fqn.push('.');
1192 written_fqn.push_str(segment);
1193 }
1194 if resolve_fqn_candidates(ctx.python, &written_fqn, |name| {
1195 ctx.graph.index.definitions(name).collect()
1196 })
1197 .into_iter()
1198 .any(|candidate| &candidate == ctx.target)
1199 {
1200 return Some(terminal);
1201 }
1202 }
1203 None
1204}
1205
1206fn imported_root_targets_module(ctx: &ScanCtx<'_>, root: Node<'_>, reference: Node<'_>) -> bool {
1207 imported_module_bindings(ctx, root, reference)
1208 .into_iter()
1209 .any(|binding| {
1210 usage_resolve_module_files(ctx.python, ctx.file, &binding.module)
1211 .into_iter()
1212 .any(|resolved_file| &resolved_file == ctx.target_source)
1213 })
1214}
1215
1216fn module_attribute_target_hit<'a>(node: Node<'a>, ctx: &ScanCtx<'_>) -> Option<Node<'a>> {
1217 let (root, attributes) = attribute_chain(node)?;
1218 if attributes.is_empty() {
1219 return None;
1220 }
1221 for binding in imported_module_bindings(ctx, root, node) {
1222 let mut module_fqn = binding.module;
1223 for attribute in attributes.iter().skip(binding.consumed_attributes) {
1224 let segment = slice(*attribute, ctx.source);
1225 if segment.is_empty() {
1226 return None;
1227 }
1228 if module_fqn.ends_with('.') {
1229 module_fqn.push_str(segment);
1230 } else {
1231 module_fqn.push('.');
1232 module_fqn.push_str(segment);
1233 }
1234 let resolved = usage_resolve_module_files(ctx.python, ctx.file, &module_fqn);
1235 if resolved.is_empty() {
1236 break;
1237 }
1238 if resolved
1239 .iter()
1240 .any(|resolved_file| resolved_file == ctx.target_source)
1241 {
1242 return Some(*attribute);
1243 }
1244 }
1245 }
1246 None
1247}
1248
1249fn call_result_matches_target(call: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
1250 let Some(target_owner) = ctx.target_owner.as_ref() else {
1251 return false;
1252 };
1253 let scope_facts = ctx.scope_facts_for_node(call);
1254 call_result_types(
1255 ctx.graph,
1256 ctx.python,
1257 ctx.file,
1258 ctx.source,
1259 call,
1260 scope_facts,
1261 )
1262 .into_iter()
1263 .any(|class| {
1264 &class == target_owner
1265 || ctx
1266 .graph
1267 .hierarchy
1268 .map(|provider| provider.get_ancestors(&class))
1269 .unwrap_or_default()
1270 .into_iter()
1271 .any(|ancestor| &ancestor == target_owner)
1272 })
1273}
1274
1275pub fn call_result_types(
1276 graph: &PythonGraphSource<'_>,
1277 python: &dyn PythonUsageSource,
1278 file: &ProjectFile,
1279 source: &str,
1280 call: Node<'_>,
1281 scope_facts: Option<&LocalBindingsSnapshot<String>>,
1282) -> Vec<CodeUnit> {
1283 let Some(function) = call.child_by_field_name("function") else {
1284 return Vec::new();
1285 };
1286 let constructed = resolve_constructor_types(graph, python, file, source, function);
1287 if !constructed.is_empty() {
1288 return constructed;
1289 }
1290 let callable_fqns = resolve_callable_fqns(graph, python, file, source, function, scope_facts);
1291 if callable_fqns.is_empty() {
1292 return Vec::new();
1293 }
1294 let callables = callable_fqns
1295 .into_iter()
1296 .flat_map(|callable_fqn| {
1297 resolve_fqn_candidates(python, &callable_fqn, |name| {
1298 graph.index.definitions(name).collect()
1299 })
1300 })
1301 .collect::<Vec<_>>();
1302 let mut classes = Vec::new();
1303 for callable in callables.into_iter().filter(CodeUnit::is_function) {
1304 let raw_type = callable_return_type_name(graph, python, &callable).or_else(|| {
1305 if callable.source() != file {
1306 return None;
1307 }
1308 let prepared = python.prepared_syntax(graph.token, file)?;
1309 let key = factory_function_key(graph, &callable);
1310 collect_factory_return_types_from_root(prepared.tree().root_node(), prepared.source())
1311 .remove(&key)
1312 });
1313 let Some(raw_type) = raw_type else {
1314 continue;
1315 };
1316 if let Some(class) =
1317 resolve_receiver_type(graph, python, callable.source(), &raw_type, true)
1318 {
1319 classes.push(class);
1320 }
1321 }
1322 classes.sort();
1323 classes.dedup();
1324 classes
1325}
1326
1327fn resolve_callable_fqns(
1328 graph: &PythonGraphSource<'_>,
1329 python: &dyn PythonUsageSource,
1330 file: &ProjectFile,
1331 source: &str,
1332 function: Node<'_>,
1333 scope_facts: Option<&LocalBindingsSnapshot<String>>,
1334) -> Vec<String> {
1335 match function.kind() {
1336 "identifier" => {
1337 resolve_identifier_callable_fqns(graph, python, file, source, function, scope_facts)
1338 }
1339 "attribute" => {
1340 resolve_attribute_callable_fqns(graph, python, file, source, function, scope_facts)
1341 }
1342 _ => Vec::new(),
1343 }
1344}
1345
1346fn resolve_identifier_callable_fqns(
1347 graph: &PythonGraphSource<'_>,
1348 python: &dyn PythonUsageSource,
1349 file: &ProjectFile,
1350 source: &str,
1351 function: Node<'_>,
1352 scope_facts: Option<&LocalBindingsSnapshot<String>>,
1353) -> Vec<String> {
1354 let local = slice(function, source);
1355 if local.is_empty() || scope_facts.is_some_and(|facts| facts.is_shadowed(local)) {
1356 return Vec::new();
1357 }
1358 let binder = python.import_binder_of(file);
1359 match binder.bindings.get(local) {
1360 Some(binding) if binding.kind == ImportKind::Named => binding
1361 .imported_name
1362 .as_ref()
1363 .map(|imported| vec![format!("{}.{}", binding.module_specifier, imported)])
1364 .unwrap_or_default(),
1365 _ => graph
1366 .index
1367 .declarations(file)
1368 .into_iter()
1369 .find(|unit| unit.is_function() && unit.identifier() == local)
1370 .map(|unit| vec![unit.fq_name()])
1371 .unwrap_or_default(),
1372 }
1373}
1374
1375fn resolve_attribute_callable_fqns(
1376 graph: &PythonGraphSource<'_>,
1377 python: &dyn PythonUsageSource,
1378 file: &ProjectFile,
1379 source: &str,
1380 function: Node<'_>,
1381 scope_facts: Option<&LocalBindingsSnapshot<String>>,
1382) -> Vec<String> {
1383 let Some(receiver) = function.child_by_field_name("object") else {
1384 return Vec::new();
1385 };
1386 let Some(method) = function.child_by_field_name("attribute") else {
1387 return Vec::new();
1388 };
1389 let method = slice(method, source);
1390 if method.is_empty() {
1391 return Vec::new();
1392 }
1393 let mut fqns = attribute_receiver_classes(graph, python, file, source, receiver, scope_facts)
1394 .into_iter()
1395 .map(|class| format!("{}.{}", class.fq_name(), method))
1396 .collect::<Vec<_>>();
1397 fqns.sort();
1398 fqns.dedup();
1399 fqns
1400}
1401
1402fn attribute_receiver_classes(
1403 graph: &PythonGraphSource<'_>,
1404 python: &dyn PythonUsageSource,
1405 file: &ProjectFile,
1406 source: &str,
1407 receiver: Node<'_>,
1408 scope_facts: Option<&LocalBindingsSnapshot<String>>,
1409) -> Vec<CodeUnit> {
1410 let mut classes = match receiver.kind() {
1411 "identifier" => {
1412 identifier_receiver_classes(graph, python, file, source, receiver, scope_facts)
1413 }
1414 "attribute" => {
1415 if let Some(root) = leftmost_identifier(receiver)
1416 && scope_facts.is_some_and(|facts| facts.is_shadowed(slice(root, source)))
1417 {
1418 Vec::new()
1419 } else {
1420 resolve_constructor_types(graph, python, file, source, receiver)
1421 }
1422 }
1423 _ => Vec::new(),
1424 };
1425 classes.sort();
1426 classes.dedup();
1427 classes
1428}
1429
1430fn identifier_receiver_classes(
1431 graph: &PythonGraphSource<'_>,
1432 python: &dyn PythonUsageSource,
1433 file: &ProjectFile,
1434 source: &str,
1435 receiver: Node<'_>,
1436 scope_facts: Option<&LocalBindingsSnapshot<String>>,
1437) -> Vec<CodeUnit> {
1438 let ident = slice(receiver, source);
1439 if ident.is_empty() {
1440 return Vec::new();
1441 }
1442 if matches!(ident, "self" | "cls")
1443 && let Some(class) = enclosing_class_for_node(graph, file, receiver)
1444 {
1445 return vec![class];
1446 }
1447 if let Some(facts) = scope_facts {
1448 if let Some(raw_type) = facts
1449 .resolution_for(ident)
1450 .as_precise()
1451 .and_then(|targets| targets.iter().next())
1452 && let Some(class) = resolve_receiver_type(graph, python, file, raw_type, false)
1453 {
1454 return vec![class];
1455 }
1456 if facts.is_shadowed(ident) {
1457 return Vec::new();
1458 }
1459 }
1460 resolve_receiver_type(graph, python, file, ident, false)
1461 .into_iter()
1462 .collect()
1463}
1464
1465fn enclosing_class_for_node(
1466 graph: &PythonGraphSource<'_>,
1467 file: &ProjectFile,
1468 node: Node<'_>,
1469) -> Option<CodeUnit> {
1470 let range = Range {
1471 start_byte: node.start_byte(),
1472 end_byte: node.end_byte(),
1473 start_line: 0,
1474 end_line: 0,
1475 };
1476 let enclosing = graph.index.enclosing_code_unit(file, &range)?;
1477 brokk_bifrost_core::analyzer::usages::common::enclosing_owner_chain(enclosing, |unit| {
1483 graph.index.parent_of(unit)
1484 })
1485 .take(2)
1486 .find(|unit| unit.is_class() && unit.source() == file)
1487}
1488
1489fn attribute_chain<'a>(node: Node<'a>) -> Option<(Node<'a>, Vec<Node<'a>>)> {
1490 let mut attributes = Vec::new();
1491 let mut current = node;
1492 loop {
1493 if current.kind() != "attribute" {
1494 return None;
1495 }
1496 attributes.push(current.child_by_field_name("attribute")?);
1497 current = current.child_by_field_name("object")?;
1498 if current.kind() == "identifier" {
1499 attributes.reverse();
1500 return Some((current, attributes));
1501 }
1502 }
1503}
1504
1505struct ImportedModuleBinding {
1506 module: String,
1507 consumed_attributes: usize,
1508}
1509
1510fn imported_module_bindings(
1511 ctx: &ScanCtx<'_>,
1512 root: Node<'_>,
1513 reference: Node<'_>,
1514) -> Vec<ImportedModuleBinding> {
1515 let root_text = slice(root, ctx.source);
1516 if root_text.is_empty() || import_root_shadowed(ctx, root_text, root, reference) {
1517 return Vec::new();
1518 }
1519
1520 if let Some(binding) = ctx.scoped_import_bindings.iter().rev().find(|binding| {
1521 binding.is_function_scoped()
1522 && binding.start_byte <= reference.start_byte()
1523 && binding.scope_start_byte <= reference.start_byte()
1524 && reference.end_byte() <= binding.scope_end_byte
1525 && binding.local_name == root_text
1526 }) {
1527 return if usage_resolve_module_files(ctx.python, ctx.file, &binding.qualified_name)
1528 .is_empty()
1529 {
1530 Vec::new()
1531 } else {
1532 vec![ImportedModuleBinding {
1533 module: binding.qualified_name.clone(),
1534 consumed_attributes: binding.consumed_attributes,
1535 }]
1536 };
1537 }
1538
1539 let Some(events) = ctx.raw_module_bindings.get(root_text) else {
1540 return Vec::new();
1541 };
1542 let cutoff = if reference_is_deferred_function_body(reference) {
1543 usize::MAX
1544 } else {
1545 reference.start_byte()
1546 };
1547 let visible: Vec<_> = events
1548 .iter()
1549 .filter(|event| event.visible_from <= cutoff)
1550 .collect();
1551 let start = visible
1556 .iter()
1557 .rposition(|event| {
1558 if event.conditional {
1559 return false;
1560 }
1561 match &event.kind {
1562 ModuleBindingEventKind::ImportModule {
1563 module,
1564 consumed_attributes,
1565 } => *consumed_attributes == 0 && module != root_text,
1566 ModuleBindingEventKind::FromImport { .. } | ModuleBindingEventKind::Other => true,
1567 }
1568 })
1569 .unwrap_or(0);
1570 let mut modules = visible[start..]
1571 .iter()
1572 .filter_map(|event| match &event.kind {
1573 ModuleBindingEventKind::ImportModule {
1574 module,
1575 consumed_attributes,
1576 } => {
1577 let mut segments = parse_symbol_path(Language::Python, module);
1578 segments.truncate(segments.len().saturating_sub(*consumed_attributes));
1579 Some(ImportedModuleBinding {
1580 module: segments.join("."),
1581 consumed_attributes: 0,
1582 })
1583 }
1584 ModuleBindingEventKind::FromImport {
1585 module,
1586 imported_name,
1587 } => {
1588 let submodule = if module.ends_with('.') {
1589 format!("{module}{imported_name}")
1590 } else {
1591 format!("{module}.{imported_name}")
1592 };
1593 (!usage_resolve_module_files(ctx.python, ctx.file, &submodule).is_empty())
1594 .then_some(ImportedModuleBinding {
1595 module: submodule,
1596 consumed_attributes: 0,
1597 })
1598 }
1599 ModuleBindingEventKind::Other => None,
1600 })
1601 .collect::<Vec<_>>();
1602 modules.sort_by(|left, right| {
1603 left.module
1604 .cmp(&right.module)
1605 .then_with(|| left.consumed_attributes.cmp(&right.consumed_attributes))
1606 });
1607 modules.dedup_by(|left, right| {
1608 left.module == right.module && left.consumed_attributes == right.consumed_attributes
1609 });
1610 modules
1611}
1612
1613fn import_root_shadowed(
1614 ctx: &ScanCtx<'_>,
1615 root_text: &str,
1616 root: Node<'_>,
1617 reference: Node<'_>,
1618) -> bool {
1619 ctx.scope_entry_for_node(root)
1620 .or_else(|| ctx.scope_entry_for_node(reference))
1621 .is_some_and(|(scope, facts)| !scope.is_module() && facts.is_shadowed(root_text))
1622 || enclosing_parameters_shadow(root_text, reference, ctx.source)
1623}
1624
1625fn enclosing_parameters_shadow(root_text: &str, reference: Node<'_>, source: &str) -> bool {
1626 let mut current = reference;
1627 while let Some(parent) = current.parent() {
1628 if matches!(parent.kind(), "function_definition" | "lambda") {
1629 let Some(parameters) = parent.child_by_field_name("parameters") else {
1630 return false;
1631 };
1632 let mut cursor = parameters.walk();
1633 return parameters.named_children(&mut cursor).any(|parameter| {
1634 parameter_symbol(parameter, source).as_deref() == Some(root_text)
1635 });
1636 }
1637 current = parent;
1638 }
1639 false
1640}
1641
1642enum EnclosingParameterType {
1643 NotDeclared,
1644 Untyped,
1645 Typed(String),
1646}
1647
1648fn enclosing_runtime_parameter_type(
1649 name: &str,
1650 reference: Node<'_>,
1651 source: &str,
1652) -> EnclosingParameterType {
1653 let site_start = reference.start_byte();
1654 let site_end = reference.end_byte();
1655 let mut current = reference;
1656 while let Some(parent) = current.parent() {
1657 if matches!(parent.kind(), "function_definition" | "lambda")
1658 && parent
1659 .child_by_field_name("body")
1660 .is_some_and(|body| body.start_byte() <= site_start && site_end <= body.end_byte())
1661 && let Some(parameters) = parent.child_by_field_name("parameters")
1662 {
1663 let mut cursor = parameters.walk();
1664 for parameter in parameters.named_children(&mut cursor) {
1665 if parameter_symbol(parameter, source).as_deref() != Some(name) {
1666 continue;
1667 }
1668 return parameter
1669 .child_by_field_name("type")
1670 .and_then(|annotation| normalized_receiver_type(slice(annotation, source)))
1671 .map_or(EnclosingParameterType::Untyped, |raw_type| {
1672 EnclosingParameterType::Typed(raw_type)
1673 });
1674 }
1675 }
1676 current = parent;
1677 }
1678 EnclosingParameterType::NotDeclared
1679}
1680
1681fn member_receiver_match_is_unproven(
1682 object: Node<'_>,
1683 object_text: &str,
1684 node: Node<'_>,
1685 ctx: &ScanCtx<'_>,
1686) -> bool {
1687 if matches!(object_text, "self" | "cls") {
1688 return false;
1689 }
1690 match object.kind() {
1691 "identifier" => {
1692 ctx.receiver_type_is_unknown(object_text, node) && !ctx.member_best_effort_unique
1693 }
1694 "attribute" => true,
1695 _ => false,
1696 }
1697}
1698
1699pub fn slice<'a>(node: Node<'_>, source: &'a str) -> &'a str {
1700 brokk_bifrost_core::analyzer::common::node_source_text(node, source)
1701}
1702
1703fn is_call_callee(node: Node<'_>) -> bool {
1705 node.parent().is_some_and(|parent| {
1706 parent.kind() == "call"
1707 && parent
1708 .child_by_field_name("function")
1709 .is_some_and(|function| function.id() == node.id())
1710 })
1711}
1712
1713pub fn is_declaration_identifier(node: Node<'_>) -> bool {
1714 let Some(parent) = node.parent() else {
1715 return false;
1716 };
1717 let contains = |container: Node<'_>| {
1718 container.start_byte() <= node.start_byte() && node.end_byte() <= container.end_byte()
1719 };
1720 match parent.kind() {
1721 "class_definition" | "function_definition" => parent
1722 .child_by_field_name("name")
1723 .is_some_and(|name| name.id() == node.id()),
1724 "parameters" | "lambda_parameters" | "list_splat_pattern" | "dictionary_splat_pattern" => {
1725 true
1726 }
1727 "default_parameter" | "typed_parameter" | "typed_default_parameter" => {
1728 parent.child_by_field_name("name").is_some_and(contains)
1729 }
1730 "assignment" | "augmented_assignment" | "for_statement" | "for_in_clause" => {
1731 parent.child_by_field_name("left").is_some_and(contains)
1732 }
1733 "named_expression" => parent.child_by_field_name("name").is_some_and(contains),
1734 "aliased_import" | "import_from_statement" | "import_statement" => true,
1735 _ => false,
1736 }
1737}
1738
1739#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1740enum ModuleBindingKind {
1741 TargetSymbolImport,
1742 TargetModuleImport,
1743 Other,
1744}
1745
1746#[derive(Clone, Copy, Debug)]
1747struct ClassifiedModuleBindingEvent {
1748 visible_from: usize,
1749 conditional: bool,
1750 kind: ModuleBindingKind,
1751}
1752
1753pub fn collect_module_binding_timeline(root: Node<'_>, source: &str) -> ModuleBindingTimeline {
1754 let mut timeline = ModuleBindingTimeline::default();
1755 let mut stack = vec![root];
1756 while let Some(node) = stack.pop() {
1757 match node.kind() {
1758 "function_definition" | "class_definition" => {
1759 if let Some(name) = node.child_by_field_name("name") {
1760 record_module_binding(
1761 &mut timeline,
1762 slice(name, source),
1763 node.end_byte(),
1764 binding_is_conditional(node),
1765 ModuleBindingEventKind::Other,
1766 );
1767 }
1768 continue;
1769 }
1770 "import_statement" | "import_from_statement" => {
1771 collect_import_binding_events(node, source, &mut timeline);
1772 continue;
1773 }
1774 "assignment" | "augmented_assignment" | "named_expression" => {
1775 if let Some(left) = node.child_by_field_name("left") {
1776 record_local_binding_targets(
1777 left,
1778 source,
1779 node.end_byte(),
1780 binding_is_conditional(node),
1781 &mut timeline,
1782 );
1783 }
1784 continue;
1785 }
1786 "for_statement" => {
1787 if let Some(left) = node.child_by_field_name("left") {
1788 record_local_binding_targets(
1789 left,
1790 source,
1791 left.end_byte(),
1792 true,
1793 &mut timeline,
1794 );
1795 }
1796 }
1797 _ => {}
1798 }
1799
1800 let mut cursor = node.walk();
1801 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
1802 children.reverse();
1803 stack.extend(children);
1804 }
1805 for events in timeline.values_mut() {
1806 events.sort_by_key(|event| event.visible_from);
1807 }
1808 timeline
1809}
1810
1811fn collect_import_binding_events(
1812 node: Node<'_>,
1813 source: &str,
1814 timeline: &mut ModuleBindingTimeline,
1815) {
1816 if node.kind() == "import_statement" {
1817 let mut cursor = node.walk();
1818 for imported in node.children_by_field_name("name", &mut cursor) {
1819 let name = imported.child_by_field_name("name").unwrap_or(imported);
1820 let Some(local) = imported
1821 .child_by_field_name("alias")
1822 .or_else(|| first_identifier(name))
1823 else {
1824 continue;
1825 };
1826 let module = slice(name, source).trim();
1827 let consumed_attributes = if imported.child_by_field_name("alias").is_some() {
1828 0
1829 } else {
1830 parse_symbol_path(Language::Python, module)
1831 .len()
1832 .saturating_sub(1)
1833 };
1834 record_module_binding(
1835 timeline,
1836 slice(local, source),
1837 node.end_byte(),
1838 binding_is_conditional(node),
1839 ModuleBindingEventKind::ImportModule {
1840 module: module.to_string(),
1841 consumed_attributes,
1842 },
1843 );
1844 }
1845 return;
1846 }
1847
1848 let Some(module_node) = node.child_by_field_name("module_name") else {
1849 return;
1850 };
1851 let module = slice(module_node, source).trim();
1852 let mut cursor = node.walk();
1853 for imported in node.children_by_field_name("name", &mut cursor) {
1854 if imported.kind() == "wildcard_import" {
1855 continue;
1856 }
1857 let name = imported.child_by_field_name("name").unwrap_or(imported);
1858 let Some(imported_identifier) = last_identifier(name) else {
1859 continue;
1860 };
1861 let imported_name = slice(imported_identifier, source).trim();
1862 let Some(local) = imported
1863 .child_by_field_name("alias")
1864 .or_else(|| last_identifier(name))
1865 else {
1866 continue;
1867 };
1868 record_module_binding(
1869 timeline,
1870 slice(local, source),
1871 node.end_byte(),
1872 binding_is_conditional(node),
1873 ModuleBindingEventKind::FromImport {
1874 module: module.to_string(),
1875 imported_name: imported_name.to_string(),
1876 },
1877 );
1878 }
1879}
1880
1881fn classify_module_binding_timeline(
1882 python: &dyn PythonUsageSource,
1883 file: &ProjectFile,
1884 timeline: &ModuleBindingTimeline,
1885 seeds: &BTreeSet<(ProjectFile, String)>,
1886 edges: &[ImportEdge],
1887) -> HashMap<String, Vec<ClassifiedModuleBindingEvent>> {
1888 let mut classified = HashMap::default();
1889 let mut module_targets: HashMap<String, bool> = HashMap::default();
1890 let relevant_locals: HashSet<&str> =
1891 edges.iter().map(|edge| edge.local_name.as_str()).collect();
1892 for (local, events) in timeline {
1893 if !relevant_locals.contains(local.as_str()) {
1894 continue;
1895 }
1896 let classified_events = events
1897 .iter()
1898 .map(|event| {
1899 let kind = match &event.kind {
1900 ModuleBindingEventKind::ImportModule { module, .. } => {
1901 if *module_targets
1902 .entry(module.clone())
1903 .or_insert_with(|| module_contains_seed(python, file, module, seeds))
1904 {
1905 ModuleBindingKind::TargetModuleImport
1906 } else {
1907 ModuleBindingKind::Other
1908 }
1909 }
1910 ModuleBindingEventKind::FromImport {
1911 module,
1912 imported_name,
1913 } => {
1914 let direct = usage_resolve_module_files(python, file, module).iter().any(
1915 |resolved| seeds.contains(&(resolved.clone(), imported_name.clone())),
1916 );
1917 let submodule = if module.ends_with('.') {
1918 format!("{module}{imported_name}")
1919 } else {
1920 format!("{module}.{imported_name}")
1921 };
1922 let imports_target_module =
1923 *module_targets.entry(submodule.clone()).or_insert_with(|| {
1924 module_contains_seed(python, file, &submodule, seeds)
1925 });
1926 if direct {
1927 ModuleBindingKind::TargetSymbolImport
1928 } else if imports_target_module {
1929 ModuleBindingKind::TargetModuleImport
1930 } else {
1931 ModuleBindingKind::Other
1932 }
1933 }
1934 ModuleBindingEventKind::Other => ModuleBindingKind::Other,
1935 };
1936 ClassifiedModuleBindingEvent {
1937 visible_from: event.visible_from,
1938 conditional: event.conditional,
1939 kind,
1940 }
1941 })
1942 .collect();
1943 classified.insert(local.clone(), classified_events);
1944 }
1945 classified
1946}
1947
1948fn module_contains_seed(
1949 python: &dyn PythonUsageSource,
1950 file: &ProjectFile,
1951 module: &str,
1952 seeds: &BTreeSet<(ProjectFile, String)>,
1953) -> bool {
1954 usage_resolve_module_files(python, file, module)
1955 .iter()
1956 .any(|resolved| seeds.iter().any(|(seed_file, _)| seed_file == resolved))
1957}
1958
1959fn record_module_binding(
1960 timeline: &mut ModuleBindingTimeline,
1961 name: &str,
1962 visible_from: usize,
1963 conditional: bool,
1964 kind: ModuleBindingEventKind,
1965) {
1966 let name = name.trim();
1967 if name.is_empty() {
1968 return;
1969 }
1970 timeline
1971 .entry(name.to_string())
1972 .or_default()
1973 .push(ModuleBindingEvent {
1974 visible_from,
1975 conditional,
1976 kind,
1977 });
1978}
1979
1980fn record_local_binding_targets(
1981 target: Node<'_>,
1982 source: &str,
1983 visible_from: usize,
1984 conditional: bool,
1985 timeline: &mut ModuleBindingTimeline,
1986) {
1987 let mut stack = vec![target];
1988 while let Some(node) = stack.pop() {
1989 if node.kind() == "identifier" {
1990 record_module_binding(
1991 timeline,
1992 slice(node, source),
1993 visible_from,
1994 conditional,
1995 ModuleBindingEventKind::Other,
1996 );
1997 continue;
1998 }
1999 if matches!(node.kind(), "attribute" | "subscript") {
2000 continue;
2001 }
2002 let mut cursor = node.walk();
2003 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
2004 children.reverse();
2005 stack.extend(children);
2006 }
2007}
2008
2009fn binding_is_conditional(mut node: Node<'_>) -> bool {
2010 while let Some(parent) = node.parent() {
2011 if matches!(
2012 parent.kind(),
2013 "if_statement"
2014 | "try_statement"
2015 | "except_clause"
2016 | "match_statement"
2017 | "case_clause"
2018 | "for_statement"
2019 | "while_statement"
2020 ) {
2021 return true;
2022 }
2023 if matches!(
2024 parent.kind(),
2025 "module" | "function_definition" | "class_definition"
2026 ) {
2027 return false;
2028 }
2029 node = parent;
2030 }
2031 false
2032}
2033
2034fn first_identifier(node: Node<'_>) -> Option<Node<'_>> {
2035 identifier_extreme(node, false)
2036}
2037
2038fn last_identifier(node: Node<'_>) -> Option<Node<'_>> {
2039 identifier_extreme(node, true)
2040}
2041
2042fn identifier_extreme(node: Node<'_>, last: bool) -> Option<Node<'_>> {
2043 let mut best = None;
2044 let mut stack = vec![node];
2045 while let Some(node) = stack.pop() {
2046 if node.kind() == "identifier" {
2047 if best.is_none_or(|current: Node<'_>| {
2048 if last {
2049 node.start_byte() > current.start_byte()
2050 } else {
2051 node.start_byte() < current.start_byte()
2052 }
2053 }) {
2054 best = Some(node);
2055 }
2056 continue;
2057 }
2058 let mut cursor = node.walk();
2059 stack.extend(node.named_children(&mut cursor));
2060 }
2061 best
2062}
2063
2064fn reference_is_deferred_function_body(node: Node<'_>) -> bool {
2065 let site_start = node.start_byte();
2066 let site_end = node.end_byte();
2067 let mut current = node;
2068 while let Some(parent) = current.parent() {
2069 if matches!(parent.kind(), "function_definition" | "lambda")
2070 && parent
2071 .child_by_field_name("body")
2072 .is_some_and(|body| body.start_byte() <= site_start && site_end <= body.end_byte())
2073 {
2074 return true;
2075 }
2076 current = parent;
2077 }
2078 false
2079}
2080
2081pub fn collect_assigned_identifiers(node: Node<'_>, source: &str, out: &mut HashSet<String>) {
2087 let mut stack = vec![node];
2088 while let Some(node) = stack.pop() {
2089 match node.kind() {
2090 "attribute" | "subscript" | "call" => continue,
2091 "identifier" => {
2092 let text = slice(node, source).trim();
2093 if !text.is_empty() {
2094 out.insert(text.to_string());
2095 }
2096 continue;
2097 }
2098 _ => {}
2099 }
2100
2101 let mut cursor = node.walk();
2102 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
2103 children.reverse();
2104 stack.extend(children);
2105 }
2106}
2107
2108pub fn collect_scope_facts_from_parsed_source(
2109 graph: &PythonGraphSource<'_>,
2110 python: &dyn PythonUsageSource,
2111 file: &ProjectFile,
2112 source: &str,
2113 root: Node<'_>,
2114) -> PythonScopeFacts {
2115 let mut factory_return_types = collect_factory_return_types_from_root(root, source);
2116 collect_imported_factory_return_types(graph, python, file, &mut factory_return_types);
2117 collect_scope_facts_with_factory_returns(graph, file, source, &factory_return_types)
2118}
2119
2120fn collect_imported_factory_return_types(
2121 graph: &PythonGraphSource<'_>,
2122 python: &dyn PythonUsageSource,
2123 file: &ProjectFile,
2124 factory_return_types: &mut HashMap<String, String>,
2125) {
2126 let binder = python.import_binder_of(file);
2127 for (local, binding) in &binder.bindings {
2128 if !matches!(binding.kind, ImportKind::Named) {
2129 continue;
2130 }
2131 let Some(imported) = binding.imported_name.as_deref() else {
2132 continue;
2133 };
2134 let fqn = format!("{}.{}", binding.module_specifier, imported);
2135 let units =
2136 resolve_fqn_candidates(python, &fqn, |name| graph.index.definitions(name).collect());
2137 for unit in units {
2138 if unit.is_function() {
2139 if let Some(return_type) = callable_return_type_name(graph, python, &unit) {
2140 factory_return_types
2141 .entry(local.clone())
2142 .or_insert(return_type);
2143 }
2144 continue;
2145 }
2146 if !unit.is_class() {
2147 continue;
2148 }
2149 factory_return_types
2150 .entry(local.clone())
2151 .or_insert_with(|| unit.identifier().to_string());
2152 collect_imported_class_method_return_types(
2153 graph,
2154 python,
2155 local,
2156 &unit,
2157 factory_return_types,
2158 );
2159 }
2160 }
2161}
2162
2163fn collect_imported_class_method_return_types(
2164 graph: &PythonGraphSource<'_>,
2165 python: &dyn PythonSource,
2166 local_class_name: &str,
2167 class_unit: &CodeUnit,
2168 factory_return_types: &mut HashMap<String, String>,
2169) {
2170 for member in graph.index.direct_children(class_unit) {
2171 if !member.is_function() {
2172 continue;
2173 }
2174 let Some(return_type) = callable_return_type_name(graph, python, &member) else {
2175 continue;
2176 };
2177 factory_return_types
2178 .entry(format!("{}.{}", local_class_name, member.identifier()))
2179 .or_insert(return_type);
2180 }
2181}
2182
2183fn callable_return_type_name(
2184 graph: &PythonGraphSource<'_>,
2185 python: &dyn PythonSource,
2186 callable: &CodeUnit,
2187) -> Option<String> {
2188 if let Some(prepared) = python.prepared_syntax(graph.token, callable.source()) {
2193 #[cfg(any(test, feature = "test-support"))]
2194 note_callable_return_type_lookup_for_test(true);
2195 return callable_return_type_name_in_tree(
2196 graph,
2197 callable,
2198 prepared.source(),
2199 prepared.tree().root_node(),
2200 );
2201 }
2202 #[cfg(any(test, feature = "test-support"))]
2203 note_callable_return_type_lookup_for_test(false);
2204 let source = graph.index.indexed_source(callable.source())?;
2205 declaration_source_slices(graph, callable, &source)
2206 .into_iter()
2207 .find_map(|declaration_source| {
2208 let mut parser = Parser::new();
2209 parser
2210 .set_language(&tree_sitter_python::LANGUAGE.into())
2211 .ok()?;
2212 let tree = parser.parse(declaration_source, None)?;
2213 let function = first_function_definition(tree.root_node())?;
2214 factory_return_type(function, declaration_source)
2215 })
2216}
2217
2218#[cfg(any(test, feature = "test-support"))]
2224#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
2225pub struct CallableReturnTypeLookupCounts {
2226 pub prepared: usize,
2228 pub reparsed: usize,
2230}
2231
2232#[cfg(any(test, feature = "test-support"))]
2233thread_local! {
2234 static CALLABLE_RETURN_TYPE_LOOKUPS_FOR_TEST: std::cell::Cell<CallableReturnTypeLookupCounts> =
2235 const { std::cell::Cell::new(CallableReturnTypeLookupCounts { prepared: 0, reparsed: 0 }) };
2236}
2237
2238#[cfg(any(test, feature = "test-support"))]
2239fn note_callable_return_type_lookup_for_test(from_prepared_syntax: bool) {
2240 CALLABLE_RETURN_TYPE_LOOKUPS_FOR_TEST.with(|counts| {
2241 let mut observed = counts.get();
2242 if from_prepared_syntax {
2243 observed.prepared += 1;
2244 } else {
2245 observed.reparsed += 1;
2246 }
2247 counts.set(observed);
2248 });
2249}
2250
2251#[cfg(any(test, feature = "test-support"))]
2254pub fn with_callable_return_type_lookup_counter_for_test<T>(
2255 body: impl FnOnce() -> T,
2256) -> (T, CallableReturnTypeLookupCounts) {
2257 CALLABLE_RETURN_TYPE_LOOKUPS_FOR_TEST.with(|counts| {
2258 counts.set(CallableReturnTypeLookupCounts::default());
2259 let result = body();
2260 let observed = counts.get();
2261 counts.set(CallableReturnTypeLookupCounts::default());
2262 (result, observed)
2263 })
2264}
2265
2266fn callable_return_type_name_in_tree(
2272 graph: &PythonGraphSource<'_>,
2273 callable: &CodeUnit,
2274 source: &str,
2275 root: Node<'_>,
2276) -> Option<String> {
2277 let mut ranges = graph.index.ranges(callable);
2278 ranges.sort_by_key(|range| range.start_byte);
2279 ranges.into_iter().find_map(|range| {
2280 let declaration = root.descendant_for_byte_range(range.start_byte, range.end_byte)?;
2281 let function = first_function_definition(declaration)?;
2282 factory_return_type(function, source)
2283 })
2284}
2285
2286fn factory_function_key(graph: &PythonGraphSource<'_>, callable: &CodeUnit) -> String {
2287 graph
2288 .index
2289 .parent_of(callable)
2290 .filter(CodeUnit::is_class)
2291 .map(|owner| format!("{}.{}", owner.identifier(), callable.identifier()))
2292 .unwrap_or_else(|| callable.identifier().to_string())
2293}
2294
2295fn first_function_definition(root: Node<'_>) -> Option<Node<'_>> {
2296 let mut stack = vec![root];
2297 while let Some(node) = stack.pop() {
2298 if node.kind() == "function_definition" {
2299 return Some(node);
2300 }
2301 let mut cursor = node.walk();
2302 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
2303 children.reverse();
2304 stack.extend(children);
2305 }
2306 None
2307}
2308
2309fn collect_scope_facts_with_factory_returns(
2310 graph: &PythonGraphSource<'_>,
2311 file: &ProjectFile,
2312 source: &str,
2313 factory_return_types: &HashMap<String, String>,
2314) -> PythonScopeFacts {
2315 let declarations = graph.index.declarations(file);
2316 let mut class_facts_by_name: HashMap<String, LocalBindingsSnapshot<String>> =
2317 HashMap::default();
2318 for declaration in declarations
2319 .iter()
2320 .filter(|declaration| declaration.is_class())
2321 {
2322 let Some(declaration_source) = declaration_source(graph, declaration, source) else {
2323 continue;
2324 };
2325 let facts = collect_scope_facts_from_source(
2326 &declaration_source,
2327 ScopeFactTraversal::Class,
2328 true,
2329 Some(declaration.short_name()),
2330 factory_return_types,
2331 );
2332 class_facts_by_name.insert(
2333 declaration.short_name().to_string(),
2334 facts.filtered_visible_bindings(|symbol, _| symbol.starts_with("self.")),
2335 );
2336 }
2337
2338 let mut scope_facts = HashMap::default();
2339 for declaration in declarations
2340 .iter()
2341 .filter(|declaration| declaration.is_function())
2342 {
2343 let Some(declaration_source) = declaration_source(graph, declaration, source) else {
2344 continue;
2345 };
2346 let owner = declaration
2351 .short_name()
2352 .rsplit_once('.')
2353 .map(|(owner, _)| owner);
2354 let mut facts = collect_scope_facts_from_source(
2355 &declaration_source,
2356 ScopeFactTraversal::Function,
2357 false,
2358 owner,
2359 factory_return_types,
2360 );
2361 if let Some(owner) = owner
2362 && let Some(class_facts) = class_facts_by_name.get(owner)
2363 {
2364 facts = facts.merged_with_visible(class_facts);
2365 }
2366 scope_facts.insert(declaration.clone(), facts);
2367 }
2368
2369 for declaration in declarations.iter().filter(|d| d.is_module()) {
2374 let Some(declaration_source) = declaration_source(graph, declaration, source) else {
2375 continue;
2376 };
2377 let facts = collect_scope_facts_from_source(
2378 &declaration_source,
2379 ScopeFactTraversal::Module,
2380 false,
2381 None,
2382 factory_return_types,
2383 );
2384 scope_facts.insert(declaration.clone(), facts);
2385 }
2386 scope_facts
2387}
2388
2389fn declaration_source(
2390 graph: &PythonGraphSource<'_>,
2391 declaration: &CodeUnit,
2392 file_source: &str,
2393) -> Option<String> {
2394 let slices = declaration_source_slices(graph, declaration, file_source);
2395 (!slices.is_empty()).then(|| slices.join("\n\n"))
2396}
2397
2398fn declaration_source_slices<'a>(
2399 graph: &PythonGraphSource<'_>,
2400 declaration: &CodeUnit,
2401 file_source: &'a str,
2402) -> Vec<&'a str> {
2403 let mut ranges = graph.index.ranges(declaration);
2404 ranges.sort_by_key(|range| range.start_byte);
2405 ranges
2406 .into_iter()
2407 .filter_map(|range| file_source.get(range.start_byte..range.end_byte))
2408 .collect()
2409}
2410
2411fn collect_scope_facts_from_source(
2412 source: &str,
2413 traversal: ScopeFactTraversal,
2414 allow_self_receivers: bool,
2415 current_class: Option<&str>,
2416 factory_return_types: &HashMap<String, String>,
2417) -> LocalBindingsSnapshot<String> {
2418 let events = collect_scope_fact_events(source, traversal);
2419 collect_scope_facts_from_events(
2420 &events,
2421 allow_self_receivers,
2422 current_class,
2423 factory_return_types,
2424 )
2425}
2426
2427pub fn collect_function_scope_facts_from_node(
2428 function: Node<'_>,
2429 source: &str,
2430) -> LocalBindingsSnapshot<String> {
2431 let mut events = Vec::new();
2432 if function.kind() == "lambda" {
2433 if let Some(parameters) = function.child_by_field_name("parameters") {
2434 collect_parameter_events(parameters, source, &mut events);
2435 }
2436 } else {
2437 collect_scope_fact_events_from_node(
2438 function,
2439 source,
2440 ScopeFactTraversal::Function,
2441 &mut events,
2442 );
2443 }
2444 collect_scope_facts_from_events(&events, false, None, &HashMap::default())
2445}
2446
2447fn collect_scope_facts_from_events(
2448 events: &[ScopeFactEvent],
2449 allow_self_receivers: bool,
2450 current_class: Option<&str>,
2451 factory_return_types: &HashMap<String, String>,
2452) -> LocalBindingsSnapshot<String> {
2453 let mut engine = LocalInferenceEngine::new(LocalInferenceConfig::default());
2454 let globals: HashSet<&str> = events
2455 .iter()
2456 .filter_map(|event| match event {
2457 ScopeFactEvent::Global { symbol } => Some(symbol.as_str()),
2458 _ => None,
2459 })
2460 .collect();
2461 let nonlocals: HashSet<&str> = events
2462 .iter()
2463 .filter_map(|event| match event {
2464 ScopeFactEvent::Nonlocal { symbol } => Some(symbol.as_str()),
2465 _ => None,
2466 })
2467 .collect();
2468 for symbol in &nonlocals {
2469 engine.declare_shadow((*symbol).to_string());
2470 }
2471 for event in events {
2472 if let ScopeFactEvent::Parameter { symbol, .. } = event
2473 && !globals.contains(symbol.as_str())
2474 && !nonlocals.contains(symbol.as_str())
2475 && !engine.is_shadowed(symbol)
2476 {
2477 engine.declare_shadow(symbol.clone());
2478 }
2479 }
2480
2481 let mut changed = true;
2482 while changed {
2483 changed = false;
2484 let mut aliases = Vec::new();
2485 for event in events {
2486 match event {
2487 ScopeFactEvent::Parameter {
2488 symbol,
2489 annotation: Some(annotation),
2490 }
2491 | ScopeFactEvent::Annotation { symbol, annotation } => {
2492 if globals.contains(symbol.as_str()) || nonlocals.contains(symbol.as_str()) {
2493 continue;
2494 }
2495 apply_annotation_event(
2496 symbol,
2497 annotation,
2498 allow_self_receivers,
2499 &mut engine,
2500 &mut changed,
2501 );
2502 }
2503 ScopeFactEvent::Parameter {
2504 annotation: None, ..
2505 } => {}
2506 ScopeFactEvent::Assignment { lhs, rhs } => {
2507 if globals.contains(lhs.as_str()) {
2508 continue;
2509 }
2510 if !engine.is_shadowed(lhs) {
2511 engine.declare_shadow(lhs.clone());
2512 }
2513 if lhs.starts_with("self.") && !allow_self_receivers {
2514 continue;
2515 }
2516
2517 match rhs {
2518 AssignmentRhs::Call(callee) => {
2519 if !engine.is_shadowed(callee) {
2520 if let Some(receiver_type) = factory_return_type_for_callee(
2521 callee,
2522 current_class,
2523 factory_return_types,
2524 ) && engine.resolve_symbol(lhs).is_unknown()
2525 {
2526 engine.seed_symbol(lhs.clone(), receiver_type.clone());
2527 changed = true;
2528 continue;
2529 }
2530
2531 if let Some(receiver_type) = normalized_receiver_type(callee)
2532 && engine.resolve_symbol(lhs).is_unknown()
2533 {
2534 engine.seed_symbol(lhs.clone(), receiver_type);
2535 changed = true;
2536 continue;
2537 }
2538 }
2539 }
2540 AssignmentRhs::Symbol(rhs_symbol) => {
2541 if !engine.is_shadowed(rhs_symbol)
2542 && let Some(receiver_type) = normalized_receiver_type(rhs_symbol)
2543 && engine.resolve_symbol(lhs).is_unknown()
2544 {
2545 engine.seed_symbol(lhs.clone(), receiver_type);
2546 changed = true;
2547 continue;
2548 }
2549
2550 if let SymbolResolution::Precise(targets) =
2551 engine.resolve_symbol(rhs_symbol)
2552 && !targets.is_empty()
2553 {
2554 aliases.push((lhs.clone(), rhs_symbol.clone()));
2555 }
2556 }
2557 AssignmentRhs::Unknown => {}
2558 }
2559 }
2560 ScopeFactEvent::Global { .. } | ScopeFactEvent::Nonlocal { .. } => {}
2561 }
2562 }
2563 let before = engine.snapshot();
2564 engine.apply_aliases_until_stable(aliases);
2565 if engine.snapshot() != before {
2566 changed = true;
2567 }
2568 }
2569
2570 engine.snapshot()
2571}
2572
2573fn factory_return_type_for_callee<'a>(
2574 callee: &str,
2575 current_class: Option<&str>,
2576 factory_return_types: &'a HashMap<String, String>,
2577) -> Option<&'a String> {
2578 if let Some(receiver_type) = factory_return_types.get(callee) {
2579 return Some(receiver_type);
2580 }
2581 let class_name = current_class?;
2582 let method = callee
2583 .strip_prefix("self.")
2584 .or_else(|| callee.strip_prefix("cls."))?;
2585 factory_return_types.get(&format!("{class_name}.{method}"))
2586}
2587
2588fn apply_annotation_event(
2589 symbol: &str,
2590 annotation: &str,
2591 allow_self_receivers: bool,
2592 engine: &mut LocalInferenceEngine<String>,
2593 changed: &mut bool,
2594) {
2595 if symbol.starts_with("self.") && !allow_self_receivers {
2596 return;
2597 }
2598 if let Some(receiver_type) = normalized_receiver_type(annotation)
2599 && engine.resolve_symbol(symbol).is_unknown()
2600 {
2601 engine.seed_symbol(symbol.to_string(), receiver_type);
2602 *changed = true;
2603 }
2604}
2605
2606enum ScopeFactEvent {
2607 Global {
2608 symbol: String,
2609 },
2610 Nonlocal {
2611 symbol: String,
2612 },
2613 Parameter {
2614 symbol: String,
2615 annotation: Option<String>,
2616 },
2617 Annotation {
2618 symbol: String,
2619 annotation: String,
2620 },
2621 Assignment {
2622 lhs: String,
2623 rhs: AssignmentRhs,
2624 },
2625}
2626
2627enum AssignmentRhs {
2628 Symbol(String),
2629 Call(String),
2630 Unknown,
2631}
2632
2633#[derive(Clone, Copy)]
2634enum ScopeFactTraversal {
2635 Module,
2636 Function,
2637 Class,
2638}
2639
2640fn collect_scope_fact_events(source: &str, traversal: ScopeFactTraversal) -> Vec<ScopeFactEvent> {
2641 if source.trim().is_empty() {
2642 return Vec::new();
2643 }
2644
2645 let mut parser = Parser::new();
2646 if parser
2647 .set_language(&tree_sitter_python::LANGUAGE.into())
2648 .is_err()
2649 {
2650 return Vec::new();
2651 }
2652 let Some(tree) = parser.parse(source, None) else {
2653 return Vec::new();
2654 };
2655
2656 let mut events = Vec::new();
2657 collect_scope_fact_events_from_node(tree.root_node(), source, traversal, &mut events);
2658 events
2659}
2660
2661fn collect_scope_fact_events_from_node(
2662 root: Node<'_>,
2663 source: &str,
2664 traversal: ScopeFactTraversal,
2665 events: &mut Vec<ScopeFactEvent>,
2666) {
2667 let mut stack = vec![(root, false)];
2668 while let Some((node, inside_function)) = stack.pop() {
2669 let next_inside_function = match traversal {
2670 ScopeFactTraversal::Module => {
2671 if matches!(
2672 node.kind(),
2673 "function_definition" | "class_definition" | "lambda"
2674 ) {
2675 continue;
2676 }
2677 false
2678 }
2679 ScopeFactTraversal::Function => match node.kind() {
2680 "function_definition" if inside_function => continue,
2681 "function_definition" => true,
2682 "class_definition" | "lambda" => continue,
2683 _ => inside_function,
2684 },
2685 ScopeFactTraversal::Class => inside_function,
2686 };
2687 if matches!(traversal, ScopeFactTraversal::Function) && !next_inside_function {
2688 let mut cursor = node.walk();
2689 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
2690 children.reverse();
2691 stack.extend(children.into_iter().map(|child| (child, false)));
2692 continue;
2693 }
2694 match node.kind() {
2695 "global_statement" => collect_scope_directive_events(node, source, events, |symbol| {
2696 ScopeFactEvent::Global { symbol }
2697 }),
2698 "nonlocal_statement" => {
2699 collect_scope_directive_events(node, source, events, |symbol| {
2700 ScopeFactEvent::Nonlocal { symbol }
2701 })
2702 }
2703 "parameters" | "lambda_parameters" => collect_parameter_events(node, source, events),
2704 "assignment" => collect_assignment_events(node, source, events),
2705 _ => {}
2706 }
2707
2708 let mut cursor = node.walk();
2709 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
2710 children.reverse();
2711 stack.extend(
2712 children
2713 .into_iter()
2714 .map(|child| (child, next_inside_function)),
2715 );
2716 }
2717}
2718
2719fn collect_scope_directive_events(
2720 node: Node<'_>,
2721 source: &str,
2722 events: &mut Vec<ScopeFactEvent>,
2723 make_event: impl Fn(String) -> ScopeFactEvent,
2724) {
2725 let mut cursor = node.walk();
2726 for identifier in node
2727 .named_children(&mut cursor)
2728 .filter(|child| child.kind() == "identifier")
2729 {
2730 let Some(symbol) = non_empty_node_text(identifier, source) else {
2731 continue;
2732 };
2733 events.push(make_event(symbol));
2734 }
2735}
2736
2737fn collect_parameter_events(node: Node<'_>, source: &str, events: &mut Vec<ScopeFactEvent>) {
2738 let mut cursor = node.walk();
2739 for child in node.named_children(&mut cursor) {
2740 if child.kind() == "type_parameter" {
2741 continue;
2742 }
2743 let Some(symbol) = parameter_symbol(child, source) else {
2744 continue;
2745 };
2746 if matches!(symbol.as_str(), "self" | "cls" | "/") {
2747 continue;
2748 }
2749 let annotation = child
2750 .child_by_field_name("type")
2751 .map(|annotation| slice(annotation, source).trim().to_string())
2752 .filter(|annotation| !annotation.is_empty());
2753 events.push(ScopeFactEvent::Parameter { symbol, annotation });
2754 }
2755}
2756
2757fn parameter_symbol(node: Node<'_>, source: &str) -> Option<String> {
2758 if node.kind() == "identifier" {
2759 return non_empty_node_text(node, source);
2760 }
2761 if let Some(name) = node.child_by_field_name("name") {
2762 return non_empty_node_text(name, source);
2763 }
2764 let mut cursor = node.walk();
2765 node.named_children(&mut cursor)
2766 .find(|child| child.kind() == "identifier")
2767 .and_then(|identifier| non_empty_node_text(identifier, source))
2768}
2769
2770fn collect_assignment_events(node: Node<'_>, source: &str, events: &mut Vec<ScopeFactEvent>) {
2771 let Some(left) = node.child_by_field_name("left") else {
2772 return;
2773 };
2774 let Some(lhs) = receiver_symbol(left, source) else {
2775 return;
2776 };
2777
2778 if let Some(annotation) = node
2779 .child_by_field_name("type")
2780 .map(|annotation| slice(annotation, source).trim().to_string())
2781 .filter(|annotation| !annotation.is_empty())
2782 {
2783 events.push(ScopeFactEvent::Annotation {
2784 symbol: lhs,
2785 annotation,
2786 });
2787 return;
2788 }
2789
2790 let rhs = node
2791 .child_by_field_name("right")
2792 .and_then(|right| rhs_symbol(right, source))
2793 .unwrap_or(AssignmentRhs::Unknown);
2794 events.push(ScopeFactEvent::Assignment { lhs, rhs });
2795}
2796
2797fn receiver_symbol(node: Node<'_>, source: &str) -> Option<String> {
2798 match node.kind() {
2799 "identifier" | "attribute" => non_empty_node_text(node, source),
2800 _ => None,
2801 }
2802}
2803
2804fn rhs_symbol(node: Node<'_>, source: &str) -> Option<AssignmentRhs> {
2805 match node.kind() {
2806 "identifier" | "attribute" => non_empty_node_text(node, source).map(AssignmentRhs::Symbol),
2807 "call" => node
2808 .child_by_field_name("function")
2809 .or_else(|| node.named_child(0))
2810 .and_then(|callee| receiver_symbol(callee, source))
2811 .map(AssignmentRhs::Call),
2812 _ => None,
2813 }
2814}
2815
2816fn non_empty_node_text(node: Node<'_>, source: &str) -> Option<String> {
2817 let text = slice(node, source).trim();
2818 (!text.is_empty()).then(|| text.to_string())
2819}
2820
2821fn collect_factory_return_types_from_root(root: Node<'_>, source: &str) -> HashMap<String, String> {
2822 let mut returns = HashMap::default();
2823 let mut functions = Vec::new();
2824 let mut stack = vec![(root, None::<String>)];
2825 while let Some((node, class_name)) = stack.pop() {
2826 match node.kind() {
2827 "class_definition" => {
2828 let next_class = node
2829 .child_by_field_name("name")
2830 .and_then(|name| non_empty_node_text(name, source))
2831 .or(class_name);
2832 push_factory_index_children(node, next_class, &mut stack);
2833 }
2834 "function_definition" => {
2835 if let Some(name) = node
2836 .child_by_field_name("name")
2837 .and_then(|name| non_empty_node_text(name, source))
2838 {
2839 let key = class_name
2840 .as_ref()
2841 .map(|class| format!("{class}.{name}"))
2842 .unwrap_or(name);
2843 if let Some(return_type) = factory_return_type(node, source) {
2844 returns.insert(key.clone(), return_type);
2845 }
2846 functions.push((key, class_name, node));
2847 }
2848 }
2849 _ => push_factory_index_children(node, class_name, &mut stack),
2850 }
2851 }
2852 for _ in 0..functions.len() {
2858 let mut changed = false;
2859 for (key, class_name, function) in &functions {
2860 let Some(return_type) =
2861 factory_return_type_with_known(*function, source, class_name.as_deref(), &returns)
2862 else {
2863 continue;
2864 };
2865 if returns.get(key) != Some(&return_type) {
2866 returns.insert(key.clone(), return_type);
2867 changed = true;
2868 }
2869 }
2870 if !changed {
2871 break;
2872 }
2873 }
2874 returns
2875}
2876
2877fn push_factory_index_children<'tree>(
2878 node: Node<'tree>,
2879 class_name: Option<String>,
2880 stack: &mut Vec<(Node<'tree>, Option<String>)>,
2881) {
2882 let mut cursor = node.walk();
2883 let mut children: Vec<Node<'tree>> = node.named_children(&mut cursor).collect();
2884 children.reverse();
2885 stack.extend(
2886 children
2887 .into_iter()
2888 .map(|child| (child, class_name.clone())),
2889 );
2890}
2891
2892fn factory_return_type(function: Node<'_>, source: &str) -> Option<String> {
2893 factory_return_type_with_known(function, source, None, &HashMap::default())
2894}
2895
2896fn factory_return_type_with_known(
2897 function: Node<'_>,
2898 source: &str,
2899 current_class: Option<&str>,
2900 known: &HashMap<String, String>,
2901) -> Option<String> {
2902 if let Some(return_type) = function.child_by_field_name("return_type") {
2903 return receiver_type_from_annotation_node(return_type, source);
2904 }
2905
2906 let body = function.child_by_field_name("body")?;
2907 let mut candidates = HashSet::default();
2908 let mut saw_return = false;
2909 let mut saw_unknown_return = false;
2910 let mut stack = vec![body];
2911 while let Some(node) = stack.pop() {
2912 if node != body && matches!(node.kind(), "function_definition" | "class_definition") {
2913 continue;
2914 }
2915 if node.kind() == "return_statement" {
2916 saw_return = true;
2917 match node
2918 .named_child(0)
2919 .and_then(|value| returned_receiver_type(value, source))
2920 {
2921 Some(returned_type) => {
2922 candidates.insert(
2923 canonical_factory_return(&returned_type, current_class, known)
2924 .unwrap_or(returned_type),
2925 );
2926 }
2927 None => saw_unknown_return = true,
2928 }
2929 }
2930 let mut cursor = node.walk();
2931 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
2932 children.reverse();
2933 stack.extend(children);
2934 }
2935 if !saw_return || saw_unknown_return {
2936 return None;
2937 }
2938 (candidates.len() == 1)
2939 .then(|| candidates.into_iter().next())
2940 .flatten()
2941}
2942
2943fn canonical_factory_return(
2944 raw: &str,
2945 current_class: Option<&str>,
2946 known: &HashMap<String, String>,
2947) -> Option<String> {
2948 let mut current = raw;
2949 let mut seen = HashSet::default();
2950 while let Some(next) = factory_return_type_for_callee(current, current_class, known) {
2951 if !seen.insert(current.to_string()) {
2952 return None;
2953 }
2954 current = next;
2955 }
2956 Some(current.to_string())
2957}
2958
2959fn receiver_type_from_annotation_node(annotation: Node<'_>, source: &str) -> Option<String> {
2965 match annotation.kind() {
2966 "type" => receiver_type_from_annotation_node(annotation.named_child(0)?, source),
2967 "identifier" | "attribute" | "member_type" | "string" => {
2968 normalized_receiver_type(slice(annotation, source).trim())
2969 }
2970 "generic_type" => {
2971 let base = annotation.named_child(0)?;
2972 if optional_annotation_wrapper(base, source) {
2973 let parameter = annotation.named_child(1)?;
2974 return receiver_type_from_annotation_node(parameter.named_child(0)?, source);
2975 }
2976 receiver_type_from_annotation_node(base, source)
2977 }
2978 "subscript" => {
2979 let value = annotation.child_by_field_name("value")?;
2980 if optional_annotation_wrapper(value, source) {
2981 let inner = annotation.child_by_field_name("subscript")?;
2982 return receiver_type_from_annotation_node(inner, source);
2983 }
2984 normalized_receiver_type(slice(value, source).trim())
2985 }
2986 _ => None,
2987 }
2988}
2989
2990fn optional_annotation_wrapper(node: Node<'_>, source: &str) -> bool {
2991 match node.kind() {
2992 "identifier" => slice(node, source) == "Optional",
2993 "attribute" => {
2994 let (Some(object), Some(attribute)) = (
2995 node.child_by_field_name("object"),
2996 node.child_by_field_name("attribute"),
2997 ) else {
2998 return false;
2999 };
3000 object.kind() == "identifier"
3001 && attribute.kind() == "identifier"
3002 && slice(object, source) == "typing"
3003 && slice(attribute, source) == "Optional"
3004 }
3005 _ => false,
3006 }
3007}
3008
3009fn returned_receiver_type(node: Node<'_>, source: &str) -> Option<String> {
3010 let raw = match node.kind() {
3011 "identifier" => non_empty_node_text(node, source),
3012 "call" => node
3013 .child_by_field_name("function")
3014 .or_else(|| node.named_child(0))
3015 .filter(|callee| callee.kind() == "identifier")
3016 .and_then(|callee| non_empty_node_text(callee, source)),
3017 _ => None,
3018 }?;
3019 normalized_receiver_type(&raw)
3020}
3021
3022#[cfg(test)]
3023mod tests {
3024 use super::*;
3025 use std::path::PathBuf;
3026
3027 #[test]
3028 fn lambda_scope_facts_preserve_untyped_parameter_shadowing() {
3029 let source = "shadowed = lambda method: method.signature\n";
3030 let mut parser = Parser::new();
3031 parser
3032 .set_language(&tree_sitter_python::LANGUAGE.into())
3033 .unwrap();
3034 let tree = parser.parse(source, None).unwrap();
3035 let mut nodes = vec![tree.root_node()];
3036 let lambda = loop {
3037 let node = nodes.pop().unwrap();
3038 if node.kind() == "lambda" {
3039 break node;
3040 }
3041 let mut cursor = node.walk();
3042 nodes.extend(node.named_children(&mut cursor));
3043 };
3044
3045 let facts = collect_function_scope_facts_from_node(lambda, source);
3046
3047 assert!(facts.is_shadowed("method"));
3048 assert!(facts.resolution_for("method").is_unknown());
3049 }
3050
3051 #[test]
3052 fn pre_cancelled_graph_build_skips_python_file_parsing() {
3053 let temp = tempfile::tempdir().unwrap();
3054 let root = temp.path().canonicalize().unwrap();
3055 std::fs::write(root.join("target.py"), "def target():\n pass\n").unwrap();
3056 let file = ProjectFile::new(root.clone(), PathBuf::from("target.py"));
3057 let files = [file.clone()].into_iter().collect();
3058 let cancellation = CancellationToken::default();
3059 cancellation.cancel();
3060
3061 let graph = build_python_graph(&files, &file, Some(&cancellation));
3062
3063 assert!(graph.parsed.is_empty());
3064 }
3065
3066 #[test]
3067 fn graph_build_parses_only_candidates_and_target_not_transitive_imports() {
3068 let temp = tempfile::tempdir().unwrap();
3069 let root = temp.path().canonicalize().unwrap();
3070 std::fs::write(root.join("target.py"), "from dependency import value\n").unwrap();
3071 std::fs::write(
3072 root.join("candidate.py"),
3073 "from transitively_imported import value\n",
3074 )
3075 .unwrap();
3076 std::fs::write(root.join("dependency.py"), "value = 1\n").unwrap();
3077 std::fs::write(root.join("transitively_imported.py"), "value = 2\n").unwrap();
3078 let target = ProjectFile::new(root.clone(), PathBuf::from("target.py"));
3079 let candidate = ProjectFile::new(root.clone(), PathBuf::from("candidate.py"));
3080 let dependency = ProjectFile::new(root.clone(), PathBuf::from("dependency.py"));
3081 let transitive = ProjectFile::new(root.clone(), PathBuf::from("transitively_imported.py"));
3082 let candidates = [candidate.clone()].into_iter().collect();
3083
3084 let graph = build_python_graph(&candidates, &target, None);
3085
3086 assert_eq!(graph.parsed.len(), 2);
3087 assert!(graph.parsed.contains_key(&target));
3088 assert!(graph.parsed.contains_key(&candidate));
3089 assert!(!graph.parsed.contains_key(&dependency));
3090 assert!(!graph.parsed.contains_key(&transitive));
3091 }
3092}