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, classify_reference_node,
40};
41use brokk_bifrost_core::analyzer::usages::local_inference::LocalBindingsSnapshot;
42use brokk_bifrost_core::analyzer::usages::model::ImportKind;
43use brokk_bifrost_core::analyzer::{CodeUnit, Language, ProjectFile, Range};
44use brokk_bifrost_core::hash::{HashMap, HashSet};
45use std::sync::{Arc, Mutex};
46use tree_sitter::Node;
47
48pub struct PythonEdgeScan<'a> {
55 targets: Option<&'a HashSet<String>>,
56 targets_by_terminal: HashMap<String, Vec<String>>,
57 canonical_namespace_candidates: Mutex<HashMap<String, Arc<Vec<String>>>>,
58}
59
60impl<'a> PythonEdgeScan<'a> {
61 pub fn new(nodes: &HashSet<String>, targets: &'a HashSet<String>) -> Self {
64 debug_assert!(targets.is_subset(nodes));
65 let mut targets_by_terminal: HashMap<String, Vec<String>> = HashMap::default();
66 for target in targets {
67 let terminal = parse_symbol_path(Language::Python, target)
72 .pop()
73 .unwrap_or_else(|| target.clone());
74 targets_by_terminal
75 .entry(terminal)
76 .or_default()
77 .push(target.clone());
78 }
79 Self {
80 targets: Some(targets),
81 targets_by_terminal,
82 canonical_namespace_candidates: Mutex::new(HashMap::default()),
83 }
84 }
85
86 pub fn new_rooted() -> Self {
90 Self {
91 targets: None,
92 targets_by_terminal: HashMap::default(),
93 canonical_namespace_candidates: Mutex::new(HashMap::default()),
94 }
95 }
96
97 pub fn scan_file(
103 &self,
104 graph: &PythonGraphSource<'_>,
105 python: &dyn PythonUsageSource,
106 file: &ProjectFile,
107 input: &FileEdgeScanInput<'_>,
108 ) -> PerFileEdges {
109 let source = input.source;
110
111 let binder = python.import_binder_of(file);
116 let mut named: HashMap<String, String> = HashMap::default();
117 let mut namespace: HashMap<String, NamespaceBinding> = HashMap::default();
118 for (local, binding) in &binder.bindings {
119 match binding.kind {
120 ImportKind::Named => {
121 if let Some(imported) = &binding.imported_name {
122 let module = canonical_import_module_fqn(
123 graph,
124 python,
125 file,
126 &binding.module_specifier,
127 )
128 .unwrap_or_else(|| binding.module_specifier.clone());
129 let imported_fqn = if module.ends_with('.') {
130 format!("{module}{imported}")
131 } else {
132 format!("{module}.{imported}")
133 };
134 if let Some(imported_module) =
135 canonical_import_module_fqn(graph, python, file, &imported_fqn)
136 {
137 namespace.insert(
138 local.clone(),
139 NamespaceBinding {
140 root_module: imported_module.clone(),
141 module: imported_module,
142 workspace_module: true,
143 consumed_attributes: 0,
144 },
145 );
146 } else {
147 named.insert(local.clone(), imported_fqn);
148 }
149 }
150 }
151 ImportKind::Namespace => {
152 let direct_module = binding.module_specifier.clone();
153 let imported_module = binding
154 .namespace_imported_module
155 .as_deref()
156 .unwrap_or(&direct_module);
157 let module = canonical_import_module_fqn(graph, python, file, imported_module);
158 let workspace_module = module.is_some();
159 let consumed_attributes = module.as_ref().map_or(0, |_| {
160 let imported_segments =
161 parse_symbol_path(Language::Python, imported_module);
162 let bound_segments = parse_symbol_path(Language::Python, &direct_module);
163 imported_segments.len().saturating_sub(bound_segments.len())
164 });
165 let canonical_module = module.unwrap_or(direct_module);
166 let mut root_segments = parse_symbol_path(Language::Python, &canonical_module);
167 root_segments.truncate(root_segments.len().saturating_sub(consumed_attributes));
168 namespace.insert(
169 local.clone(),
170 NamespaceBinding {
171 root_module: root_segments.join("."),
172 module: canonical_module,
173 workspace_module,
174 consumed_attributes,
175 },
176 );
177 }
178 ImportKind::Default | ImportKind::CommonJsRequire | ImportKind::Glob => {}
179 }
180 }
181 let same_file: HashMap<String, String> = graph
182 .index
183 .declarations(file)
184 .into_iter()
185 .map(|unit| (unit.identifier().to_string(), unit.fq_name()))
186 .collect();
187 let module_bindings = usage_module_binding_timeline(python, file, || {
188 super::extractor::collect_module_binding_timeline(input.root(), source)
189 });
190
191 let scope_facts = usage_scope_facts(python, file, || {
195 collect_scope_facts_from_parsed_source(graph, python, file, source, input.root())
196 });
197
198 let mut ctx = PyScan {
199 graph,
200 python,
201 targets: self.targets,
202 targets_by_terminal: &self.targets_by_terminal,
203 file,
204 source,
205 named,
206 namespace,
207 same_file,
208 module_bindings,
209 scope_facts: scope_facts.as_ref(),
210 canonical_namespace_candidates: &self.canonical_namespace_candidates,
211 input,
212 edges: PerFileEdges::default(),
213 };
214 scan_tree(input.root(), &mut ctx);
215 ctx.edges
216 }
217}
218
219fn canonical_import_module_fqn(
220 graph: &PythonGraphSource<'_>,
221 python: &dyn PythonUsageSource,
222 importing_file: &ProjectFile,
223 module_specifier: &str,
224) -> Option<String> {
225 let resolved = usage_resolve_module_files(python, importing_file, module_specifier);
226 let [module_file] = resolved.as_slice() else {
227 return None;
228 };
229 graph
230 .index
231 .declarations(module_file)
232 .into_iter()
233 .find(CodeUnit::is_module)
234 .map(|module| module.fq_name())
235}
236
237struct PyScan<'a> {
238 graph: &'a PythonGraphSource<'a>,
239 python: &'a dyn PythonUsageSource,
240 targets: Option<&'a HashSet<String>>,
241 targets_by_terminal: &'a HashMap<String, Vec<String>>,
242 file: &'a ProjectFile,
243 source: &'a str,
244 named: HashMap<String, String>,
245 namespace: HashMap<String, NamespaceBinding>,
246 same_file: HashMap<String, String>,
247 module_bindings: Arc<ModuleBindingTimeline>,
248 scope_facts: &'a HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
249 canonical_namespace_candidates: &'a Mutex<HashMap<String, Arc<Vec<String>>>>,
250 input: &'a FileEdgeScanInput<'a>,
251 edges: PerFileEdges,
252}
253
254struct NamespaceBinding {
255 root_module: String,
256 module: String,
257 workspace_module: bool,
258 consumed_attributes: usize,
259}
260
261impl PyScan<'_> {
262 fn bare_callee(&self, text: &str, node: Node<'_>) -> Option<String> {
265 if let Some(fqn) = self.named.get(text) {
266 return Some(fqn.clone());
267 }
268 if let Some(fqn) = self.namespace.get(text) {
269 return self
270 .visible_namespace_root(text, node)
271 .or_else(|| Some(fqn.root_module.clone()));
272 }
273 if let Some(fqn) = self.same_file.get(text) {
274 return Some(fqn.clone());
275 }
276 None
277 }
278
279 fn visible_namespace_root(&self, local: &str, node: Node<'_>) -> Option<String> {
280 let events = self.module_bindings.get(local)?;
281 let cutoff = if reference_is_deferred_function_body(node) {
282 usize::MAX
283 } else {
284 node.start_byte()
285 };
286 let visible: Vec<_> = events
287 .iter()
288 .filter(|event| event.visible_from <= cutoff)
289 .collect();
290 let start = visible
291 .iter()
292 .rposition(|event| {
293 if event.conditional {
294 return false;
295 }
296 match &event.kind {
297 ModuleBindingEventKind::ImportModule {
298 module,
299 consumed_attributes,
300 } => *consumed_attributes == 0 && module != local,
301 ModuleBindingEventKind::FromImport { .. } | ModuleBindingEventKind::Other => {
302 true
303 }
304 }
305 })
306 .unwrap_or(0);
307 let mut roots = visible[start..]
308 .iter()
309 .filter_map(|event| match &event.kind {
310 ModuleBindingEventKind::ImportModule {
311 module,
312 consumed_attributes,
313 } => {
314 let mut segments = parse_symbol_path(Language::Python, module);
315 segments.truncate(segments.len().saturating_sub(*consumed_attributes));
316 let root = segments.join(".");
317 canonical_import_module_fqn(self.graph, self.python, self.file, &root)
318 .or(Some(root))
319 }
320 ModuleBindingEventKind::FromImport { .. } | ModuleBindingEventKind::Other => None,
321 })
322 .collect::<Vec<_>>();
323 roots.sort();
324 roots.dedup();
325 (roots.len() == 1).then(|| roots.remove(0))
326 }
327
328 fn receiver_type_fqn(
332 &self,
333 facts: &LocalBindingsSnapshot<String>,
334 receiver: &str,
335 ) -> Option<String> {
336 let resolution = facts.resolution_for(receiver);
337 let type_name = resolution
338 .as_precise()
339 .and_then(|targets| targets.iter().next())?;
340 resolve_receiver_type(self.graph, self.python, self.file, type_name, false)
346 .map(|unit| unit.fq_name())
347 }
348
349 fn record(&mut self, callee: String, node: Node<'_>) {
350 if !self.accepts_target(&callee) {
351 return;
352 }
353 self.edges.record_kind(
354 self.input,
355 callee,
356 classify_reference_node(node),
357 node.start_byte(),
358 node.end_byte(),
359 );
360 }
361
362 fn record_unproven_name(&mut self, name: &str, node: Node<'_>) {
363 let Some(targets) = self.targets_by_terminal.get(name) else {
364 return;
365 };
366 for target in targets {
367 self.edges.record_unproven(
368 self.input,
369 target.clone(),
370 node.start_byte(),
371 node.end_byte(),
372 );
373 }
374 }
375
376 fn accepts_target(&self, fqn: &str) -> bool {
377 self.targets.map_or_else(
378 || self.graph.index.definitions(fqn).next().is_some(),
379 |targets| targets.contains(fqn),
380 )
381 }
382
383 fn may_have_target_terminal(&self, terminal: &str) -> bool {
384 self.targets.is_none() || self.targets_by_terminal.contains_key(terminal)
385 }
386
387 fn canonical_namespace_candidates(&self, direct: &str) -> Arc<Vec<String>> {
388 if let Some(cached) = self
389 .canonical_namespace_candidates
390 .lock()
391 .expect("Python namespace candidate cache mutex poisoned")
392 .get(direct)
393 .cloned()
394 {
395 return cached;
396 }
397
398 let resolved: Arc<Vec<String>> = Arc::new(
399 resolve_fqn_candidates(self.python, direct, |name| {
400 self.graph.index.definitions(name).collect()
401 })
402 .into_iter()
403 .map(|unit| unit.fq_name())
404 .collect(),
405 );
406 self.canonical_namespace_candidates
407 .lock()
408 .expect("Python namespace candidate cache mutex poisoned")
409 .entry(direct.to_string())
410 .or_insert_with(|| resolved.clone())
411 .clone()
412 }
413}
414
415fn scan_tree(root: Node<'_>, ctx: &mut PyScan<'_>) {
416 let mut scopes: Vec<FunctionScope> = Vec::new();
419 walk(root, ctx, &mut scopes, None);
420}
421
422fn walk(
423 node: Node<'_>,
424 ctx: &mut PyScan<'_>,
425 scopes: &mut Vec<FunctionScope>,
426 facts: Option<usize>,
427) {
428 let mut merged_facts = Vec::new();
429 let mut stack = vec![WalkFrame::Enter { node, facts }];
430 while let Some(frame) = stack.pop() {
431 match frame {
432 WalkFrame::Enter { node, facts } => match node.kind() {
433 "import_statement" | "import_from_statement" => {}
434 "function_definition" | "lambda" => {
438 let function_scope = collect_function_scope(node, ctx.source);
439 let scope_facts = merged_enclosing_scope_facts(
440 ctx.graph,
441 ctx.file,
442 ctx.scope_facts,
443 &mut merged_facts,
444 node,
445 ctx.source,
446 facts,
447 );
448 push_function_children(node, facts, scope_facts, function_scope, &mut stack);
449 }
450 "class_definition" => push_children(node, None, &mut stack),
453 "identifier" => {
454 if !handle_annotation_reference(node, ctx) {
455 handle_identifier(node, ctx, scopes);
456 }
457 push_children(node, facts, &mut stack);
458 }
459 "attribute" => {
460 if handle_annotation_reference(node, ctx) {
461 continue;
462 }
463 let scope_facts = facts.and_then(|id| merged_facts.get(id));
464 handle_attribute(node, ctx, scopes, scope_facts);
465 push_children(node, facts, &mut stack);
466 }
467 "string_content" => {
468 handle_annotation_reference(node, ctx);
469 }
470 "keyword_argument" => {
471 handle_keyword_argument(node, ctx, scopes);
472 if let Some(value) = node.child_by_field_name("value") {
473 stack.push(WalkFrame::Enter { node: value, facts });
474 }
475 }
476 _ => push_children(node, facts, &mut stack),
477 },
478 WalkFrame::ExitScope => {
479 scopes.pop();
480 }
481 WalkFrame::EnterScope(scope) => scopes.push(scope),
482 }
483 }
484}
485
486enum WalkFrame<'tree> {
487 Enter {
488 node: Node<'tree>,
489 facts: Option<usize>,
490 },
491 EnterScope(FunctionScope),
492 ExitScope,
493}
494
495fn push_children<'tree>(
496 node: Node<'tree>,
497 facts: Option<usize>,
498 stack: &mut Vec<WalkFrame<'tree>>,
499) {
500 for index in (0..node.named_child_count()).rev() {
501 if let Some(child) = node.named_child(index) {
502 stack.push(WalkFrame::Enter { node: child, facts });
503 }
504 }
505}
506
507fn push_function_children<'tree>(
508 function: Node<'tree>,
509 enclosing_facts: Option<usize>,
510 body_facts: Option<usize>,
511 function_scope: FunctionScope,
512 stack: &mut Vec<WalkFrame<'tree>>,
513) {
514 let body = function.child_by_field_name("body");
515 let mut function_scope = Some(function_scope);
516 for index in (0..function.named_child_count()).rev() {
517 if let Some(child) = function.named_child(index) {
518 let facts = if body == Some(child) {
522 body_facts
523 } else {
524 enclosing_facts
525 };
526 if body == Some(child) {
527 stack.push(WalkFrame::ExitScope);
528 stack.push(WalkFrame::Enter { node: child, facts });
529 stack.push(WalkFrame::EnterScope(
530 function_scope
531 .take()
532 .expect("a function has exactly one body scope"),
533 ));
534 } else {
535 stack.push(WalkFrame::Enter { node: child, facts });
536 }
537 }
538 }
539}
540
541fn merged_enclosing_scope_facts(
542 graph: &PythonGraphSource<'_>,
543 file: &ProjectFile,
544 scope_facts: &HashMap<CodeUnit, LocalBindingsSnapshot<String>>,
545 merged_facts: &mut Vec<LocalBindingsSnapshot<String>>,
546 node: Node<'_>,
547 source: &str,
548 inherited: Option<usize>,
549) -> Option<usize> {
550 let structural_local = collect_function_scope_facts_from_node(node, source);
551 let local = if inherited.is_none() && node.kind() == "function_definition" {
556 enclosing_scope_facts(graph.index, file, scope_facts, node)
557 .cloned()
558 .unwrap_or(structural_local)
559 } else {
560 structural_local
561 };
562 match (local, inherited) {
563 (local, Some(inherited_id)) => {
564 let inherited = merged_facts.get(inherited_id)?;
565 let merged = inherited.merged_with_shadowing(&local);
566 let next_id = merged_facts.len();
567 merged_facts.push(merged);
568 Some(next_id)
569 }
570 (local, None) => {
571 let next_id = merged_facts.len();
572 merged_facts.push(local);
573 Some(next_id)
574 }
575 }
576}
577
578#[derive(Default)]
579struct FunctionScope {
580 locals: HashSet<String>,
581 parameters: HashSet<String>,
582 globals: HashSet<String>,
583}
584
585fn is_shadowed(scopes: &[FunctionScope], name: &str) -> bool {
586 for scope in scopes.iter().rev() {
587 if scope.globals.contains(name) {
588 return false;
589 }
590 if scope.locals.contains(name) {
591 return true;
592 }
593 }
594 false
595}
596
597fn is_receiver_parameter(scopes: &[FunctionScope], name: &str) -> bool {
598 scopes
599 .iter()
600 .rev()
601 .any(|scope| scope.parameters.contains(name))
602}
603
604fn handle_identifier(node: Node<'_>, ctx: &mut PyScan<'_>, scopes: &[FunctionScope]) {
605 if node
607 .parent()
608 .is_some_and(|parent| parent.kind() == "attribute")
609 {
610 return;
611 }
612 if is_declaration_identifier(node) {
613 return;
614 }
615 let text = slice(node, ctx.source);
616 if text.is_empty()
617 || is_shadowed(scopes, text)
618 || python_comprehension_binds_name_at(text, node, ctx.source)
619 || python_type_parameter_binds_name_at(text, node, ctx.source)
620 {
621 return;
622 }
623 if let Some(callee) = ctx.bare_callee(text, node) {
624 ctx.record(callee, node);
625 }
626}
627
628fn handle_annotation_reference(node: Node<'_>, ctx: &mut PyScan<'_>) -> bool {
629 let Some(candidates) =
630 annotation_reference_candidates(ctx.graph, ctx.python, ctx.file, ctx.source, node, false)
631 else {
632 return false;
633 };
634 let [candidate] = candidates.as_slice() else {
635 return !(node.kind() == "attribute" && candidates.is_empty());
636 };
637
638 let site = if node.kind() == "attribute" {
639 node.child_by_field_name("attribute").unwrap_or(node)
640 } else {
641 node
642 };
643 ctx.record(candidate.fq_name(), site);
644 true
645}
646
647fn handle_attribute(
648 node: Node<'_>,
649 ctx: &mut PyScan<'_>,
650 scopes: &[FunctionScope],
651 facts: Option<&LocalBindingsSnapshot<String>>,
652) {
653 let (Some(object), Some(attribute)) = (
654 node.child_by_field_name("object"),
655 node.child_by_field_name("attribute"),
656 ) else {
657 return;
658 };
659 let object_text = slice(object, ctx.source);
660 let attribute_text = slice(attribute, ctx.source);
661 if object_text.is_empty() || attribute_text.is_empty() {
662 return;
663 }
664 if object.kind() == "identifier"
665 && ctx.may_have_target_terminal(attribute_text)
666 && let Some(module) =
667 imported_module_assignment_at(node, object_text, ctx.source, |local| {
668 !is_shadowed(scopes, local)
669 && ctx.namespace.get(local).is_some_and(|binding| {
670 binding.module == "importlib" && binding.consumed_attributes == 0
671 })
672 })
673 {
674 let direct = format!("{module}.{attribute_text}");
675 if ctx.accepts_target(&direct) {
676 ctx.record(direct, attribute);
677 } else {
678 for resolved in ctx.canonical_namespace_candidates(&direct).iter() {
679 ctx.record(resolved.clone(), attribute);
680 }
681 }
682 }
683 if object.kind() == "call" && ctx.may_have_target_terminal(attribute_text) {
684 for class in call_result_types(ctx.graph, ctx.python, ctx.file, ctx.source, object, facts) {
685 let direct = format!("{}.{attribute_text}", class.fq_name());
686 if ctx.accepts_target(&direct) {
687 ctx.record(direct, attribute);
688 continue;
689 }
690 if let Some(provider) = ctx.graph.hierarchy {
691 for ancestor in provider.get_ancestors(&class) {
692 let inherited = format!("{}.{attribute_text}", ancestor.fq_name());
693 if ctx.accepts_target(&inherited) {
694 ctx.record(inherited, attribute);
695 }
696 }
697 }
698 }
699 }
700 if let Some((root, attributes)) = attribute_chain(node) {
704 let root_text = slice(root, ctx.source);
705 if !root_text.is_empty()
706 && !is_shadowed(scopes, root_text)
707 && let Some(binding) = ctx.namespace.get(root_text)
708 {
709 let mut direct = binding.module.clone();
710 let workspace_module = binding.workspace_module;
711 let consumed_attributes = binding.consumed_attributes;
712 if object.kind() == "identifier" && ctx.accepts_target(&direct) {
713 ctx.record(direct.clone(), object);
714 }
715 for member in attributes.into_iter().skip(consumed_attributes) {
716 let member_text = slice(member, ctx.source);
717 if member_text.is_empty() {
718 return;
719 }
720 direct.push('.');
721 direct.push_str(member_text);
722 }
723 if ctx.accepts_target(&direct) {
724 ctx.record(direct, attribute);
725 return;
726 }
727 if workspace_module {
733 for resolved in ctx.canonical_namespace_candidates(&direct).iter() {
734 ctx.record(resolved.clone(), attribute);
735 }
736 }
737 return;
738 }
739 }
740
741 if let Some(facts) = facts
747 && ctx.may_have_target_terminal(attribute_text)
748 {
749 if matches!(object_text, "self" | "cls") {
750 ctx.record_unproven_name(attribute_text, attribute);
756 } else if let Some(type_fqn) = ctx.receiver_type_fqn(facts, object_text) {
757 ctx.record(format!("{type_fqn}.{attribute_text}"), attribute);
758 } else if object.kind() == "identifier" && !ctx.named.contains_key(object_text) {
759 let resolution = facts.resolution_for(object_text);
760 if resolution.is_ambiguous()
761 || (resolution.is_unknown() && is_receiver_parameter(scopes, object_text))
762 {
763 ctx.record_unproven_name(attribute_text, attribute);
764 }
765 }
766 }
767}
768
769fn handle_keyword_argument(node: Node<'_>, ctx: &mut PyScan<'_>, scopes: &[FunctionScope]) {
770 let (Some(name), Some(arguments)) = (node.child_by_field_name("name"), node.parent()) else {
771 return;
772 };
773 if name.kind() != "identifier" || arguments.kind() != "argument_list" {
774 return;
775 }
776 let Some(call) = arguments.parent().filter(|parent| parent.kind() == "call") else {
777 return;
778 };
779 let Some(function) = call.child_by_field_name("function") else {
780 return;
781 };
782 let member = slice(name, ctx.source);
783 if member.is_empty() || !ctx.may_have_target_terminal(member) {
784 return;
785 }
786 let scoped_class_fqn = if function.kind() == "identifier" {
787 enclosing_scope_facts(ctx.graph.index, ctx.file, ctx.scope_facts, function)
788 .and_then(|facts| ctx.receiver_type_fqn(facts, slice(function, ctx.source)))
789 } else {
790 None
791 };
792 let function_name = (function.kind() == "identifier").then(|| slice(function, ctx.source));
793 let mut default_classes = function_name.map_or_else(Vec::new, |local_name| {
794 resolve_callable_parameter_default_types(
795 ctx.graph, ctx.python, ctx.file, ctx.source, function, local_name,
796 )
797 });
798 let root_shadowed = leftmost_identifier(function)
799 .is_some_and(|root| is_shadowed(scopes, slice(root, ctx.source)));
800 if !root_shadowed && let Some(local_name) = function_name {
801 let cutoff = if reference_is_deferred_function_body(function) {
802 usize::MAX
803 } else {
804 function.start_byte()
805 };
806 default_classes.extend(resolve_visible_named_import_candidates(
807 ctx.graph,
808 ctx.python,
809 ctx.file,
810 ctx.module_bindings.as_ref(),
811 local_name,
812 cutoff,
813 ));
814 }
815 let mut classes = if function_name == Some("cls") {
816 lexical_class(ctx, function).into_iter().collect()
817 } else {
818 if root_shadowed && scoped_class_fqn.is_none() && default_classes.is_empty() {
819 return;
820 }
821 if !root_shadowed {
822 default_classes.extend(resolve_constructor_types(
823 ctx.graph, ctx.python, ctx.file, ctx.source, function,
824 ));
825 }
826 default_classes
827 };
828 if let Some(fqn) = scoped_class_fqn {
829 classes.extend(ctx.graph.index.definitions(&fqn).filter(CodeUnit::is_class));
830 classes.sort();
831 classes.dedup();
832 }
833 for class in classes {
834 for declaration in resolved_member_declarations(ctx.graph, &class, member) {
835 let fqn = declaration.fq_name();
836 if ctx.accepts_target(&fqn) {
837 ctx.record(fqn, name);
838 }
839 }
840 }
841}
842
843fn lexical_class(ctx: &PyScan<'_>, node: Node<'_>) -> Option<CodeUnit> {
844 let range = Range {
845 start_byte: node.start_byte(),
846 end_byte: node.end_byte(),
847 start_line: 0,
848 end_line: 0,
849 };
850 let enclosing = ctx.graph.index.enclosing_code_unit(ctx.file, &range)?;
851 if enclosing.is_class() {
852 Some(enclosing)
853 } else {
854 ctx.graph
855 .index
856 .parent_of(&enclosing)
857 .filter(CodeUnit::is_class)
858 }
859}
860
861fn leftmost_identifier(mut node: Node<'_>) -> Option<Node<'_>> {
862 loop {
863 match node.kind() {
864 "identifier" => return Some(node),
865 "attribute" => node = node.child_by_field_name("object")?,
866 _ => return None,
867 }
868 }
869}
870
871fn attribute_chain<'a>(node: Node<'a>) -> Option<(Node<'a>, Vec<Node<'a>>)> {
872 let mut attributes = Vec::new();
873 let mut current = node;
874 loop {
875 if current.kind() != "attribute" {
876 return None;
877 }
878 attributes.push(current.child_by_field_name("attribute")?);
879 current = current.child_by_field_name("object")?;
880 if current.kind() == "identifier" {
881 attributes.reverse();
882 return Some((current, attributes));
883 }
884 }
885}
886
887fn collect_function_scope(func: Node<'_>, source: &str) -> FunctionScope {
892 let mut scope = FunctionScope::default();
893 if let Some(params) = func.child_by_field_name("parameters") {
894 collect_parameter_names(params, source, &mut scope.parameters);
895 scope.locals.extend(scope.parameters.iter().cloned());
896 }
897 if let Some(body) = func.child_by_field_name("body") {
898 collect_bound_targets(body, source, &mut scope.locals);
899 collect_scope_globals(body, source, &mut scope.globals);
900 scope.locals.retain(|name| !scope.globals.contains(name));
901 }
902 scope
903}
904
905fn collect_scope_globals(node: Node<'_>, source: &str, out: &mut HashSet<String>) {
906 let root = node;
907 let mut stack = vec![node];
908 while let Some(node) = stack.pop() {
909 if node.kind() == "global_statement" {
910 let mut cursor = node.walk();
911 for child in node.named_children(&mut cursor) {
912 if child.kind() == "identifier" {
913 let name = slice(child, source).trim();
914 if !name.is_empty() {
915 out.insert(name.to_string());
916 }
917 }
918 }
919 continue;
920 }
921 if node != root
922 && matches!(
923 node.kind(),
924 "function_definition" | "lambda" | "class_definition"
925 )
926 {
927 continue;
928 }
929 let mut cursor = node.walk();
930 let mut children: Vec<_> = node.named_children(&mut cursor).collect();
931 children.reverse();
932 stack.extend(children);
933 }
934}
935
936fn collect_parameter_names(params: Node<'_>, source: &str, out: &mut HashSet<String>) {
937 let mut cursor = params.walk();
938 for child in params.named_children(&mut cursor) {
939 let name = match child.kind() {
940 "identifier" => Some(child),
941 _ => child
944 .child_by_field_name("name")
945 .or_else(|| child.named_child(0).filter(|n| n.kind() == "identifier")),
946 };
947 if let Some(name) = name {
948 let text = slice(name, source).trim();
949 if !text.is_empty() {
950 out.insert(text.to_string());
951 }
952 }
953 }
954}
955
956fn collect_bound_targets(node: Node<'_>, source: &str, out: &mut HashSet<String>) {
960 let mut stack = vec![node];
961 while let Some(node) = stack.pop() {
962 match node.kind() {
963 "function_definition" | "class_definition" => {
964 if let Some(name) = node.child_by_field_name("name") {
965 let text = slice(name, source).trim();
966 if !text.is_empty() {
967 out.insert(text.to_string());
968 }
969 }
970 continue;
971 }
972 "lambda" => continue,
973 "assignment" | "augmented_assignment" | "for_statement" | "for_in_clause" => {
974 if let Some(left) = node.child_by_field_name("left") {
975 collect_assigned_identifiers(left, source, out);
976 }
977 }
978 "named_expression" => {
979 if let Some(name) = node.child_by_field_name("name") {
980 collect_assigned_identifiers(name, source, out);
981 }
982 }
983 _ => {}
984 }
985 let mut cursor = node.walk();
986 let mut children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
987 children.reverse();
988 stack.extend(children);
989 }
990}