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