1use super::extractor::{
20 call_result_types, collect_assigned_identifiers, collect_function_scope_facts_from_node,
21 collect_scope_facts_from_parsed_source, enclosing_scope_facts, is_declaration_identifier,
22 slice,
23};
24use super::resolver::{
25 annotation_reference_candidates, resolve_callable_parameter_default_types,
26 resolve_constructor_types, resolve_receiver_type, resolve_visible_named_import_candidates,
27 resolved_member_declarations,
28};
29use crate::bindings::{python_comprehension_binds_name_at, python_type_parameter_binds_name_at};
30use crate::graph::PythonGraphSource;
31use crate::graph_support::PythonUsageSource;
32use crate::imports::{imported_module_assignment_at, resolve_fqn_candidates};
33use crate::syntax::{
34 attribute_chain, leftmost_identifier, python_reference_is_deferred_function_body,
35};
36use crate::usage_index::{
37 ModuleBindingEventKind, ModuleBindingTimeline, usage_module_binding_timeline,
38 usage_resolve_module_files, usage_scope_facts,
39};
40use brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path;
41use brokk_bifrost_core::analyzer::usages::inverted_edges::{
42 FileEdgeScanInput, PerFileEdges, UsageReferenceKind, classify_reference_node,
43};
44use brokk_bifrost_core::analyzer::usages::local_inference::LocalBindingsSnapshot;
45use brokk_bifrost_core::analyzer::usages::model::ImportKind;
46use brokk_bifrost_core::analyzer::{CodeUnit, Language, ProjectFile, Range};
47use brokk_bifrost_core::cancellation::CancellationToken;
48use brokk_bifrost_core::hash::{HashMap, HashSet};
49use std::sync::{Arc, Mutex};
50use tree_sitter::Node;
51
52pub struct PythonEdgeScan<'a> {
59 targets: Option<&'a HashSet<String>>,
60 targets_by_terminal: HashMap<String, Vec<String>>,
61 canonical_namespace_candidates: Mutex<HashMap<String, Arc<Vec<String>>>>,
62}
63
64impl<'a> PythonEdgeScan<'a> {
65 pub fn new(nodes: &HashSet<String>, targets: &'a HashSet<String>) -> Self {
68 debug_assert!(targets.is_subset(nodes));
69 let mut targets_by_terminal: HashMap<String, Vec<String>> = HashMap::default();
70 for target in targets {
71 let terminal = parse_symbol_path(Language::Python, target)
76 .pop()
77 .unwrap_or_else(|| target.clone());
78 targets_by_terminal
79 .entry(terminal)
80 .or_default()
81 .push(target.clone());
82 }
83 Self {
84 targets: Some(targets),
85 targets_by_terminal,
86 canonical_namespace_candidates: Mutex::new(HashMap::default()),
87 }
88 }
89
90 pub fn new_rooted() -> Self {
94 Self {
95 targets: None,
96 targets_by_terminal: HashMap::default(),
97 canonical_namespace_candidates: Mutex::new(HashMap::default()),
98 }
99 }
100
101 pub fn scan_file(
107 &self,
108 graph: &PythonGraphSource<'_>,
109 python: &dyn PythonUsageSource,
110 file: &ProjectFile,
111 input: &FileEdgeScanInput<'_>,
112 cancellation: &CancellationToken,
113 ) -> Result<PerFileEdges, ProjectFile> {
114 let source = input.source;
115 let canonical_imports = python.import_info_of(graph.token, file);
116
117 let binder = python.import_binder_of(file);
122 let mut named: HashMap<String, String> = HashMap::default();
123 let mut namespace: HashMap<String, NamespaceBinding> = HashMap::default();
124 for (local, binding) in &binder.bindings {
125 if cancellation.is_cancelled() {
126 return Err(file.clone());
127 }
128 match binding.kind {
129 ImportKind::Named => {
130 if let Some(imported) = &binding.imported_name {
131 let module = canonical_import_module_fqn(
132 graph,
133 python,
134 file,
135 &binding.module_specifier,
136 )
137 .unwrap_or_else(|| binding.module_specifier.clone());
138 let imported_fqn = if module.ends_with('.') {
139 format!("{module}{imported}")
140 } else {
141 format!("{module}.{imported}")
142 };
143 named.insert(local.clone(), imported_fqn);
148 }
149 }
150 ImportKind::Namespace => {
151 let direct_module = binding.module_specifier.clone();
152 let imported_module = binding
153 .namespace_imported_module
154 .as_deref()
155 .unwrap_or(&direct_module);
156 let module = canonical_import_module_fqn(graph, python, file, imported_module);
157 let workspace_module = module.is_some();
158 let consumed_attributes = module.as_ref().map_or(0, |_| {
159 let imported_segments =
160 parse_symbol_path(Language::Python, imported_module);
161 let bound_segments = parse_symbol_path(Language::Python, &direct_module);
162 imported_segments.len().saturating_sub(bound_segments.len())
163 });
164 let canonical_module = module.unwrap_or(direct_module);
165 let mut root_segments = parse_symbol_path(Language::Python, &canonical_module);
166 root_segments.truncate(root_segments.len().saturating_sub(consumed_attributes));
167 namespace.insert(
168 local.clone(),
169 NamespaceBinding {
170 root_module: root_segments.join("."),
171 module: canonical_module,
172 workspace_module,
173 consumed_attributes,
174 },
175 );
176 }
177 ImportKind::Default | ImportKind::CommonJsRequire | ImportKind::Glob => {}
178 }
179 }
180 let same_file: HashMap<String, String> = graph
181 .index
182 .declarations(file)
183 .into_iter()
184 .map(|unit| (unit.identifier().to_string(), unit.fq_name()))
185 .collect();
186 let module_bindings = usage_module_binding_timeline(python, file, || {
187 super::extractor::collect_module_binding_timeline(
188 input.root(),
189 source,
190 &canonical_imports,
191 )
192 });
193
194 let Some(scope_facts) = usage_scope_facts(python, file, || {
198 collect_scope_facts_from_parsed_source(graph, python, file, source, input.root())
199 }) else {
200 return Err(file.clone());
201 };
202
203 let mut ctx = PyScan {
204 cancellation,
205 graph,
206 python,
207 targets: self.targets,
208 targets_by_terminal: &self.targets_by_terminal,
209 file,
210 source,
211 named,
212 namespace,
213 same_file,
214 module_bindings,
215 scope_facts: scope_facts.as_ref(),
216 canonical_namespace_candidates: &self.canonical_namespace_candidates,
217 input,
218 edges: PerFileEdges::default(),
219 pending: Vec::new(),
220 };
221 scan_tree(input.root(), &mut ctx);
222 ctx.resolve_pending();
223 if cancellation.is_cancelled() {
224 return Err(file.clone());
225 }
226 Ok(ctx.edges)
227 }
228}
229
230fn reference_span(node: Node<'_>) -> (UsageReferenceKind, usize, usize) {
231 (
232 classify_reference_node(node),
233 node.start_byte(),
234 node.end_byte(),
235 )
236}
237
238fn canonical_import_module_fqn(
239 graph: &PythonGraphSource<'_>,
240 python: &dyn PythonUsageSource,
241 importing_file: &ProjectFile,
242 module_specifier: &str,
243) -> Option<String> {
244 let resolved = usage_resolve_module_files(python, importing_file, module_specifier);
245 let [module_file] = resolved.as_slice() else {
246 return None;
247 };
248 graph
249 .index
250 .declarations(module_file)
251 .into_iter()
252 .find(CodeUnit::is_module)
253 .map(|module| module.fq_name())
254}
255
256struct PyScan<'a> {
257 cancellation: &'a CancellationToken,
258 graph: &'a PythonGraphSource<'a>,
259 python: &'a dyn PythonUsageSource,
260 targets: Option<&'a HashSet<String>>,
261 targets_by_terminal: &'a HashMap<String, Vec<String>>,
262 file: &'a ProjectFile,
263 source: &'a str,
264 named: HashMap<String, String>,
265 namespace: HashMap<String, NamespaceBinding>,
266 same_file: HashMap<String, String>,
267 module_bindings: Arc<ModuleBindingTimeline>,
268 scope_facts: &'a HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
269 canonical_namespace_candidates: &'a Mutex<HashMap<String, Arc<Vec<String>>>>,
270 input: &'a FileEdgeScanInput<'a>,
271 edges: PerFileEdges,
272 pending: Vec<PendingCallee>,
277}
278
279enum PendingCallee {
284 Direct {
286 callee: String,
287 kind: UsageReferenceKind,
288 start: usize,
289 end: usize,
290 },
291 WithNamespaceFallback {
294 direct: String,
295 kind: UsageReferenceKind,
296 start: usize,
297 end: usize,
298 },
299 WithAncestorFallback {
302 direct: String,
303 inherited: Vec<String>,
304 kind: UsageReferenceKind,
305 start: usize,
306 end: usize,
307 },
308}
309
310struct NamespaceBinding {
311 root_module: String,
312 module: String,
313 workspace_module: bool,
314 consumed_attributes: usize,
315}
316
317impl PyScan<'_> {
318 fn bare_callee(&self, text: &str, node: Node<'_>) -> Option<String> {
321 if let Some(fqn) = self.named.get(text) {
322 return Some(fqn.clone());
323 }
324 if let Some(fqn) = self.namespace.get(text) {
325 return self
326 .visible_namespace_root(text, node)
327 .or_else(|| Some(fqn.root_module.clone()));
328 }
329 if let Some(fqn) = self.same_file.get(text) {
330 return Some(fqn.clone());
331 }
332 None
333 }
334
335 fn visible_namespace_root(&self, local: &str, node: Node<'_>) -> Option<String> {
336 let events = self.module_bindings.get(local)?;
337 let cutoff = if python_reference_is_deferred_function_body(node) {
338 usize::MAX
339 } else {
340 node.start_byte()
341 };
342 let visible: Vec<_> = events
343 .iter()
344 .filter(|event| event.visible_from <= cutoff)
345 .collect();
346 let start = visible
347 .iter()
348 .rposition(|event| {
349 if event.conditional {
350 return false;
351 }
352 match &event.kind {
353 ModuleBindingEventKind::ImportModule {
354 module,
355 consumed_attributes,
356 } => *consumed_attributes == 0 && module != local,
357 ModuleBindingEventKind::FromImport { .. } | ModuleBindingEventKind::Other => {
358 true
359 }
360 }
361 })
362 .unwrap_or(0);
363 let mut roots = visible[start..]
364 .iter()
365 .filter_map(|event| match &event.kind {
366 ModuleBindingEventKind::ImportModule {
367 module,
368 consumed_attributes,
369 } => {
370 let mut segments = parse_symbol_path(Language::Python, module);
371 segments.truncate(segments.len().saturating_sub(*consumed_attributes));
372 let root = segments.join(".");
373 canonical_import_module_fqn(self.graph, self.python, self.file, &root)
374 .or(Some(root))
375 }
376 ModuleBindingEventKind::FromImport { .. } | ModuleBindingEventKind::Other => None,
377 })
378 .collect::<Vec<_>>();
379 roots.sort();
380 roots.dedup();
381 (roots.len() == 1).then(|| roots.remove(0))
382 }
383
384 fn receiver_type_fqn(
388 &self,
389 facts: &LocalBindingsSnapshot<String>,
390 receiver: &str,
391 ) -> Option<String> {
392 let resolution = facts.resolution_for(receiver);
393 let type_name = resolution
394 .as_precise()
395 .and_then(|targets| targets.iter().next())?;
396 resolve_receiver_type(self.graph, self.python, self.file, type_name, false)
402 .map(|unit| unit.fq_name())
403 }
404
405 fn record(&mut self, callee: String, node: Node<'_>) {
406 let (kind, start, end) = reference_span(node);
407 if self.targets.is_none() {
414 self.pending.push(PendingCallee::Direct {
415 callee,
416 kind,
417 start,
418 end,
419 });
420 return;
421 }
422 self.record_direct(callee, kind, start, end);
423 }
424
425 fn record_direct_or_namespace_fallback(&mut self, direct: String, node: Node<'_>) {
430 let (kind, start, end) = reference_span(node);
431 if self.targets.is_none() {
432 self.pending.push(PendingCallee::WithNamespaceFallback {
433 direct,
434 kind,
435 start,
436 end,
437 });
438 return;
439 }
440 self.record_namespace_fallback(direct, kind, start, end);
441 }
442
443 fn record_direct_or_ancestor_fallback(
447 &mut self,
448 direct: String,
449 inherited: Vec<String>,
450 node: Node<'_>,
451 ) {
452 let (kind, start, end) = reference_span(node);
453 if self.targets.is_none() {
454 self.pending.push(PendingCallee::WithAncestorFallback {
455 direct,
456 inherited,
457 kind,
458 start,
459 end,
460 });
461 return;
462 }
463 self.record_ancestor_fallback(direct, inherited, kind, start, end);
464 }
465
466 fn record_direct(
470 &mut self,
471 callee: String,
472 kind: UsageReferenceKind,
473 start: usize,
474 end: usize,
475 ) {
476 self.record_if_accepted(callee, kind, start, end);
477 }
478
479 fn record_if_accepted(
480 &mut self,
481 callee: String,
482 kind: UsageReferenceKind,
483 start: usize,
484 end: usize,
485 ) -> bool {
486 if !self.accepts_target(&callee) {
487 return false;
488 }
489 self.edges.record_kind(self.input, callee, kind, start, end);
490 true
491 }
492
493 fn record_namespace_fallback(
496 &mut self,
497 direct: String,
498 kind: UsageReferenceKind,
499 start: usize,
500 end: usize,
501 ) {
502 if self.accepts_target(&direct) {
503 self.edges.record_kind(self.input, direct, kind, start, end);
504 return;
505 }
506 for resolved in self.canonical_namespace_candidates(&direct).iter() {
510 self.record_direct(resolved.clone(), kind, start, end);
511 }
512 }
513
514 fn record_ancestor_fallback(
517 &mut self,
518 direct: String,
519 inherited: Vec<String>,
520 kind: UsageReferenceKind,
521 start: usize,
522 end: usize,
523 ) {
524 if self.record_if_accepted(direct, kind, start, end) {
525 return;
526 }
527 for candidate in inherited {
528 self.record_if_accepted(candidate, kind, start, end);
529 }
530 }
531
532 fn resolve_pending(&mut self) {
536 if self.pending.is_empty() {
537 return;
538 }
539 let mut names = Vec::new();
540 for item in &self.pending {
541 if self.cancellation.is_cancelled() {
542 return;
543 }
544 match item {
545 PendingCallee::Direct { callee, .. } => names.push(callee.clone()),
546 PendingCallee::WithNamespaceFallback { direct, .. } => names.push(direct.clone()),
547 PendingCallee::WithAncestorFallback {
548 direct, inherited, ..
549 } => {
550 names.push(direct.clone());
551 names.extend(inherited.iter().cloned());
552 }
553 }
554 }
555 self.graph.index.prefetch_definitions(&names);
556 for item in std::mem::take(&mut self.pending) {
557 if self.cancellation.is_cancelled() {
558 return;
559 }
560 match item {
561 PendingCallee::Direct {
562 callee,
563 kind,
564 start,
565 end,
566 } => self.record_direct(callee, kind, start, end),
567 PendingCallee::WithNamespaceFallback {
568 direct,
569 kind,
570 start,
571 end,
572 } => self.record_namespace_fallback(direct, kind, start, end),
573 PendingCallee::WithAncestorFallback {
574 direct,
575 inherited,
576 kind,
577 start,
578 end,
579 } => self.record_ancestor_fallback(direct, inherited, kind, start, end),
580 }
581 }
582 }
583
584 fn record_unproven_name(&mut self, name: &str, node: Node<'_>) {
585 self.edges
586 .record_unproven_name_at_node(self.input, name, node);
587 }
588
589 fn accepts_target(&self, fqn: &str) -> bool {
590 self.targets.map_or_else(
591 || self.graph.index.definitions(fqn).next().is_some(),
592 |targets| targets.contains(fqn),
593 )
594 }
595
596 fn may_have_target_terminal(&self, terminal: &str) -> bool {
597 self.targets.is_none() || self.targets_by_terminal.contains_key(terminal)
598 }
599
600 fn canonical_namespace_candidates(&self, direct: &str) -> Arc<Vec<String>> {
601 if let Some(cached) = self
602 .canonical_namespace_candidates
603 .lock()
604 .expect("Python namespace candidate cache mutex poisoned")
605 .get(direct)
606 .cloned()
607 {
608 return cached;
609 }
610
611 let resolved: Arc<Vec<String>> = Arc::new(
612 resolve_fqn_candidates(self.python, None, direct, |name| {
613 self.graph.index.definitions(name).collect()
614 })
615 .into_iter()
616 .map(|unit| unit.fq_name())
617 .collect(),
618 );
619 self.canonical_namespace_candidates
620 .lock()
621 .expect("Python namespace candidate cache mutex poisoned")
622 .entry(direct.to_string())
623 .or_insert_with(|| resolved.clone())
624 .clone()
625 }
626}
627
628fn scan_tree(root: Node<'_>, ctx: &mut PyScan<'_>) {
629 let mut scopes: Vec<FunctionScope> = Vec::new();
632 walk(root, ctx, &mut scopes, None);
633}
634
635fn walk(
636 node: Node<'_>,
637 ctx: &mut PyScan<'_>,
638 scopes: &mut Vec<FunctionScope>,
639 facts: Option<usize>,
640) {
641 let mut merged_facts = Vec::new();
642 let mut stack = vec![WalkFrame::Enter { node, facts }];
643 while let Some(frame) = stack.pop() {
644 if ctx.cancellation.is_cancelled() {
645 return;
646 }
647 match frame {
648 WalkFrame::Enter { node, facts } => match node.kind() {
649 "import_statement" | "import_from_statement" => {}
650 "function_definition" | "lambda" => {
654 let function_scope = collect_function_scope(node, ctx.source);
655 let scope_facts = merged_enclosing_scope_facts(
656 ctx.graph,
657 ctx.file,
658 ctx.scope_facts,
659 &mut merged_facts,
660 node,
661 ctx.source,
662 facts,
663 );
664 push_function_children(node, facts, scope_facts, function_scope, &mut stack);
665 }
666 "class_definition" => push_children(node, None, &mut stack),
669 "identifier" => {
670 if !handle_annotation_reference(node, ctx) {
671 handle_identifier(node, ctx, scopes);
672 }
673 push_children(node, facts, &mut stack);
674 }
675 "attribute" => {
676 if handle_annotation_reference(node, ctx) {
677 continue;
678 }
679 let scope_facts = facts.and_then(|id| merged_facts.get(id));
680 handle_attribute(node, ctx, scopes, scope_facts);
681 push_children(node, facts, &mut stack);
682 }
683 "string_content" => {
684 handle_annotation_reference(node, ctx);
685 }
686 "keyword_argument" => {
687 handle_keyword_argument(node, ctx, scopes);
688 if let Some(value) = node.child_by_field_name("value") {
689 stack.push(WalkFrame::Enter { node: value, facts });
690 }
691 }
692 _ => push_children(node, facts, &mut stack),
693 },
694 WalkFrame::ExitScope => {
695 scopes.pop();
696 }
697 WalkFrame::EnterScope(scope) => scopes.push(scope),
698 }
699 }
700}
701
702enum WalkFrame<'tree> {
703 Enter {
704 node: Node<'tree>,
705 facts: Option<usize>,
706 },
707 EnterScope(FunctionScope),
708 ExitScope,
709}
710
711fn push_children<'tree>(
712 node: Node<'tree>,
713 facts: Option<usize>,
714 stack: &mut Vec<WalkFrame<'tree>>,
715) {
716 brokk_bifrost_core::analyzer::tree_walk::push_named_children_reversed_as(node, stack, |node| {
717 WalkFrame::Enter { node, facts }
718 });
719}
720
721fn push_function_children<'tree>(
722 function: Node<'tree>,
723 enclosing_facts: Option<usize>,
724 body_facts: Option<usize>,
725 function_scope: FunctionScope,
726 stack: &mut Vec<WalkFrame<'tree>>,
727) {
728 let body = function.child_by_field_name("body");
729 let mut function_scope = Some(function_scope);
730 for index in (0..function.named_child_count()).rev() {
731 if let Some(child) = function.named_child(index) {
732 let facts = if body == Some(child) {
736 body_facts
737 } else {
738 enclosing_facts
739 };
740 if body == Some(child) {
741 stack.push(WalkFrame::ExitScope);
742 stack.push(WalkFrame::Enter { node: child, facts });
743 stack.push(WalkFrame::EnterScope(
744 function_scope
745 .take()
746 .expect("a function has exactly one body scope"),
747 ));
748 } else {
749 stack.push(WalkFrame::Enter { node: child, facts });
750 }
751 }
752 }
753}
754
755fn merged_enclosing_scope_facts(
756 graph: &PythonGraphSource<'_>,
757 file: &ProjectFile,
758 scope_facts: &HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
759 merged_facts: &mut Vec<LocalBindingsSnapshot<String>>,
760 node: Node<'_>,
761 source: &str,
762 inherited: Option<usize>,
763) -> Option<usize> {
764 let structural_local = collect_function_scope_facts_from_node(node, source);
765 let local = if inherited.is_none() && node.kind() == "function_definition" {
770 enclosing_scope_facts(graph.index, file, scope_facts, node)
771 .cloned()
772 .unwrap_or(structural_local)
773 } else {
774 structural_local
775 };
776 match (local, inherited) {
777 (local, Some(inherited_id)) => {
778 let inherited = merged_facts.get(inherited_id)?;
779 let merged = inherited.merged_with_shadowing(&local);
780 let next_id = merged_facts.len();
781 merged_facts.push(merged);
782 Some(next_id)
783 }
784 (local, None) => {
785 let next_id = merged_facts.len();
786 merged_facts.push(local);
787 Some(next_id)
788 }
789 }
790}
791
792#[derive(Default)]
793struct FunctionScope {
794 locals: HashSet<String>,
795 parameters: HashSet<String>,
796 globals: HashSet<String>,
797}
798
799fn is_shadowed(scopes: &[FunctionScope], name: &str) -> bool {
800 for scope in scopes.iter().rev() {
801 if scope.globals.contains(name) {
802 return false;
803 }
804 if scope.locals.contains(name) {
805 return true;
806 }
807 }
808 false
809}
810
811fn is_receiver_parameter(scopes: &[FunctionScope], name: &str) -> bool {
812 scopes
813 .iter()
814 .rev()
815 .any(|scope| scope.parameters.contains(name))
816}
817
818fn handle_identifier(node: Node<'_>, ctx: &mut PyScan<'_>, scopes: &[FunctionScope]) {
819 if node
821 .parent()
822 .is_some_and(|parent| parent.kind() == "attribute")
823 {
824 return;
825 }
826 if is_declaration_identifier(node) {
827 return;
828 }
829 let text = slice(node, ctx.source);
830 if text.is_empty()
831 || is_shadowed(scopes, text)
832 || python_comprehension_binds_name_at(text, node, ctx.source)
833 || python_type_parameter_binds_name_at(text, node, ctx.source)
834 {
835 return;
836 }
837 if let Some(callee) = ctx.bare_callee(text, node) {
838 ctx.record(callee, node);
839 }
840}
841
842fn handle_annotation_reference(node: Node<'_>, ctx: &mut PyScan<'_>) -> bool {
843 let Some(candidates) =
844 annotation_reference_candidates(ctx.graph, ctx.python, ctx.file, ctx.source, node, false)
845 else {
846 return false;
847 };
848 let [candidate] = candidates.as_slice() else {
849 return !(node.kind() == "attribute" && candidates.is_empty());
850 };
851
852 let site = if node.kind() == "attribute" {
853 node.child_by_field_name("attribute").unwrap_or(node)
854 } else {
855 node
856 };
857 ctx.record(candidate.fq_name(), site);
858 true
859}
860
861fn handle_attribute(
862 node: Node<'_>,
863 ctx: &mut PyScan<'_>,
864 scopes: &[FunctionScope],
865 facts: Option<&LocalBindingsSnapshot<String>>,
866) {
867 let (Some(object), Some(attribute)) = (
868 node.child_by_field_name("object"),
869 node.child_by_field_name("attribute"),
870 ) else {
871 return;
872 };
873 let object_text = slice(object, ctx.source);
874 let attribute_text = slice(attribute, ctx.source);
875 if object_text.is_empty() || attribute_text.is_empty() {
876 return;
877 }
878 if object.kind() == "identifier"
879 && ctx.may_have_target_terminal(attribute_text)
880 && let Some(module) =
881 imported_module_assignment_at(node, object_text, ctx.source, |local| {
882 !is_shadowed(scopes, local)
883 && ctx.namespace.get(local).is_some_and(|binding| {
884 binding.module == "importlib" && binding.consumed_attributes == 0
885 })
886 })
887 {
888 let direct = format!("{module}.{attribute_text}");
889 ctx.record_direct_or_namespace_fallback(direct, attribute);
890 }
891 if object.kind() == "call" && ctx.may_have_target_terminal(attribute_text) {
892 for class in call_result_types(ctx.graph, ctx.python, ctx.file, ctx.source, object, facts) {
893 let direct = format!("{}.{attribute_text}", class.fq_name());
894 let inherited = ctx.graph.hierarchy.map_or_else(Vec::new, |provider| {
895 provider
896 .get_ancestors(&class)
897 .into_iter()
898 .map(|ancestor| format!("{}.{attribute_text}", ancestor.fq_name()))
899 .collect()
900 });
901 ctx.record_direct_or_ancestor_fallback(direct, inherited, attribute);
902 }
903 }
904 if let Some((root, attributes)) = attribute_chain(node) {
908 let root_text = slice(root, ctx.source);
909 if !root_text.is_empty()
910 && !is_shadowed(scopes, root_text)
911 && let Some(binding) = ctx.namespace.get(root_text)
912 {
913 let mut direct = binding.module.clone();
914 let workspace_module = binding.workspace_module;
915 let consumed_attributes = binding.consumed_attributes;
916 if object.kind() == "identifier" {
917 ctx.record(direct.clone(), object);
918 }
919 for member in attributes.into_iter().skip(consumed_attributes) {
920 let member_text = slice(member, ctx.source);
921 if member_text.is_empty() {
922 return;
923 }
924 direct.push('.');
925 direct.push_str(member_text);
926 }
927 if workspace_module {
933 ctx.record_direct_or_namespace_fallback(direct, attribute);
934 } else {
935 ctx.record(direct, attribute);
936 }
937 return;
938 }
939 }
940
941 if let Some(facts) = facts
947 && ctx.may_have_target_terminal(attribute_text)
948 {
949 if matches!(object_text, "self" | "cls") {
950 ctx.record_unproven_name(attribute_text, attribute);
956 } else if let Some(type_fqn) = ctx.receiver_type_fqn(facts, object_text) {
957 ctx.record(format!("{type_fqn}.{attribute_text}"), attribute);
958 } else if object.kind() == "identifier" && !ctx.named.contains_key(object_text) {
959 let resolution = facts.resolution_for(object_text);
960 if resolution.is_ambiguous()
961 || (resolution.is_unknown() && is_receiver_parameter(scopes, object_text))
962 {
963 ctx.record_unproven_name(attribute_text, attribute);
964 }
965 }
966 }
967}
968
969fn handle_keyword_argument(node: Node<'_>, ctx: &mut PyScan<'_>, scopes: &[FunctionScope]) {
970 let (Some(name), Some(arguments)) = (node.child_by_field_name("name"), node.parent()) else {
971 return;
972 };
973 if name.kind() != "identifier" || arguments.kind() != "argument_list" {
974 return;
975 }
976 let Some(call) = arguments.parent().filter(|parent| parent.kind() == "call") else {
977 return;
978 };
979 let Some(function) = call.child_by_field_name("function") else {
980 return;
981 };
982 let member = slice(name, ctx.source);
983 if member.is_empty() || !ctx.may_have_target_terminal(member) {
984 return;
985 }
986 let scoped_class_fqn = if function.kind() == "identifier" {
987 enclosing_scope_facts(ctx.graph.index, ctx.file, ctx.scope_facts, function)
988 .and_then(|facts| ctx.receiver_type_fqn(facts, slice(function, ctx.source)))
989 } else {
990 None
991 };
992 let function_name = (function.kind() == "identifier").then(|| slice(function, ctx.source));
993 let mut default_classes = function_name.map_or_else(Vec::new, |local_name| {
994 resolve_callable_parameter_default_types(
995 ctx.graph, ctx.python, ctx.file, ctx.source, function, local_name,
996 )
997 });
998 let root_shadowed = leftmost_identifier(function)
999 .is_some_and(|root| is_shadowed(scopes, slice(root, ctx.source)));
1000 if !root_shadowed && let Some(local_name) = function_name {
1001 let cutoff = if python_reference_is_deferred_function_body(function) {
1002 usize::MAX
1003 } else {
1004 function.start_byte()
1005 };
1006 default_classes.extend(resolve_visible_named_import_candidates(
1007 ctx.graph,
1008 ctx.python,
1009 ctx.file,
1010 ctx.module_bindings.as_ref(),
1011 local_name,
1012 cutoff,
1013 ));
1014 }
1015 let mut classes = if function_name == Some("cls") {
1016 lexical_class(ctx, function).into_iter().collect()
1017 } else {
1018 if root_shadowed && scoped_class_fqn.is_none() && default_classes.is_empty() {
1019 return;
1020 }
1021 if !root_shadowed {
1022 default_classes.extend(resolve_constructor_types(
1023 ctx.graph, ctx.python, ctx.file, ctx.source, function,
1024 ));
1025 }
1026 default_classes
1027 };
1028 if let Some(fqn) = scoped_class_fqn {
1029 classes.extend(ctx.graph.index.definitions(&fqn).filter(CodeUnit::is_class));
1030 classes.sort();
1031 classes.dedup();
1032 }
1033 for class in classes {
1034 for declaration in resolved_member_declarations(ctx.graph, &class, member) {
1035 ctx.record(declaration.fq_name(), name);
1036 }
1037 }
1038}
1039
1040fn lexical_class(ctx: &PyScan<'_>, node: Node<'_>) -> Option<CodeUnit> {
1041 let range = Range {
1042 start_byte: node.start_byte(),
1043 end_byte: node.end_byte(),
1044 start_line: 0,
1045 end_line: 0,
1046 };
1047 let enclosing = ctx.graph.index.enclosing_code_unit(ctx.file, &range)?;
1048 if enclosing.is_class() {
1049 Some(enclosing)
1050 } else {
1051 ctx.graph
1052 .index
1053 .parent_of(&enclosing)
1054 .filter(CodeUnit::is_class)
1055 }
1056}
1057
1058fn collect_function_scope(func: Node<'_>, source: &str) -> FunctionScope {
1063 let mut scope = FunctionScope::default();
1064 if let Some(params) = func.child_by_field_name("parameters") {
1065 collect_parameter_names(params, source, &mut scope.parameters);
1066 scope.locals.extend(scope.parameters.iter().cloned());
1067 }
1068 if let Some(body) = func.child_by_field_name("body") {
1069 collect_bound_targets(body, source, &mut scope.locals);
1070 collect_scope_globals(body, source, &mut scope.globals);
1071 scope.locals.retain(|name| !scope.globals.contains(name));
1072 }
1073 scope
1074}
1075
1076fn collect_scope_globals(node: Node<'_>, source: &str, out: &mut HashSet<String>) {
1077 let root = node;
1078 let mut stack = vec![node];
1079 while let Some(node) = stack.pop() {
1080 if node.kind() == "global_statement" {
1081 let mut cursor = node.walk();
1082 for child in node.named_children(&mut cursor) {
1083 if child.kind() == "identifier" {
1084 let name = slice(child, source).trim();
1085 if !name.is_empty() {
1086 out.insert(name.to_string());
1087 }
1088 }
1089 }
1090 continue;
1091 }
1092 if node != root
1093 && matches!(
1094 node.kind(),
1095 "function_definition" | "lambda" | "class_definition"
1096 )
1097 {
1098 continue;
1099 }
1100 let mut cursor = node.walk();
1101 let mut children: Vec<_> = node.named_children(&mut cursor).collect();
1102 children.reverse();
1103 stack.extend(children);
1104 }
1105}
1106
1107fn collect_parameter_names(params: Node<'_>, source: &str, out: &mut HashSet<String>) {
1108 let mut cursor = params.walk();
1109 for child in params.named_children(&mut cursor) {
1110 let name = match child.kind() {
1111 "identifier" => Some(child),
1112 _ => child
1115 .child_by_field_name("name")
1116 .or_else(|| child.named_child(0).filter(|n| n.kind() == "identifier")),
1117 };
1118 if let Some(name) = name {
1119 let text = slice(name, source).trim();
1120 if !text.is_empty() {
1121 out.insert(text.to_string());
1122 }
1123 }
1124 }
1125}
1126
1127fn collect_bound_targets(node: Node<'_>, source: &str, out: &mut HashSet<String>) {
1131 let mut stack = vec![node];
1132 while let Some(node) = stack.pop() {
1133 match node.kind() {
1134 "function_definition" | "class_definition" => {
1135 if let Some(name) = node.child_by_field_name("name") {
1136 let text = slice(name, source).trim();
1137 if !text.is_empty() {
1138 out.insert(text.to_string());
1139 }
1140 }
1141 continue;
1142 }
1143 "lambda" => continue,
1144 "assignment" | "augmented_assignment" | "for_statement" | "for_in_clause" => {
1145 if let Some(left) = node.child_by_field_name("left") {
1146 collect_assigned_identifiers(left, source, out);
1147 }
1148 }
1149 "named_expression" => {
1150 if let Some(name) = node.child_by_field_name("name") {
1151 collect_assigned_identifiers(name, source, out);
1152 }
1153 }
1154 _ => {}
1155 }
1156 let mut cursor = node.walk();
1157 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
1158 children.reverse();
1159 stack.extend(children);
1160 }
1161}