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