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