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