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