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::hash::{HashMap, HashSet};
48use std::sync::{Arc, Mutex};
49use tree_sitter::Node;
50
51pub struct PythonEdgeScan<'a> {
58 targets: Option<&'a HashSet<String>>,
59 targets_by_terminal: HashMap<String, Vec<String>>,
60 canonical_namespace_candidates: Mutex<HashMap<String, Arc<Vec<String>>>>,
61}
62
63impl<'a> PythonEdgeScan<'a> {
64 pub fn new(nodes: &HashSet<String>, targets: &'a HashSet<String>) -> Self {
67 debug_assert!(targets.is_subset(nodes));
68 let mut targets_by_terminal: HashMap<String, Vec<String>> = HashMap::default();
69 for target in targets {
70 let terminal = parse_symbol_path(Language::Python, target)
75 .pop()
76 .unwrap_or_else(|| target.clone());
77 targets_by_terminal
78 .entry(terminal)
79 .or_default()
80 .push(target.clone());
81 }
82 Self {
83 targets: Some(targets),
84 targets_by_terminal,
85 canonical_namespace_candidates: Mutex::new(HashMap::default()),
86 }
87 }
88
89 pub fn new_rooted() -> Self {
93 Self {
94 targets: None,
95 targets_by_terminal: HashMap::default(),
96 canonical_namespace_candidates: Mutex::new(HashMap::default()),
97 }
98 }
99
100 pub fn scan_file(
106 &self,
107 graph: &PythonGraphSource<'_>,
108 python: &dyn PythonUsageSource,
109 file: &ProjectFile,
110 input: &FileEdgeScanInput<'_>,
111 ) -> Result<PerFileEdges, ProjectFile> {
112 let source = input.source;
113 let canonical_imports = python.import_info_of(graph.token, file);
114
115 let binder = python.import_binder_of(file);
120 let mut named: HashMap<String, String> = HashMap::default();
121 let mut namespace: HashMap<String, NamespaceBinding> = HashMap::default();
122 for (local, binding) in &binder.bindings {
123 match binding.kind {
124 ImportKind::Named => {
125 if let Some(imported) = &binding.imported_name {
126 let module = canonical_import_module_fqn(
127 graph,
128 python,
129 file,
130 &binding.module_specifier,
131 )
132 .unwrap_or_else(|| binding.module_specifier.clone());
133 let imported_fqn = if module.ends_with('.') {
134 format!("{module}{imported}")
135 } else {
136 format!("{module}.{imported}")
137 };
138 named.insert(local.clone(), imported_fqn);
143 }
144 }
145 ImportKind::Namespace => {
146 let direct_module = binding.module_specifier.clone();
147 let imported_module = binding
148 .namespace_imported_module
149 .as_deref()
150 .unwrap_or(&direct_module);
151 let module = canonical_import_module_fqn(graph, python, file, imported_module);
152 let workspace_module = module.is_some();
153 let consumed_attributes = module.as_ref().map_or(0, |_| {
154 let imported_segments =
155 parse_symbol_path(Language::Python, imported_module);
156 let bound_segments = parse_symbol_path(Language::Python, &direct_module);
157 imported_segments.len().saturating_sub(bound_segments.len())
158 });
159 let canonical_module = module.unwrap_or(direct_module);
160 let mut root_segments = parse_symbol_path(Language::Python, &canonical_module);
161 root_segments.truncate(root_segments.len().saturating_sub(consumed_attributes));
162 namespace.insert(
163 local.clone(),
164 NamespaceBinding {
165 root_module: root_segments.join("."),
166 module: canonical_module,
167 workspace_module,
168 consumed_attributes,
169 },
170 );
171 }
172 ImportKind::Default | ImportKind::CommonJsRequire | ImportKind::Glob => {}
173 }
174 }
175 let same_file: HashMap<String, String> = graph
176 .index
177 .declarations(file)
178 .into_iter()
179 .map(|unit| (unit.identifier().to_string(), unit.fq_name()))
180 .collect();
181 let module_bindings = usage_module_binding_timeline(python, file, || {
182 super::extractor::collect_module_binding_timeline(
183 input.root(),
184 source,
185 &canonical_imports,
186 )
187 });
188
189 let Some(scope_facts) = usage_scope_facts(python, file, || {
193 collect_scope_facts_from_parsed_source(graph, python, file, source, input.root())
194 }) else {
195 return Err(file.clone());
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 Ok(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 python_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 self.record_if_accepted(callee, kind, start, end);
467 }
468
469 fn record_if_accepted(
470 &mut self,
471 callee: String,
472 kind: UsageReferenceKind,
473 start: usize,
474 end: usize,
475 ) -> bool {
476 if !self.accepts_target(&callee) {
477 return false;
478 }
479 self.edges.record_kind(self.input, callee, kind, start, end);
480 true
481 }
482
483 fn record_namespace_fallback(
486 &mut self,
487 direct: String,
488 kind: UsageReferenceKind,
489 start: usize,
490 end: usize,
491 ) {
492 if self.accepts_target(&direct) {
493 self.edges.record_kind(self.input, direct, kind, start, end);
494 return;
495 }
496 for resolved in self.canonical_namespace_candidates(&direct).iter() {
500 self.record_direct(resolved.clone(), kind, start, end);
501 }
502 }
503
504 fn record_ancestor_fallback(
507 &mut self,
508 direct: String,
509 inherited: Vec<String>,
510 kind: UsageReferenceKind,
511 start: usize,
512 end: usize,
513 ) {
514 if self.record_if_accepted(direct, kind, start, end) {
515 return;
516 }
517 for candidate in inherited {
518 self.record_if_accepted(candidate, kind, start, end);
519 }
520 }
521
522 fn resolve_pending(&mut self) {
526 if self.pending.is_empty() {
527 return;
528 }
529 let mut names = Vec::new();
530 for item in &self.pending {
531 match item {
532 PendingCallee::Direct { callee, .. } => names.push(callee.clone()),
533 PendingCallee::WithNamespaceFallback { direct, .. } => names.push(direct.clone()),
534 PendingCallee::WithAncestorFallback {
535 direct, inherited, ..
536 } => {
537 names.push(direct.clone());
538 names.extend(inherited.iter().cloned());
539 }
540 }
541 }
542 self.graph.index.prefetch_definitions(&names);
543 for item in std::mem::take(&mut self.pending) {
544 match item {
545 PendingCallee::Direct {
546 callee,
547 kind,
548 start,
549 end,
550 } => self.record_direct(callee, kind, start, end),
551 PendingCallee::WithNamespaceFallback {
552 direct,
553 kind,
554 start,
555 end,
556 } => self.record_namespace_fallback(direct, kind, start, end),
557 PendingCallee::WithAncestorFallback {
558 direct,
559 inherited,
560 kind,
561 start,
562 end,
563 } => self.record_ancestor_fallback(direct, inherited, kind, start, end),
564 }
565 }
566 }
567
568 fn record_unproven_name(&mut self, name: &str, node: Node<'_>) {
569 self.edges
570 .record_unproven_name_at_node(self.input, name, node);
571 }
572
573 fn accepts_target(&self, fqn: &str) -> bool {
574 self.targets.map_or_else(
575 || self.graph.index.definitions(fqn).next().is_some(),
576 |targets| targets.contains(fqn),
577 )
578 }
579
580 fn may_have_target_terminal(&self, terminal: &str) -> bool {
581 self.targets.is_none() || self.targets_by_terminal.contains_key(terminal)
582 }
583
584 fn canonical_namespace_candidates(&self, direct: &str) -> Arc<Vec<String>> {
585 if let Some(cached) = self
586 .canonical_namespace_candidates
587 .lock()
588 .expect("Python namespace candidate cache mutex poisoned")
589 .get(direct)
590 .cloned()
591 {
592 return cached;
593 }
594
595 let resolved: Arc<Vec<String>> = Arc::new(
596 resolve_fqn_candidates(self.python, None, direct, |name| {
597 self.graph.index.definitions(name).collect()
598 })
599 .into_iter()
600 .map(|unit| unit.fq_name())
601 .collect(),
602 );
603 self.canonical_namespace_candidates
604 .lock()
605 .expect("Python namespace candidate cache mutex poisoned")
606 .entry(direct.to_string())
607 .or_insert_with(|| resolved.clone())
608 .clone()
609 }
610}
611
612fn scan_tree(root: Node<'_>, ctx: &mut PyScan<'_>) {
613 let mut scopes: Vec<FunctionScope> = Vec::new();
616 walk(root, ctx, &mut scopes, None);
617}
618
619fn walk(
620 node: Node<'_>,
621 ctx: &mut PyScan<'_>,
622 scopes: &mut Vec<FunctionScope>,
623 facts: Option<usize>,
624) {
625 let mut merged_facts = Vec::new();
626 let mut stack = vec![WalkFrame::Enter { node, facts }];
627 while let Some(frame) = stack.pop() {
628 match frame {
629 WalkFrame::Enter { node, facts } => match node.kind() {
630 "import_statement" | "import_from_statement" => {}
631 "function_definition" | "lambda" => {
635 let function_scope = collect_function_scope(node, ctx.source);
636 let scope_facts = merged_enclosing_scope_facts(
637 ctx.graph,
638 ctx.file,
639 ctx.scope_facts,
640 &mut merged_facts,
641 node,
642 ctx.source,
643 facts,
644 );
645 push_function_children(node, facts, scope_facts, function_scope, &mut stack);
646 }
647 "class_definition" => push_children(node, None, &mut stack),
650 "identifier" => {
651 if !handle_annotation_reference(node, ctx) {
652 handle_identifier(node, ctx, scopes);
653 }
654 push_children(node, facts, &mut stack);
655 }
656 "attribute" => {
657 if handle_annotation_reference(node, ctx) {
658 continue;
659 }
660 let scope_facts = facts.and_then(|id| merged_facts.get(id));
661 handle_attribute(node, ctx, scopes, scope_facts);
662 push_children(node, facts, &mut stack);
663 }
664 "string_content" => {
665 handle_annotation_reference(node, ctx);
666 }
667 "keyword_argument" => {
668 handle_keyword_argument(node, ctx, scopes);
669 if let Some(value) = node.child_by_field_name("value") {
670 stack.push(WalkFrame::Enter { node: value, facts });
671 }
672 }
673 _ => push_children(node, facts, &mut stack),
674 },
675 WalkFrame::ExitScope => {
676 scopes.pop();
677 }
678 WalkFrame::EnterScope(scope) => scopes.push(scope),
679 }
680 }
681}
682
683enum WalkFrame<'tree> {
684 Enter {
685 node: Node<'tree>,
686 facts: Option<usize>,
687 },
688 EnterScope(FunctionScope),
689 ExitScope,
690}
691
692fn push_children<'tree>(
693 node: Node<'tree>,
694 facts: Option<usize>,
695 stack: &mut Vec<WalkFrame<'tree>>,
696) {
697 brokk_bifrost_core::analyzer::tree_walk::push_named_children_reversed_as(node, stack, |node| {
698 WalkFrame::Enter { node, facts }
699 });
700}
701
702fn push_function_children<'tree>(
703 function: Node<'tree>,
704 enclosing_facts: Option<usize>,
705 body_facts: Option<usize>,
706 function_scope: FunctionScope,
707 stack: &mut Vec<WalkFrame<'tree>>,
708) {
709 let body = function.child_by_field_name("body");
710 let mut function_scope = Some(function_scope);
711 for index in (0..function.named_child_count()).rev() {
712 if let Some(child) = function.named_child(index) {
713 let facts = if body == Some(child) {
717 body_facts
718 } else {
719 enclosing_facts
720 };
721 if body == Some(child) {
722 stack.push(WalkFrame::ExitScope);
723 stack.push(WalkFrame::Enter { node: child, facts });
724 stack.push(WalkFrame::EnterScope(
725 function_scope
726 .take()
727 .expect("a function has exactly one body scope"),
728 ));
729 } else {
730 stack.push(WalkFrame::Enter { node: child, facts });
731 }
732 }
733 }
734}
735
736fn merged_enclosing_scope_facts(
737 graph: &PythonGraphSource<'_>,
738 file: &ProjectFile,
739 scope_facts: &HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
740 merged_facts: &mut Vec<LocalBindingsSnapshot<String>>,
741 node: Node<'_>,
742 source: &str,
743 inherited: Option<usize>,
744) -> Option<usize> {
745 let structural_local = collect_function_scope_facts_from_node(node, source);
746 let local = if inherited.is_none() && node.kind() == "function_definition" {
751 enclosing_scope_facts(graph.index, file, scope_facts, node)
752 .cloned()
753 .unwrap_or(structural_local)
754 } else {
755 structural_local
756 };
757 match (local, inherited) {
758 (local, Some(inherited_id)) => {
759 let inherited = merged_facts.get(inherited_id)?;
760 let merged = inherited.merged_with_shadowing(&local);
761 let next_id = merged_facts.len();
762 merged_facts.push(merged);
763 Some(next_id)
764 }
765 (local, None) => {
766 let next_id = merged_facts.len();
767 merged_facts.push(local);
768 Some(next_id)
769 }
770 }
771}
772
773#[derive(Default)]
774struct FunctionScope {
775 locals: HashSet<String>,
776 parameters: HashSet<String>,
777 globals: HashSet<String>,
778}
779
780fn is_shadowed(scopes: &[FunctionScope], name: &str) -> bool {
781 for scope in scopes.iter().rev() {
782 if scope.globals.contains(name) {
783 return false;
784 }
785 if scope.locals.contains(name) {
786 return true;
787 }
788 }
789 false
790}
791
792fn is_receiver_parameter(scopes: &[FunctionScope], name: &str) -> bool {
793 scopes
794 .iter()
795 .rev()
796 .any(|scope| scope.parameters.contains(name))
797}
798
799fn handle_identifier(node: Node<'_>, ctx: &mut PyScan<'_>, scopes: &[FunctionScope]) {
800 if node
802 .parent()
803 .is_some_and(|parent| parent.kind() == "attribute")
804 {
805 return;
806 }
807 if is_declaration_identifier(node) {
808 return;
809 }
810 let text = slice(node, ctx.source);
811 if text.is_empty()
812 || is_shadowed(scopes, text)
813 || python_comprehension_binds_name_at(text, node, ctx.source)
814 || python_type_parameter_binds_name_at(text, node, ctx.source)
815 {
816 return;
817 }
818 if let Some(callee) = ctx.bare_callee(text, node) {
819 ctx.record(callee, node);
820 }
821}
822
823fn handle_annotation_reference(node: Node<'_>, ctx: &mut PyScan<'_>) -> bool {
824 let Some(candidates) =
825 annotation_reference_candidates(ctx.graph, ctx.python, ctx.file, ctx.source, node, false)
826 else {
827 return false;
828 };
829 let [candidate] = candidates.as_slice() else {
830 return !(node.kind() == "attribute" && candidates.is_empty());
831 };
832
833 let site = if node.kind() == "attribute" {
834 node.child_by_field_name("attribute").unwrap_or(node)
835 } else {
836 node
837 };
838 ctx.record(candidate.fq_name(), site);
839 true
840}
841
842fn handle_attribute(
843 node: Node<'_>,
844 ctx: &mut PyScan<'_>,
845 scopes: &[FunctionScope],
846 facts: Option<&LocalBindingsSnapshot<String>>,
847) {
848 let (Some(object), Some(attribute)) = (
849 node.child_by_field_name("object"),
850 node.child_by_field_name("attribute"),
851 ) else {
852 return;
853 };
854 let object_text = slice(object, ctx.source);
855 let attribute_text = slice(attribute, ctx.source);
856 if object_text.is_empty() || attribute_text.is_empty() {
857 return;
858 }
859 if object.kind() == "identifier"
860 && ctx.may_have_target_terminal(attribute_text)
861 && let Some(module) =
862 imported_module_assignment_at(node, object_text, ctx.source, |local| {
863 !is_shadowed(scopes, local)
864 && ctx.namespace.get(local).is_some_and(|binding| {
865 binding.module == "importlib" && binding.consumed_attributes == 0
866 })
867 })
868 {
869 let direct = format!("{module}.{attribute_text}");
870 ctx.record_direct_or_namespace_fallback(direct, attribute);
871 }
872 if object.kind() == "call" && ctx.may_have_target_terminal(attribute_text) {
873 for class in call_result_types(ctx.graph, ctx.python, ctx.file, ctx.source, object, facts) {
874 let direct = format!("{}.{attribute_text}", class.fq_name());
875 let inherited = ctx.graph.hierarchy.map_or_else(Vec::new, |provider| {
876 provider
877 .get_ancestors(&class)
878 .into_iter()
879 .map(|ancestor| format!("{}.{attribute_text}", ancestor.fq_name()))
880 .collect()
881 });
882 ctx.record_direct_or_ancestor_fallback(direct, inherited, attribute);
883 }
884 }
885 if let Some((root, attributes)) = attribute_chain(node) {
889 let root_text = slice(root, ctx.source);
890 if !root_text.is_empty()
891 && !is_shadowed(scopes, root_text)
892 && let Some(binding) = ctx.namespace.get(root_text)
893 {
894 let mut direct = binding.module.clone();
895 let workspace_module = binding.workspace_module;
896 let consumed_attributes = binding.consumed_attributes;
897 if object.kind() == "identifier" {
898 ctx.record(direct.clone(), object);
899 }
900 for member in attributes.into_iter().skip(consumed_attributes) {
901 let member_text = slice(member, ctx.source);
902 if member_text.is_empty() {
903 return;
904 }
905 direct.push('.');
906 direct.push_str(member_text);
907 }
908 if workspace_module {
914 ctx.record_direct_or_namespace_fallback(direct, attribute);
915 } else {
916 ctx.record(direct, attribute);
917 }
918 return;
919 }
920 }
921
922 if let Some(facts) = facts
928 && ctx.may_have_target_terminal(attribute_text)
929 {
930 if matches!(object_text, "self" | "cls") {
931 ctx.record_unproven_name(attribute_text, attribute);
937 } else if let Some(type_fqn) = ctx.receiver_type_fqn(facts, object_text) {
938 ctx.record(format!("{type_fqn}.{attribute_text}"), attribute);
939 } else if object.kind() == "identifier" && !ctx.named.contains_key(object_text) {
940 let resolution = facts.resolution_for(object_text);
941 if resolution.is_ambiguous()
942 || (resolution.is_unknown() && is_receiver_parameter(scopes, object_text))
943 {
944 ctx.record_unproven_name(attribute_text, attribute);
945 }
946 }
947 }
948}
949
950fn handle_keyword_argument(node: Node<'_>, ctx: &mut PyScan<'_>, scopes: &[FunctionScope]) {
951 let (Some(name), Some(arguments)) = (node.child_by_field_name("name"), node.parent()) else {
952 return;
953 };
954 if name.kind() != "identifier" || arguments.kind() != "argument_list" {
955 return;
956 }
957 let Some(call) = arguments.parent().filter(|parent| parent.kind() == "call") else {
958 return;
959 };
960 let Some(function) = call.child_by_field_name("function") else {
961 return;
962 };
963 let member = slice(name, ctx.source);
964 if member.is_empty() || !ctx.may_have_target_terminal(member) {
965 return;
966 }
967 let scoped_class_fqn = if function.kind() == "identifier" {
968 enclosing_scope_facts(ctx.graph.index, ctx.file, ctx.scope_facts, function)
969 .and_then(|facts| ctx.receiver_type_fqn(facts, slice(function, ctx.source)))
970 } else {
971 None
972 };
973 let function_name = (function.kind() == "identifier").then(|| slice(function, ctx.source));
974 let mut default_classes = function_name.map_or_else(Vec::new, |local_name| {
975 resolve_callable_parameter_default_types(
976 ctx.graph, ctx.python, ctx.file, ctx.source, function, local_name,
977 )
978 });
979 let root_shadowed = leftmost_identifier(function)
980 .is_some_and(|root| is_shadowed(scopes, slice(root, ctx.source)));
981 if !root_shadowed && let Some(local_name) = function_name {
982 let cutoff = if python_reference_is_deferred_function_body(function) {
983 usize::MAX
984 } else {
985 function.start_byte()
986 };
987 default_classes.extend(resolve_visible_named_import_candidates(
988 ctx.graph,
989 ctx.python,
990 ctx.file,
991 ctx.module_bindings.as_ref(),
992 local_name,
993 cutoff,
994 ));
995 }
996 let mut classes = if function_name == Some("cls") {
997 lexical_class(ctx, function).into_iter().collect()
998 } else {
999 if root_shadowed && scoped_class_fqn.is_none() && default_classes.is_empty() {
1000 return;
1001 }
1002 if !root_shadowed {
1003 default_classes.extend(resolve_constructor_types(
1004 ctx.graph, ctx.python, ctx.file, ctx.source, function,
1005 ));
1006 }
1007 default_classes
1008 };
1009 if let Some(fqn) = scoped_class_fqn {
1010 classes.extend(ctx.graph.index.definitions(&fqn).filter(CodeUnit::is_class));
1011 classes.sort();
1012 classes.dedup();
1013 }
1014 for class in classes {
1015 for declaration in resolved_member_declarations(ctx.graph, &class, member) {
1016 ctx.record(declaration.fq_name(), name);
1017 }
1018 }
1019}
1020
1021fn lexical_class(ctx: &PyScan<'_>, node: Node<'_>) -> Option<CodeUnit> {
1022 let range = Range {
1023 start_byte: node.start_byte(),
1024 end_byte: node.end_byte(),
1025 start_line: 0,
1026 end_line: 0,
1027 };
1028 let enclosing = ctx.graph.index.enclosing_code_unit(ctx.file, &range)?;
1029 if enclosing.is_class() {
1030 Some(enclosing)
1031 } else {
1032 ctx.graph
1033 .index
1034 .parent_of(&enclosing)
1035 .filter(CodeUnit::is_class)
1036 }
1037}
1038
1039fn collect_function_scope(func: Node<'_>, source: &str) -> FunctionScope {
1044 let mut scope = FunctionScope::default();
1045 if let Some(params) = func.child_by_field_name("parameters") {
1046 collect_parameter_names(params, source, &mut scope.parameters);
1047 scope.locals.extend(scope.parameters.iter().cloned());
1048 }
1049 if let Some(body) = func.child_by_field_name("body") {
1050 collect_bound_targets(body, source, &mut scope.locals);
1051 collect_scope_globals(body, source, &mut scope.globals);
1052 scope.locals.retain(|name| !scope.globals.contains(name));
1053 }
1054 scope
1055}
1056
1057fn collect_scope_globals(node: Node<'_>, source: &str, out: &mut HashSet<String>) {
1058 let root = node;
1059 let mut stack = vec![node];
1060 while let Some(node) = stack.pop() {
1061 if node.kind() == "global_statement" {
1062 let mut cursor = node.walk();
1063 for child in node.named_children(&mut cursor) {
1064 if child.kind() == "identifier" {
1065 let name = slice(child, source).trim();
1066 if !name.is_empty() {
1067 out.insert(name.to_string());
1068 }
1069 }
1070 }
1071 continue;
1072 }
1073 if node != root
1074 && matches!(
1075 node.kind(),
1076 "function_definition" | "lambda" | "class_definition"
1077 )
1078 {
1079 continue;
1080 }
1081 let mut cursor = node.walk();
1082 let mut children: Vec<_> = node.named_children(&mut cursor).collect();
1083 children.reverse();
1084 stack.extend(children);
1085 }
1086}
1087
1088fn collect_parameter_names(params: Node<'_>, source: &str, out: &mut HashSet<String>) {
1089 let mut cursor = params.walk();
1090 for child in params.named_children(&mut cursor) {
1091 let name = match child.kind() {
1092 "identifier" => Some(child),
1093 _ => child
1096 .child_by_field_name("name")
1097 .or_else(|| child.named_child(0).filter(|n| n.kind() == "identifier")),
1098 };
1099 if let Some(name) = name {
1100 let text = slice(name, source).trim();
1101 if !text.is_empty() {
1102 out.insert(text.to_string());
1103 }
1104 }
1105 }
1106}
1107
1108fn collect_bound_targets(node: Node<'_>, source: &str, out: &mut HashSet<String>) {
1112 let mut stack = vec![node];
1113 while let Some(node) = stack.pop() {
1114 match node.kind() {
1115 "function_definition" | "class_definition" => {
1116 if let Some(name) = node.child_by_field_name("name") {
1117 let text = slice(name, source).trim();
1118 if !text.is_empty() {
1119 out.insert(text.to_string());
1120 }
1121 }
1122 continue;
1123 }
1124 "lambda" => continue,
1125 "assignment" | "augmented_assignment" | "for_statement" | "for_in_clause" => {
1126 if let Some(left) = node.child_by_field_name("left") {
1127 collect_assigned_identifiers(left, source, out);
1128 }
1129 }
1130 "named_expression" => {
1131 if let Some(name) = node.child_by_field_name("name") {
1132 collect_assigned_identifiers(name, source, out);
1133 }
1134 }
1135 _ => {}
1136 }
1137 let mut cursor = node.walk();
1138 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
1139 children.reverse();
1140 stack.extend(children);
1141 }
1142}