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