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, slice,
22};
23use super::resolver::{
24 annotation_reference_candidates, resolve_callable_parameter_default_types,
25 resolve_constructor_types, resolve_receiver_type, resolve_visible_named_import_candidates,
26 resolved_member_declarations,
27};
28use crate::bindings::{python_comprehension_binds_name_at, python_type_parameter_binds_name_at};
29use crate::graph::PythonGraphSource;
30use crate::graph_support::PythonUsageSource;
31use crate::imports::{imported_module_assignment_at, resolve_fqn_candidates};
32use crate::syntax::is_declaration_identifier;
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
52type CanonicalNamespaceCandidates = HashMap<([u8; 32], String), Arc<Vec<String>>>;
53
54pub struct PythonEdgeScan<'a> {
61 targets: Option<&'a HashSet<String>>,
62 targets_by_terminal: HashMap<String, Vec<String>>,
63 canonical_namespace_candidates: Mutex<CanonicalNamespaceCandidates>,
64}
65
66impl<'a> PythonEdgeScan<'a> {
67 pub fn new(nodes: &HashSet<String>, targets: &'a HashSet<String>) -> Self {
70 debug_assert!(targets.is_subset(nodes));
71 let mut targets_by_terminal: HashMap<String, Vec<String>> = HashMap::default();
72 for target in targets {
73 let terminal = parse_symbol_path(Language::Python, target)
78 .pop()
79 .unwrap_or_else(|| target.clone());
80 targets_by_terminal
81 .entry(terminal)
82 .or_default()
83 .push(target.clone());
84 }
85 Self {
86 targets: Some(targets),
87 targets_by_terminal,
88 canonical_namespace_candidates: Mutex::new(HashMap::default()),
89 }
90 }
91
92 pub fn new_rooted() -> Self {
96 Self {
97 targets: None,
98 targets_by_terminal: HashMap::default(),
99 canonical_namespace_candidates: Mutex::new(HashMap::default()),
100 }
101 }
102
103 pub fn scan_file(
109 &self,
110 graph: &PythonGraphSource<'_>,
111 python: &dyn PythonUsageSource,
112 file: &ProjectFile,
113 input: &FileEdgeScanInput<'_>,
114 cancellation: &CancellationToken,
115 ) -> Result<PerFileEdges, ProjectFile> {
116 let source = input.source;
117 let canonical_imports = python.import_info_of(graph.token, file);
118
119 let binder = python.import_binder_of(file);
124 let mut named: HashMap<String, String> = HashMap::default();
125 let mut namespace: HashMap<String, NamespaceBinding> = HashMap::default();
126 for (local, binding) in &binder.bindings {
127 if cancellation.is_cancelled() {
128 return Err(file.clone());
129 }
130 match binding.kind {
131 ImportKind::Named => {
132 if let Some(imported) = &binding.imported_name {
133 let module = canonical_import_module_fqn(
134 graph,
135 python,
136 file,
137 &binding.module_specifier,
138 )
139 .unwrap_or_else(|| binding.module_specifier.clone());
140 let imported_fqn = if module.ends_with('.') {
141 format!("{module}{imported}")
142 } else {
143 format!("{module}.{imported}")
144 };
145 named.insert(local.clone(), imported_fqn);
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 cancellation,
207 graph,
208 python,
209 targets: self.targets,
210 targets_by_terminal: &self.targets_by_terminal,
211 file,
212 source,
213 named,
214 namespace,
215 same_file,
216 module_bindings,
217 scope_facts: scope_facts.as_ref(),
218 canonical_namespace_candidates: &self.canonical_namespace_candidates,
219 input,
220 edges: PerFileEdges::default(),
221 pending: Vec::new(),
222 };
223 scan_tree(input.root(), &mut ctx);
224 ctx.resolve_pending();
225 if cancellation.is_cancelled() {
226 return Err(file.clone());
227 }
228 Ok(ctx.edges)
229 }
230}
231
232fn reference_span(node: Node<'_>) -> (UsageReferenceKind, usize, usize) {
233 (
234 classify_reference_node(node),
235 node.start_byte(),
236 node.end_byte(),
237 )
238}
239
240fn canonical_import_module_fqn(
241 graph: &PythonGraphSource<'_>,
242 python: &dyn PythonUsageSource,
243 importing_file: &ProjectFile,
244 module_specifier: &str,
245) -> Option<String> {
246 let resolved = usage_resolve_module_files(python, importing_file, module_specifier);
247 let [module_file] = resolved.as_slice() else {
248 return None;
249 };
250 graph
251 .index
252 .declarations(module_file)
253 .into_iter()
254 .find(CodeUnit::is_module)
255 .map(|module| module.fq_name())
256}
257
258struct PyScan<'a> {
259 cancellation: &'a CancellationToken,
260 graph: &'a PythonGraphSource<'a>,
261 python: &'a dyn PythonUsageSource,
262 targets: Option<&'a HashSet<String>>,
263 targets_by_terminal: &'a HashMap<String, Vec<String>>,
264 file: &'a ProjectFile,
265 source: &'a str,
266 named: HashMap<String, String>,
267 namespace: HashMap<String, NamespaceBinding>,
268 same_file: HashMap<String, String>,
269 module_bindings: Arc<ModuleBindingTimeline>,
270 scope_facts: &'a HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
271 canonical_namespace_candidates: &'a Mutex<CanonicalNamespaceCandidates>,
272 input: &'a FileEdgeScanInput<'a>,
273 edges: PerFileEdges,
274 pending: Vec<PendingCallee>,
279}
280
281enum PendingCallee {
286 Direct {
288 callee: String,
289 kind: UsageReferenceKind,
290 start: usize,
291 end: usize,
292 },
293 WithNamespaceFallback {
296 direct: String,
297 kind: UsageReferenceKind,
298 start: usize,
299 end: usize,
300 },
301 WithAncestorFallback {
304 direct: String,
305 inherited: Vec<String>,
306 kind: UsageReferenceKind,
307 start: usize,
308 end: usize,
309 },
310}
311
312struct NamespaceBinding {
313 root_module: String,
314 module: String,
315 workspace_module: bool,
316 consumed_attributes: usize,
317}
318
319impl PyScan<'_> {
320 fn bare_callee(&self, text: &str, node: Node<'_>) -> Option<String> {
323 if let Some(fqn) = self.named.get(text) {
324 return Some(fqn.clone());
325 }
326 if let Some(fqn) = self.namespace.get(text) {
327 return self
328 .visible_namespace_root(text, node)
329 .or_else(|| Some(fqn.root_module.clone()));
330 }
331 if let Some(fqn) = self.same_file.get(text) {
332 return Some(fqn.clone());
333 }
334 None
335 }
336
337 fn visible_namespace_root(&self, local: &str, node: Node<'_>) -> Option<String> {
338 let events = self.module_bindings.get(local)?;
339 let cutoff = if python_reference_is_deferred_function_body(node) {
340 usize::MAX
341 } else {
342 node.start_byte()
343 };
344 let visible: Vec<_> = events
345 .iter()
346 .filter(|event| event.visible_from <= cutoff)
347 .collect();
348 let start = visible
349 .iter()
350 .rposition(|event| {
351 if event.conditional {
352 return false;
353 }
354 match &event.kind {
355 ModuleBindingEventKind::ImportModule {
356 module,
357 consumed_attributes,
358 } => *consumed_attributes == 0 && module != local,
359 ModuleBindingEventKind::FromImport { .. } | ModuleBindingEventKind::Other => {
360 true
361 }
362 }
363 })
364 .unwrap_or(0);
365 let mut roots = visible[start..]
366 .iter()
367 .filter_map(|event| match &event.kind {
368 ModuleBindingEventKind::ImportModule {
369 module,
370 consumed_attributes,
371 } => {
372 let mut segments = parse_symbol_path(Language::Python, module);
373 segments.truncate(segments.len().saturating_sub(*consumed_attributes));
374 let root = segments.join(".");
375 canonical_import_module_fqn(self.graph, self.python, self.file, &root)
376 .or(Some(root))
377 }
378 ModuleBindingEventKind::FromImport { .. } | ModuleBindingEventKind::Other => None,
379 })
380 .collect::<Vec<_>>();
381 roots.sort();
382 roots.dedup();
383 (roots.len() == 1).then(|| roots.remove(0))
384 }
385
386 fn receiver_type_fqn(
390 &self,
391 facts: &LocalBindingsSnapshot<String>,
392 receiver: &str,
393 ) -> Option<String> {
394 let resolution = facts.resolution_for(receiver);
395 let type_name = resolution
396 .as_precise()
397 .and_then(|targets| targets.iter().next())?;
398 resolve_receiver_type(self.graph, self.python, self.file, type_name, false)
404 .map(|unit| unit.fq_name())
405 }
406
407 fn record(&mut self, callee: String, node: Node<'_>) {
408 let (kind, start, end) = reference_span(node);
409 if self.targets.is_none() {
416 self.pending.push(PendingCallee::Direct {
417 callee,
418 kind,
419 start,
420 end,
421 });
422 return;
423 }
424 self.record_direct(callee, kind, start, end);
425 }
426
427 fn record_direct_or_namespace_fallback(&mut self, direct: String, node: Node<'_>) {
432 let (kind, start, end) = reference_span(node);
433 if self.targets.is_none() {
434 self.pending.push(PendingCallee::WithNamespaceFallback {
435 direct,
436 kind,
437 start,
438 end,
439 });
440 return;
441 }
442 self.record_namespace_fallback(direct, kind, start, end);
443 }
444
445 fn record_direct_or_ancestor_fallback(
449 &mut self,
450 direct: String,
451 inherited: Vec<String>,
452 node: Node<'_>,
453 ) {
454 let (kind, start, end) = reference_span(node);
455 if self.targets.is_none() {
456 self.pending.push(PendingCallee::WithAncestorFallback {
457 direct,
458 inherited,
459 kind,
460 start,
461 end,
462 });
463 return;
464 }
465 self.record_ancestor_fallback(direct, inherited, kind, start, end);
466 }
467
468 fn record_direct(
472 &mut self,
473 callee: String,
474 kind: UsageReferenceKind,
475 start: usize,
476 end: usize,
477 ) {
478 self.record_if_accepted(callee, kind, start, end);
479 }
480
481 fn record_if_accepted(
482 &mut self,
483 callee: String,
484 kind: UsageReferenceKind,
485 start: usize,
486 end: usize,
487 ) -> bool {
488 if !self.accepts_target(&callee) {
489 return false;
490 }
491 self.edges.record_kind(self.input, callee, kind, start, end);
492 true
493 }
494
495 fn record_namespace_fallback(
498 &mut self,
499 direct: String,
500 kind: UsageReferenceKind,
501 start: usize,
502 end: usize,
503 ) {
504 if self.accepts_target(&direct) {
505 self.edges.record_kind(self.input, direct, kind, start, end);
506 return;
507 }
508 for resolved in self.canonical_namespace_candidates(&direct).iter() {
512 self.record_direct(resolved.clone(), kind, start, end);
513 }
514 }
515
516 fn record_ancestor_fallback(
519 &mut self,
520 direct: String,
521 inherited: Vec<String>,
522 kind: UsageReferenceKind,
523 start: usize,
524 end: usize,
525 ) {
526 if self.record_if_accepted(direct, kind, start, end) {
527 return;
528 }
529 for candidate in inherited {
530 self.record_if_accepted(candidate, kind, start, end);
531 }
532 }
533
534 fn resolve_pending(&mut self) {
538 if self.pending.is_empty() {
539 return;
540 }
541 let mut names = Vec::new();
542 for item in &self.pending {
543 if self.cancellation.is_cancelled() {
544 return;
545 }
546 match item {
547 PendingCallee::Direct { callee, .. } => names.push(callee.clone()),
548 PendingCallee::WithNamespaceFallback { direct, .. } => names.push(direct.clone()),
549 PendingCallee::WithAncestorFallback {
550 direct, inherited, ..
551 } => {
552 names.push(direct.clone());
553 names.extend(inherited.iter().cloned());
554 }
555 }
556 }
557 self.graph.index.prefetch_definitions(&names);
558 for item in std::mem::take(&mut self.pending) {
559 if self.cancellation.is_cancelled() {
560 return;
561 }
562 match item {
563 PendingCallee::Direct {
564 callee,
565 kind,
566 start,
567 end,
568 } => self.record_direct(callee, kind, start, end),
569 PendingCallee::WithNamespaceFallback {
570 direct,
571 kind,
572 start,
573 end,
574 } => self.record_namespace_fallback(direct, kind, start, end),
575 PendingCallee::WithAncestorFallback {
576 direct,
577 inherited,
578 kind,
579 start,
580 end,
581 } => self.record_ancestor_fallback(direct, inherited, kind, start, end),
582 }
583 }
584 }
585
586 fn record_unproven_name(&mut self, name: &str, node: Node<'_>) {
587 self.edges
588 .record_unproven_name_at_node(self.input, name, node);
589 }
590
591 fn accepts_target(&self, fqn: &str) -> bool {
592 self.targets.map_or_else(
593 || self.graph.index.definitions(fqn).next().is_some(),
594 |targets| targets.contains(fqn),
595 )
596 }
597
598 fn may_have_target_terminal(&self, terminal: &str) -> bool {
599 self.targets.is_none() || self.targets_by_terminal.contains_key(terminal)
600 }
601
602 fn canonical_namespace_candidates(&self, direct: &str) -> Arc<Vec<String>> {
603 let key = (self.python.record_memo_inputs(), direct.to_owned());
604 if let Some(cached) = self
605 .canonical_namespace_candidates
606 .lock()
607 .expect("Python namespace candidate cache mutex poisoned")
608 .get(&key)
609 .cloned()
610 {
611 return cached;
612 }
613
614 let resolved: Arc<Vec<String>> = Arc::new(
615 resolve_fqn_candidates(self.python, None, direct, |name| {
616 self.graph.index.definitions(name).collect()
617 })
618 .into_iter()
619 .map(|unit| unit.fq_name())
620 .collect(),
621 );
622 self.canonical_namespace_candidates
623 .lock()
624 .expect("Python namespace candidate cache mutex poisoned")
625 .entry(key)
626 .or_insert_with(|| resolved.clone())
627 .clone()
628 }
629}
630
631fn scan_tree(root: Node<'_>, ctx: &mut PyScan<'_>) {
632 let mut scopes: Vec<FunctionScope> = Vec::new();
635 walk(root, ctx, &mut scopes, None);
636}
637
638fn walk(
639 node: Node<'_>,
640 ctx: &mut PyScan<'_>,
641 scopes: &mut Vec<FunctionScope>,
642 facts: Option<usize>,
643) {
644 let mut merged_facts = Vec::new();
645 let mut stack = vec![WalkFrame::Enter { node, facts }];
646 while let Some(frame) = stack.pop() {
647 if ctx.cancellation.is_cancelled() {
648 return;
649 }
650 match frame {
651 WalkFrame::Enter { node, facts } => match node.kind() {
652 "import_statement" | "import_from_statement" => {}
653 "function_definition" | "lambda" => {
657 let function_scope = collect_function_scope(node, ctx.source);
658 let scope_facts = merged_enclosing_scope_facts(
659 ctx.graph,
660 ctx.file,
661 ctx.scope_facts,
662 &mut merged_facts,
663 node,
664 ctx.source,
665 facts,
666 );
667 push_function_children(node, facts, scope_facts, function_scope, &mut stack);
668 }
669 "class_definition" => push_children(node, None, &mut stack),
672 "identifier" => {
673 if !handle_annotation_reference(node, ctx) {
674 handle_identifier(node, ctx, scopes);
675 }
676 push_children(node, facts, &mut stack);
677 }
678 "attribute" => {
679 if handle_annotation_reference(node, ctx) {
680 continue;
681 }
682 let scope_facts = facts.and_then(|id| merged_facts.get(id));
683 handle_attribute(node, ctx, scopes, scope_facts);
684 push_children(node, facts, &mut stack);
685 }
686 "string_content" => {
687 handle_annotation_reference(node, ctx);
688 }
689 "keyword_argument" => {
690 handle_keyword_argument(node, ctx, scopes);
691 if let Some(value) = node.child_by_field_name("value") {
692 stack.push(WalkFrame::Enter { node: value, facts });
693 }
694 }
695 _ => push_children(node, facts, &mut stack),
696 },
697 WalkFrame::ExitScope => {
698 scopes.pop();
699 }
700 WalkFrame::EnterScope(scope) => scopes.push(scope),
701 }
702 }
703}
704
705enum WalkFrame<'tree> {
706 Enter {
707 node: Node<'tree>,
708 facts: Option<usize>,
709 },
710 EnterScope(FunctionScope),
711 ExitScope,
712}
713
714fn push_children<'tree>(
715 node: Node<'tree>,
716 facts: Option<usize>,
717 stack: &mut Vec<WalkFrame<'tree>>,
718) {
719 brokk_bifrost_core::analyzer::tree_walk::push_named_children_reversed_as(node, stack, |node| {
720 WalkFrame::Enter { node, facts }
721 });
722}
723
724fn push_function_children<'tree>(
725 function: Node<'tree>,
726 enclosing_facts: Option<usize>,
727 body_facts: Option<usize>,
728 function_scope: FunctionScope,
729 stack: &mut Vec<WalkFrame<'tree>>,
730) {
731 let body = function.child_by_field_name("body");
732 let mut function_scope = Some(function_scope);
733 for index in (0..function.named_child_count()).rev() {
734 if let Some(child) = function.named_child(index) {
735 let facts = if body == Some(child) {
739 body_facts
740 } else {
741 enclosing_facts
742 };
743 if body == Some(child) {
744 stack.push(WalkFrame::ExitScope);
745 stack.push(WalkFrame::Enter { node: child, facts });
746 stack.push(WalkFrame::EnterScope(
747 function_scope
748 .take()
749 .expect("a function has exactly one body scope"),
750 ));
751 } else {
752 stack.push(WalkFrame::Enter { node: child, facts });
753 }
754 }
755 }
756}
757
758fn merged_enclosing_scope_facts(
759 graph: &PythonGraphSource<'_>,
760 file: &ProjectFile,
761 scope_facts: &HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
762 merged_facts: &mut Vec<LocalBindingsSnapshot<String>>,
763 node: Node<'_>,
764 source: &str,
765 inherited: Option<usize>,
766) -> Option<usize> {
767 let structural_local = collect_function_scope_facts_from_node(node, source);
768 let local = if inherited.is_none() && node.kind() == "function_definition" {
773 enclosing_scope_facts(graph.index, file, scope_facts, node)
774 .cloned()
775 .unwrap_or(structural_local)
776 } else {
777 structural_local
778 };
779 match (local, inherited) {
780 (local, Some(inherited_id)) => {
781 let inherited = merged_facts.get(inherited_id)?;
782 let merged = inherited.merged_with_shadowing(&local);
783 let next_id = merged_facts.len();
784 merged_facts.push(merged);
785 Some(next_id)
786 }
787 (local, None) => {
788 let next_id = merged_facts.len();
789 merged_facts.push(local);
790 Some(next_id)
791 }
792 }
793}
794
795#[derive(Default)]
796struct FunctionScope {
797 locals: HashSet<String>,
798 parameters: HashSet<String>,
799 globals: HashSet<String>,
800}
801
802fn is_shadowed(scopes: &[FunctionScope], name: &str) -> bool {
803 for scope in scopes.iter().rev() {
804 if scope.globals.contains(name) {
805 return false;
806 }
807 if scope.locals.contains(name) {
808 return true;
809 }
810 }
811 false
812}
813
814fn is_receiver_parameter(scopes: &[FunctionScope], name: &str) -> bool {
815 scopes
816 .iter()
817 .rev()
818 .any(|scope| scope.parameters.contains(name))
819}
820
821fn handle_identifier(node: Node<'_>, ctx: &mut PyScan<'_>, scopes: &[FunctionScope]) {
822 if node
824 .parent()
825 .is_some_and(|parent| parent.kind() == "attribute")
826 {
827 return;
828 }
829 if is_declaration_identifier(node) {
830 return;
831 }
832 let text = slice(node, ctx.source);
833 if text.is_empty()
834 || is_shadowed(scopes, text)
835 || python_comprehension_binds_name_at(text, node, ctx.source)
836 || python_type_parameter_binds_name_at(text, node, ctx.source)
837 {
838 return;
839 }
840 let cutoff = if python_reference_is_deferred_function_body(node) {
841 usize::MAX
842 } else {
843 node.start_byte()
844 };
845 let selected = ctx.module_bindings.get(text).and_then(|events| {
846 let mut visible = crate::usage_index::visible_module_binding_suffix(events, cutoff);
847 let event = visible.next()?;
848 (visible.next().is_none() && !event.conditional).then_some(event)
849 });
850 if let Some(binding) = selected
851 && ctx.edges.record_missing_relative_import_site(
852 ctx.input,
853 text,
854 binding,
855 node,
856 |binding| {
857 let ModuleBindingEventKind::FromImport { module, .. } = &binding.kind else {
858 return None;
859 };
860 (module.starts_with('.')
861 && module != "."
862 && usage_resolve_module_files(ctx.python, ctx.file, module).is_empty())
863 .then(|| brokk_bifrost_core::analyzer::usages::inverted_edges::RelativeImportGap {
864 target: module.clone(),
865 kind: if ctx.python.usage_index().relative_module_leaves_workspace(ctx.file, module) {
866 brokk_bifrost_core::analyzer::usages::inverted_edges::RelativeImportGapKind::OutsideWorkspace
867 } else {
868 brokk_bifrost_core::analyzer::usages::inverted_edges::RelativeImportGapKind::Absent
869 },
870 })
871 },
872 )
873 {
874 return;
875 }
876 if let Some(callee) = ctx.bare_callee(text, node) {
877 ctx.record(callee, node);
878 }
879}
880
881fn handle_annotation_reference(node: Node<'_>, ctx: &mut PyScan<'_>) -> bool {
882 let Some(candidates) =
883 annotation_reference_candidates(ctx.graph, ctx.python, ctx.file, ctx.source, node, false)
884 else {
885 return false;
886 };
887 let [candidate] = candidates.as_slice() else {
888 return !(node.kind() == "attribute" && candidates.is_empty());
889 };
890
891 let site = if node.kind() == "attribute" {
892 node.child_by_field_name("attribute").unwrap_or(node)
893 } else {
894 node
895 };
896 ctx.record(candidate.fq_name(), site);
897 true
898}
899
900fn handle_attribute(
901 node: Node<'_>,
902 ctx: &mut PyScan<'_>,
903 scopes: &[FunctionScope],
904 facts: Option<&LocalBindingsSnapshot<String>>,
905) {
906 let (Some(object), Some(attribute)) = (
907 node.child_by_field_name("object"),
908 node.child_by_field_name("attribute"),
909 ) else {
910 return;
911 };
912 let object_text = slice(object, ctx.source);
913 let attribute_text = slice(attribute, ctx.source);
914 if object_text.is_empty() || attribute_text.is_empty() {
915 return;
916 }
917 if object.kind() == "identifier"
918 && ctx.may_have_target_terminal(attribute_text)
919 && let Some(module) =
920 imported_module_assignment_at(node, object_text, ctx.source, |local| {
921 !is_shadowed(scopes, local)
922 && ctx.namespace.get(local).is_some_and(|binding| {
923 binding.module == "importlib" && binding.consumed_attributes == 0
924 })
925 })
926 {
927 let direct = format!("{module}.{attribute_text}");
928 ctx.record_direct_or_namespace_fallback(direct, attribute);
929 }
930 if object.kind() == "call" && ctx.may_have_target_terminal(attribute_text) {
931 for class in call_result_types(ctx.graph, ctx.python, ctx.file, ctx.source, object, facts) {
932 let direct = format!("{}.{attribute_text}", class.fq_name());
933 let inherited = ctx.graph.hierarchy.map_or_else(Vec::new, |provider| {
934 provider
935 .get_ancestors(&class)
936 .definite()
937 .to_vec()
938 .into_iter()
939 .map(|ancestor| format!("{}.{attribute_text}", ancestor.fq_name()))
940 .collect()
941 });
942 ctx.record_direct_or_ancestor_fallback(direct, inherited, attribute);
943 }
944 }
945 if let Some((root, attributes)) = attribute_chain(node) {
949 let root_text = slice(root, ctx.source);
950 if !root_text.is_empty()
951 && !is_shadowed(scopes, root_text)
952 && let Some(binding) = ctx.namespace.get(root_text)
953 {
954 let mut direct = binding.module.clone();
955 let workspace_module = binding.workspace_module;
956 let consumed_attributes = binding.consumed_attributes;
957 if object.kind() == "identifier" {
958 ctx.record(direct.clone(), object);
959 }
960 for member in attributes.into_iter().skip(consumed_attributes) {
961 let member_text = slice(member, ctx.source);
962 if member_text.is_empty() {
963 return;
964 }
965 direct.push('.');
966 direct.push_str(member_text);
967 }
968 if workspace_module {
974 ctx.record_direct_or_namespace_fallback(direct, attribute);
975 } else {
976 ctx.record(direct, attribute);
977 }
978 return;
979 }
980 }
981
982 if let Some(facts) = facts
988 && ctx.may_have_target_terminal(attribute_text)
989 {
990 if matches!(object_text, "self" | "cls") {
991 ctx.record_unproven_name(attribute_text, attribute);
997 } else if let Some(type_fqn) = ctx.receiver_type_fqn(facts, object_text) {
998 ctx.record(format!("{type_fqn}.{attribute_text}"), attribute);
999 } else if object.kind() == "identifier" && !ctx.named.contains_key(object_text) {
1000 let resolution = facts.resolution_for(object_text);
1001 if resolution.is_ambiguous()
1002 || (resolution.is_unknown() && is_receiver_parameter(scopes, object_text))
1003 {
1004 ctx.record_unproven_name(attribute_text, attribute);
1005 }
1006 }
1007 }
1008}
1009
1010fn handle_keyword_argument(node: Node<'_>, ctx: &mut PyScan<'_>, scopes: &[FunctionScope]) {
1011 let (Some(name), Some(arguments)) = (node.child_by_field_name("name"), node.parent()) else {
1012 return;
1013 };
1014 if name.kind() != "identifier" || arguments.kind() != "argument_list" {
1015 return;
1016 }
1017 let Some(call) = arguments.parent().filter(|parent| parent.kind() == "call") else {
1018 return;
1019 };
1020 let Some(function) = call.child_by_field_name("function") else {
1021 return;
1022 };
1023 let member = slice(name, ctx.source);
1024 if member.is_empty() || !ctx.may_have_target_terminal(member) {
1025 return;
1026 }
1027 let scoped_class_fqn = if function.kind() == "identifier" {
1028 enclosing_scope_facts(ctx.graph.index, ctx.file, ctx.scope_facts, function)
1029 .and_then(|facts| ctx.receiver_type_fqn(facts, slice(function, ctx.source)))
1030 } else {
1031 None
1032 };
1033 let function_name = (function.kind() == "identifier").then(|| slice(function, ctx.source));
1034 let mut default_classes = function_name.map_or_else(Vec::new, |local_name| {
1035 resolve_callable_parameter_default_types(
1036 ctx.graph, ctx.python, ctx.file, ctx.source, function, local_name,
1037 )
1038 });
1039 let root_shadowed = leftmost_identifier(function)
1040 .is_some_and(|root| is_shadowed(scopes, slice(root, ctx.source)));
1041 if !root_shadowed && let Some(local_name) = function_name {
1042 let cutoff = if python_reference_is_deferred_function_body(function) {
1043 usize::MAX
1044 } else {
1045 function.start_byte()
1046 };
1047 default_classes.extend(resolve_visible_named_import_candidates(
1048 ctx.graph,
1049 ctx.python,
1050 ctx.file,
1051 ctx.module_bindings.as_ref(),
1052 local_name,
1053 cutoff,
1054 ));
1055 }
1056 let mut classes = if function_name == Some("cls") {
1057 lexical_class(ctx, function).into_iter().collect()
1058 } else {
1059 if root_shadowed && scoped_class_fqn.is_none() && default_classes.is_empty() {
1060 return;
1061 }
1062 if !root_shadowed {
1063 default_classes.extend(resolve_constructor_types(
1064 ctx.graph, ctx.python, ctx.file, ctx.source, function,
1065 ));
1066 }
1067 default_classes
1068 };
1069 if let Some(fqn) = scoped_class_fqn {
1070 classes.extend(ctx.graph.index.definitions(&fqn).filter(CodeUnit::is_class));
1071 classes.sort();
1072 classes.dedup();
1073 }
1074 for class in classes {
1075 for declaration in resolved_member_declarations(ctx.graph, &class, member) {
1076 ctx.record(declaration.fq_name(), name);
1077 }
1078 }
1079}
1080
1081fn lexical_class(ctx: &PyScan<'_>, node: Node<'_>) -> Option<CodeUnit> {
1082 let range = Range {
1083 start_byte: node.start_byte(),
1084 end_byte: node.end_byte(),
1085 start_line: 0,
1086 end_line: 0,
1087 };
1088 let enclosing = ctx.graph.index.enclosing_code_unit(ctx.file, &range)?;
1089 if enclosing.is_class() {
1090 Some(enclosing)
1091 } else {
1092 ctx.graph
1093 .index
1094 .parent_of(&enclosing)
1095 .filter(CodeUnit::is_class)
1096 }
1097}
1098
1099fn collect_function_scope(func: Node<'_>, source: &str) -> FunctionScope {
1104 let mut scope = FunctionScope::default();
1105 if let Some(params) = func.child_by_field_name("parameters") {
1106 collect_parameter_names(params, source, &mut scope.parameters);
1107 scope.locals.extend(scope.parameters.iter().cloned());
1108 }
1109 if let Some(body) = func.child_by_field_name("body") {
1110 collect_bound_targets(body, source, &mut scope.locals);
1111 collect_scope_globals(body, source, &mut scope.globals);
1112 scope.locals.retain(|name| !scope.globals.contains(name));
1113 }
1114 scope
1115}
1116
1117fn collect_scope_globals(node: Node<'_>, source: &str, out: &mut HashSet<String>) {
1118 let root = node;
1119 let mut stack = vec![node];
1120 while let Some(node) = stack.pop() {
1121 if node.kind() == "global_statement" {
1122 let mut cursor = node.walk();
1123 for child in node.named_children(&mut cursor) {
1124 if child.kind() == "identifier" {
1125 let name = slice(child, source).trim();
1126 if !name.is_empty() {
1127 out.insert(name.to_string());
1128 }
1129 }
1130 }
1131 continue;
1132 }
1133 if node != root
1134 && matches!(
1135 node.kind(),
1136 "function_definition" | "lambda" | "class_definition"
1137 )
1138 {
1139 continue;
1140 }
1141 let mut cursor = node.walk();
1142 let mut children: Vec<_> = node.named_children(&mut cursor).collect();
1143 children.reverse();
1144 stack.extend(children);
1145 }
1146}
1147
1148fn collect_parameter_names(params: Node<'_>, source: &str, out: &mut HashSet<String>) {
1149 let mut cursor = params.walk();
1150 for child in params.named_children(&mut cursor) {
1151 let name = match child.kind() {
1152 "identifier" => Some(child),
1153 _ => child
1156 .child_by_field_name("name")
1157 .or_else(|| child.named_child(0).filter(|n| n.kind() == "identifier")),
1158 };
1159 if let Some(name) = name {
1160 let text = slice(name, source).trim();
1161 if !text.is_empty() {
1162 out.insert(text.to_string());
1163 }
1164 }
1165 }
1166}
1167
1168fn collect_bound_targets(node: Node<'_>, source: &str, out: &mut HashSet<String>) {
1172 let mut stack = vec![node];
1173 while let Some(node) = stack.pop() {
1174 match node.kind() {
1175 "function_definition" | "class_definition" => {
1176 if let Some(name) = node.child_by_field_name("name") {
1177 let text = slice(name, source).trim();
1178 if !text.is_empty() {
1179 out.insert(text.to_string());
1180 }
1181 }
1182 continue;
1183 }
1184 "lambda" => continue,
1185 "assignment" | "augmented_assignment" | "for_statement" | "for_in_clause" => {
1186 if let Some(left) = node.child_by_field_name("left") {
1187 collect_assigned_identifiers(left, source, out);
1188 }
1189 }
1190 "named_expression" => {
1191 if let Some(name) = node.child_by_field_name("name") {
1192 collect_assigned_identifiers(name, source, out);
1193 }
1194 }
1195 _ => {}
1196 }
1197 let mut cursor = node.walk();
1198 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
1199 children.reverse();
1200 stack.extend(children);
1201 }
1202}