1use crate::graph::PythonGraphSource;
9use crate::graph_support::PythonUsageSource;
10use crate::imports::resolve_fqn_candidates;
11use crate::syntax::{
12 python_deferred_annotation_identifier_ranges, python_deferred_annotation_tree,
13 python_node_is_in_annotation,
14};
15use crate::usage_index::{
16 ModuleBindingEventKind, ModuleBindingTimeline, usage_resolve_module_files, usage_seeds,
17};
18use brokk_bifrost_core::analyzer::fq_name::{FqName, SegmentKind, segment_interner};
19use brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path;
20use brokk_bifrost_core::analyzer::usages::model::{ExportEntry, ImportBinder, ImportKind};
21use brokk_bifrost_core::analyzer::usages::{ImportEdge, ImportEdgeKind};
22use brokk_bifrost_core::analyzer::{
23 CodeUnit, CodeUnitIndex, DefinitionLanguageScope, Language, ProjectFile, Range,
24 RelationalDefinitionQuery, RelationalDefinitionQuestion, RelationalDefinitionValue,
25 RelationalName,
26};
27use brokk_bifrost_core::hash::HashSet;
28use std::collections::BTreeSet;
29use tree_sitter::Node;
30
31const MAX_MEMBER_OWNER_FRONTIER: usize = 512;
32
33pub(crate) fn resolved_member_declarations(
37 graph: &PythonGraphSource<'_>,
38 receiver: &CodeUnit,
39 member: &str,
40) -> Vec<CodeUnit> {
41 let mut visited = HashSet::default();
42 let mut frontier = vec![receiver.clone()];
43 while !frontier.is_empty() && visited.len() < MAX_MEMBER_OWNER_FRONTIER {
44 let mut declarations = Vec::new();
45 let mut next = Vec::new();
46 for owner in frontier {
47 if !visited.insert(owner.clone()) {
48 continue;
49 }
50 let fqn = format!("{}.{member}", owner.fq_name());
51 declarations.extend(
52 graph
53 .index
54 .definitions(&fqn)
55 .filter(|candidate| graph.index.parent_of(candidate).as_ref() == Some(&owner)),
56 );
57 if let Some(hierarchy) = graph.hierarchy {
58 next.extend(hierarchy.get_direct_ancestors(&owner));
59 }
60 }
61 declarations.sort();
62 declarations.dedup();
63 if !declarations.is_empty() {
64 return declarations;
65 }
66 next.sort();
67 next.dedup();
68 frontier = next;
69 }
70 Vec::new()
71}
72
73pub fn infer_export_names(python: &dyn PythonUsageSource, target: &CodeUnit) -> BTreeSet<String> {
74 if target_owner_code_unit(python, target).is_some() {
75 let owner_name = top_level_identifier(python, target);
76 let owner_exports =
77 infer_export_names_for_local(python, target, target.source(), &owner_name);
78 if !owner_exports.is_empty() {
79 return owner_exports;
80 }
81 }
82
83 infer_export_names_for_local(python, target, target.source(), target.identifier())
84}
85
86pub fn infer_usage_seeds(
87 python: &dyn PythonUsageSource,
88 target: &CodeUnit,
89 seed_names: BTreeSet<String>,
90) -> BTreeSet<(ProjectFile, String)> {
91 let mut seeds = BTreeSet::new();
92 for seed_name in &seed_names {
93 seeds.extend(usage_seeds(python, target.source(), seed_name));
94 }
95 if seeds.is_empty()
96 && seed_names.contains(target.identifier())
97 && is_module_level_target_identifier(python, target, target.source(), target.identifier())
98 {
99 seeds.insert((target.source().clone(), target.identifier().to_string()));
100 }
101 seeds
102}
103
104fn infer_export_names_for_local(
105 python: &dyn PythonUsageSource,
106 target: &CodeUnit,
107 file: &ProjectFile,
108 local_name: &str,
109) -> BTreeSet<String> {
110 let index = python.export_index_of(file);
111 let mut export_names = BTreeSet::new();
112 if index.exports_by_name.contains_key(local_name) {
113 export_names.insert(local_name.to_string());
114 }
115 for (export_name, entry) in &index.exports_by_name {
116 if matches!(entry, ExportEntry::Local { local_name: name } if name == local_name) {
117 export_names.insert(export_name.clone());
118 }
119 }
120 if export_names.is_empty()
121 && is_module_level_target_identifier(python, target, file, local_name)
122 {
123 export_names.insert(local_name.to_string());
124 }
125 export_names
126}
127
128fn is_module_level_target_identifier(
129 python: &dyn PythonUsageSource,
130 target: &CodeUnit,
131 file: &ProjectFile,
132 local_name: &str,
133) -> bool {
134 target.source() == file
135 && target.identifier() == local_name
136 && python
137 .parent_of(target)
138 .is_some_and(|parent| parent.is_module() && parent.source() == file)
139}
140
141pub fn top_level_identifier(index: &dyn CodeUnitIndex, target: &CodeUnit) -> String {
142 let mut current = target.clone();
143 while let Some(parent) = index.parent_of(¤t) {
144 if parent.is_module() {
145 break;
146 }
147 current = parent;
148 }
149 current.identifier().to_string()
150}
151
152pub fn member_name(index: &dyn CodeUnitIndex, target: &CodeUnit) -> Option<String> {
153 target_owner_code_unit(index, target).map(|_| target.identifier().to_string())
154}
155
156pub fn target_owner_code_unit(index: &dyn CodeUnitIndex, target: &CodeUnit) -> Option<CodeUnit> {
157 index
158 .parent_of(target)
159 .filter(|parent| parent.source() == target.source() && parent.is_class())
160}
161
162pub fn resolve_receiver_type(
163 graph: &PythonGraphSource<'_>,
164 python: &dyn PythonUsageSource,
165 file: &ProjectFile,
166 raw_type: &str,
167 target_self_file: bool,
168) -> Option<CodeUnit> {
169 let raw_type = raw_type.trim();
170 if raw_type.is_empty() || raw_type.contains('|') {
171 return None;
172 }
173 if raw_type.contains('.') {
174 let candidates = resolve_fqn_candidates(python, raw_type, |name| {
175 graph.index.definitions(name).collect()
176 })
177 .into_iter()
178 .filter(CodeUnit::is_class)
179 .collect::<Vec<_>>();
180 return (candidates.len() == 1)
181 .then(|| candidates.into_iter().next())
182 .flatten();
183 }
184
185 if let Some(binding) = python.import_binder_of(file).bindings.get(raw_type)
186 && binding.kind == ImportKind::Named
187 && let Some(imported) = binding.imported_name.as_ref()
188 {
189 let fqn = format!("{}.{}", binding.module_specifier, imported);
190 if let Some(class) =
191 resolve_fqn_candidates(python, &fqn, |name| graph.index.definitions(name).collect())
192 .into_iter()
193 .find(CodeUnit::is_class)
194 {
195 return Some(class);
196 }
197 }
198
199 if let Some(provider) = graph.imports
200 && let Some(imported) = provider
201 .imported_code_units_of(file)
202 .iter()
203 .find(|code_unit| code_unit.identifier() == raw_type && code_unit.is_class())
204 {
205 return Some(imported.clone());
206 }
207
208 graph
209 .index
210 .declarations(file)
211 .into_iter()
212 .find(|code_unit| code_unit.identifier() == raw_type && code_unit.is_class())
213 .or_else(|| {
214 if !target_self_file {
215 return None;
216 }
217 resolve_indexed_receiver_type(graph, file, raw_type)
218 })
219}
220
221fn resolve_bare_annotation_symbol(
222 graph: &PythonGraphSource<'_>,
223 python: &dyn PythonUsageSource,
224 file: &ProjectFile,
225 source: &str,
226 node: Node<'_>,
227 raw_symbol: &str,
228) -> Vec<CodeUnit> {
229 let raw_symbol = raw_symbol.trim();
230 if raw_symbol.is_empty() {
231 return Vec::new();
232 }
233
234 if let Some(owner) = annotation_scope_owner_class(graph, file, source, node) {
235 let owner_candidates: Vec<_> = exact_owner_annotation_members(graph, &owner, raw_symbol)
236 .into_iter()
237 .filter(|candidate| !candidate.is_function())
238 .collect();
239 if !owner_candidates.is_empty() {
240 return owner_candidates;
241 }
242 }
243
244 let mut candidates = Vec::new();
245 if let Some(binding) = python.import_binder_of(file).bindings.get(raw_symbol)
246 && binding.kind == ImportKind::Named
247 && let Some(imported) = binding.imported_name.as_ref()
248 {
249 let fqn = format!("{}.{}", binding.module_specifier, imported);
250 let mut imported_candidates = resolve_fqn_candidates(python, &fqn, |name| {
251 graph
252 .index
253 .definitions(name)
254 .filter(|candidate| candidate.source().language() == Language::Python)
255 .collect()
256 });
257 imported_candidates.retain(|candidate| {
258 !candidate.is_module() || candidate.fq_name() != binding.module_specifier
259 });
260 candidates.extend(imported_candidates);
261 }
262
263 candidates.extend(
264 graph
265 .index
266 .top_level_declarations(file)
267 .into_iter()
268 .filter(|code_unit| {
269 !code_unit.is_module()
270 && code_unit.identifier() == raw_symbol
271 && graph
272 .index
273 .parent_of(code_unit)
274 .is_some_and(|parent| parent.is_module())
275 }),
276 );
277
278 candidates.retain(|candidate| !candidate.is_function());
279 candidates.sort();
280 candidates.dedup();
281 candidates
282}
283
284pub fn annotation_reference_candidates(
291 graph: &PythonGraphSource<'_>,
292 python: &dyn PythonUsageSource,
293 file: &ProjectFile,
294 source: &str,
295 node: Node<'_>,
296 target_self_file: bool,
297) -> Option<Vec<CodeUnit>> {
298 if !is_annotation_reference_node(node, source) {
299 return None;
300 }
301
302 let mut candidates = match node.kind() {
303 "identifier" => {
304 let mut candidates = resolve_bare_annotation_symbol(
305 graph,
306 python,
307 file,
308 source,
309 node,
310 node_text(node, source),
311 );
312 if candidates.is_empty() {
313 candidates.extend(resolve_receiver_type(
314 graph,
315 python,
316 file,
317 node_text(node, source),
318 target_self_file,
319 ));
320 }
321 candidates
322 }
323 "string_content" => {
324 let Some(string) = node.parent() else {
325 return Some(Vec::new());
326 };
327 let Some(ranges) = python_deferred_annotation_identifier_ranges(string, source, None)
328 else {
329 return Some(Vec::new());
330 };
331 let mut candidates = Vec::new();
332 for range in ranges {
333 let Some(symbol) = source.get(range.start_byte..range.end_byte) else {
334 continue;
335 };
336 let mut symbol_candidates =
337 resolve_bare_annotation_symbol(graph, python, file, source, node, symbol);
338 if symbol_candidates.is_empty() {
339 symbol_candidates.extend(resolve_receiver_type(
340 graph,
341 python,
342 file,
343 symbol,
344 target_self_file,
345 ));
346 }
347 candidates.extend(symbol_candidates);
348 }
349 candidates
350 }
351 "attribute" => resolve_annotation_attribute_types(graph, python, file, source, node),
352 _ => Vec::new(),
353 };
354 candidates.sort();
355 candidates.dedup();
356 Some(candidates)
357}
358
359#[allow(clippy::too_many_arguments)]
363pub fn annotation_reference_candidates_at_focus(
364 graph: &PythonGraphSource<'_>,
365 python: &dyn PythonUsageSource,
366 file: &ProjectFile,
367 source: &str,
368 node: Node<'_>,
369 focus_start: usize,
370 focus_end: usize,
371 target_self_file: bool,
372) -> Option<Vec<CodeUnit>> {
373 if !is_annotation_reference_node(node, source) {
374 return None;
375 }
376
377 let deferred_tree = if node.kind() == "string_content" {
378 Some(python_deferred_annotation_tree(
379 node.parent()?,
380 source,
381 None,
382 )?)
383 } else {
384 None
385 };
386 let search_root = deferred_tree.as_ref().map_or(node, |tree| tree.root_node());
387 let focused = search_root.descendant_for_byte_range(focus_start, focus_end)?;
388 if focused.kind() != "identifier"
389 || focused.start_byte() != focus_start
390 || focused.end_byte() != focus_end
391 {
392 return Some(Vec::new());
393 }
394
395 let mut path = focused;
396 while let Some(parent) = path.parent() {
397 if parent.kind() != "attribute" {
398 break;
399 }
400 path = parent;
401 }
402 if path.kind() == "attribute" {
403 return Some(focused_annotation_attribute_candidates(
404 graph, python, file, source, node, path, focused,
405 ));
406 }
407
408 let symbol = node_text(focused, source);
409 let mut candidates = resolve_bare_annotation_symbol(graph, python, file, source, node, symbol);
410 if candidates.is_empty() {
411 candidates.extend(resolve_receiver_type(
412 graph,
413 python,
414 file,
415 symbol,
416 target_self_file,
417 ));
418 }
419 candidates.sort();
420 candidates.dedup();
421 Some(candidates)
422}
423
424fn focused_annotation_attribute_candidates(
425 graph: &PythonGraphSource<'_>,
426 python: &dyn PythonUsageSource,
427 file: &ProjectFile,
428 source: &str,
429 scope_node: Node<'_>,
430 path: Node<'_>,
431 focused: Node<'_>,
432) -> Vec<CodeUnit> {
433 let Some((root, attributes)) = annotation_attribute_chain(path) else {
434 return Vec::new();
435 };
436 if root.id() == focused.id() {
437 return resolve_bare_annotation_symbol(
438 graph,
439 python,
440 file,
441 source,
442 scope_node,
443 node_text(root, source),
444 );
445 }
446
447 if attributes
448 .last()
449 .is_some_and(|node| node.id() == focused.id())
450 {
451 let candidates = namespace_qualified_declarations(graph, python, file, source, path);
452 if !candidates.is_empty() {
453 return candidates;
454 }
455 }
456
457 let owners: Vec<_> = resolve_bare_annotation_symbol(
458 graph,
459 python,
460 file,
461 source,
462 scope_node,
463 node_text(root, source),
464 )
465 .into_iter()
466 .filter(CodeUnit::is_class)
467 .collect();
468 let [owner] = owners.as_slice() else {
469 return Vec::new();
470 };
471 let mut owner = owner.clone();
472 for attribute in attributes {
473 let candidates = exact_nested_annotation_class(graph, &owner, node_text(attribute, source));
474 if attribute.id() == focused.id() {
475 return candidates;
476 }
477 let [next] = candidates.as_slice() else {
478 return Vec::new();
479 };
480 owner = next.clone();
481 }
482 Vec::new()
483}
484
485pub fn annotation_class_qualifier_site<'tree>(
488 graph: &PythonGraphSource<'_>,
489 python: &dyn PythonUsageSource,
490 file: &ProjectFile,
491 source: &str,
492 node: Node<'tree>,
493 target: &CodeUnit,
494) -> Option<Node<'tree>> {
495 if node.kind() != "attribute"
496 || !target.is_class()
497 || !is_annotation_reference_node(node, source)
498 {
499 return None;
500 }
501
502 let (root, attributes) = annotation_attribute_chain(node)?;
503 let owners: Vec<_> =
504 resolve_bare_annotation_symbol(graph, python, file, source, root, node_text(root, source))
505 .into_iter()
506 .filter(CodeUnit::is_class)
507 .collect();
508 let [owner] = owners.as_slice() else {
509 return None;
510 };
511 let mut owner = owner.clone();
512 if &owner == target {
513 return Some(root);
514 }
515
516 let qualifier_count = attributes.len().saturating_sub(1);
519 for attribute in attributes.into_iter().take(qualifier_count) {
520 let next_candidates =
521 exact_nested_annotation_class(graph, &owner, node_text(attribute, source));
522 let [next] = next_candidates.as_slice() else {
523 return None;
524 };
525 owner = next.clone();
526 if &owner == target {
527 return Some(attribute);
528 }
529 }
530
531 None
532}
533
534fn resolve_annotation_attribute_types(
535 graph: &PythonGraphSource<'_>,
536 python: &dyn PythonUsageSource,
537 file: &ProjectFile,
538 source: &str,
539 node: Node<'_>,
540) -> Vec<CodeUnit> {
541 let mut candidates = namespace_qualified_declarations(graph, python, file, source, node);
546 let Some((root, attributes)) = annotation_attribute_chain(node) else {
547 return candidates;
548 };
549 let root_text = node_text(root, source);
550 let owners: Vec<_> =
551 resolve_bare_annotation_symbol(graph, python, file, source, root, root_text)
552 .into_iter()
553 .filter(CodeUnit::is_class)
554 .collect();
555 let [owner] = owners.as_slice() else {
556 return candidates;
557 };
558 let mut owner = owner.clone();
559
560 for attribute in attributes {
561 let segment = node_text(attribute, source);
562 let next_candidates = exact_nested_annotation_class(graph, &owner, segment);
563 let [next] = next_candidates.as_slice() else {
564 return candidates;
565 };
566 owner = next.clone();
567 }
568
569 candidates.push(owner);
570 candidates
571}
572
573fn annotation_attribute_chain(node: Node<'_>) -> Option<(Node<'_>, Vec<Node<'_>>)> {
574 let mut attributes = Vec::new();
575 let mut current = node;
576 while current.kind() == "attribute" {
577 attributes.push(current.child_by_field_name("attribute")?);
578 current = current.child_by_field_name("object")?;
579 }
580 if current.kind() != "identifier" || attributes.is_empty() {
581 return None;
582 }
583 attributes.reverse();
584 Some((current, attributes))
585}
586
587fn exact_nested_annotation_class(
588 graph: &PythonGraphSource<'_>,
589 owner: &CodeUnit,
590 segment: &str,
591) -> Vec<CodeUnit> {
592 let mut candidates: Vec<_> = exact_owner_annotation_members(graph, owner, segment)
593 .into_iter()
594 .filter(CodeUnit::is_class)
595 .collect();
596 candidates.sort();
597 candidates.dedup();
598 candidates
599}
600
601fn exact_owner_annotation_members(
602 graph: &PythonGraphSource<'_>,
603 owner: &CodeUnit,
604 segment: &str,
605) -> Vec<CodeUnit> {
606 let mut candidates: Vec<_> = graph
607 .index
608 .declarations(owner.source())
609 .into_iter()
610 .filter(|unit| {
611 unit.identifier() == segment
612 && graph
613 .index
614 .parent_of(unit)
615 .is_some_and(|parent| parent.fq_name() == owner.fq_name())
616 })
617 .collect();
618 candidates.sort();
619 candidates.dedup();
620 candidates
621}
622
623fn annotation_scope_owner_class(
624 graph: &PythonGraphSource<'_>,
625 file: &ProjectFile,
626 source: &str,
627 node: Node<'_>,
628) -> Option<CodeUnit> {
629 if !annotation_expression_is_class_scoped(node) {
630 return None;
631 }
632 let range = Range {
633 start_byte: node.start_byte(),
634 end_byte: node.end_byte(),
635 start_line: 0,
636 end_line: 0,
637 };
638 if let Some(enclosing) = graph.index.enclosing_code_unit(file, &range) {
639 if enclosing.is_class() {
640 return Some(enclosing);
641 }
642 if let Some(owner) = target_owner_code_unit(graph.index, &enclosing) {
643 return Some(owner);
644 }
645 }
646 structural_annotation_owner_class(graph, file, source, node)
647}
648
649fn annotation_expression_is_class_scoped(node: Node<'_>) -> bool {
650 let site_start = node.start_byte();
651 let site_end = node.end_byte();
652 let mut current = node;
653 while let Some(parent) = current.parent() {
654 if matches!(parent.kind(), "function_definition" | "lambda")
655 && parent
656 .child_by_field_name("body")
657 .is_some_and(|body| body.start_byte() <= site_start && site_end <= body.end_byte())
658 {
659 return false;
660 }
661 if parent.kind() == "class_definition" {
662 return true;
663 }
664 current = parent;
665 }
666 false
667}
668
669fn structural_annotation_owner_class(
670 graph: &PythonGraphSource<'_>,
671 file: &ProjectFile,
672 source: &str,
673 node: Node<'_>,
674) -> Option<CodeUnit> {
675 let mut current = node;
676 while let Some(parent) = current.parent() {
677 if parent.kind() == "class_definition" {
678 let name = node_text(parent.child_by_field_name("name")?, source).trim();
679 if name.is_empty() {
680 return None;
681 }
682 let class_range = Range {
683 start_byte: parent.start_byte(),
684 end_byte: parent.end_byte(),
685 start_line: 0,
686 end_line: 0,
687 };
688 let mut matches: Vec<_> = graph
689 .index
690 .declarations(file)
691 .into_iter()
692 .filter(|unit| unit.is_class() && unit.identifier() == name)
693 .filter(|unit| {
694 graph
695 .index
696 .ranges(unit)
697 .into_iter()
698 .any(|range| range.contains(&class_range))
699 })
700 .collect();
701 matches.sort();
702 matches.dedup();
703 let [owner] = matches.as_slice() else {
704 return None;
705 };
706 return Some(owner.clone());
707 }
708 current = parent;
709 }
710 None
711}
712
713fn is_annotation_reference_node(node: Node<'_>, source: &str) -> bool {
714 if !matches!(node.kind(), "identifier" | "attribute" | "string_content") {
715 return false;
716 }
717 if !python_node_is_in_annotation(node) {
718 return false;
719 }
720
721 let mut current = node;
722 while let Some(parent) = current.parent() {
723 if parent.kind() == "call"
724 && parent
725 .child_by_field_name("arguments")
726 .is_some_and(|arguments| {
727 arguments.start_byte() <= node.start_byte()
728 && node.end_byte() <= arguments.end_byte()
729 })
730 {
731 return false;
732 }
733 if parent.kind() == "subscript"
734 && let Some(value) = parent.child_by_field_name("value")
735 && value.kind() == "identifier"
736 {
737 match node_text(value, source) {
738 "Literal" => return false,
742 "Annotated" => {
745 let Some(arguments) = parent.child_by_field_name("subscript") else {
746 return false;
747 };
748 let type_argument = if arguments.kind() == "expression_list" {
749 arguments.named_child(0)
750 } else {
751 Some(arguments)
752 };
753 if !type_argument.is_some_and(|argument| {
754 argument.start_byte() <= node.start_byte()
755 && node.end_byte() <= argument.end_byte()
756 }) {
757 return false;
758 }
759 }
760 _ => {}
761 }
762 }
763 if parent.kind() == "type_parameter"
764 && let Some(generic) = parent
765 .parent()
766 .filter(|parent| parent.kind() == "generic_type")
767 {
768 let generic_name = generic
769 .child_by_field_name("name")
770 .or_else(|| generic.named_child(0))
771 .map(|name| node_text(name, source));
772 match generic_name {
773 Some("Literal") => return false,
774 Some("Annotated") => {
775 let type_argument = parent.named_child(0);
776 if !type_argument.is_some_and(|argument| {
777 argument.start_byte() <= node.start_byte()
778 && node.end_byte() <= argument.end_byte()
779 }) {
780 return false;
781 }
782 }
783 _ => {}
784 }
785 }
786 current = parent;
787 }
788 true
789}
790
791pub fn resolve_visible_named_import_candidates(
799 graph: &PythonGraphSource<'_>,
800 python: &dyn PythonUsageSource,
801 file: &ProjectFile,
802 timeline: &ModuleBindingTimeline,
803 local: &str,
804 cutoff: usize,
805) -> Vec<CodeUnit> {
806 let Some(events) = timeline.get(local) else {
807 return Vec::new();
808 };
809 let visible: Vec<_> = events
810 .iter()
811 .filter(|event| event.visible_from <= cutoff)
812 .collect();
813 let start = visible
814 .iter()
815 .rposition(|event| !event.conditional)
816 .unwrap_or(0);
817 let mut candidates = Vec::new();
818 for event in &visible[start..] {
819 let ModuleBindingEventKind::FromImport {
820 module,
821 imported_name,
822 } = &event.kind
823 else {
824 continue;
825 };
826 let mut resolved_module = false;
827 for module_file in usage_resolve_module_files(python, file, module) {
828 let Some(module_fqn) = graph
829 .index
830 .declarations(&module_file)
831 .into_iter()
832 .find(CodeUnit::is_module)
833 .map(|unit| unit.fq_name())
834 else {
835 continue;
836 };
837 resolved_module = true;
838 let fqn = format!("{module_fqn}.{imported_name}");
839 candidates.extend(resolve_fqn_candidates(python, &fqn, |name| {
840 graph.index.definitions(name).collect()
841 }));
842 }
843 if !resolved_module {
844 let fqn = if module.ends_with('.') {
845 format!("{module}{imported_name}")
846 } else {
847 format!("{module}.{imported_name}")
848 };
849 candidates.extend(resolve_fqn_candidates(python, &fqn, |name| {
850 graph.index.definitions(name).collect()
851 }));
852 }
853 }
854 candidates.sort();
855 candidates.dedup();
856 candidates
857}
858
859pub fn resolve_constructor_types(
864 graph: &PythonGraphSource<'_>,
865 python: &dyn PythonUsageSource,
866 file: &ProjectFile,
867 source: &str,
868 function: Node<'_>,
869) -> Vec<CodeUnit> {
870 let candidates = match function.kind() {
871 "identifier" => {
872 let local = node_text(function, source);
873 if local.is_empty() {
874 return Vec::new();
875 }
876 let binder = python.import_binder_of(file);
877 let fqn = match binder.bindings.get(local) {
878 Some(binding) if binding.kind == ImportKind::Named => binding
879 .imported_name
880 .as_ref()
881 .map(|imported| format!("{}.{}", binding.module_specifier, imported)),
882 _ => graph
883 .index
884 .declarations(file)
885 .into_iter()
886 .find(|unit| unit.is_class() && unit.identifier() == local)
887 .map(|unit| unit.fq_name()),
888 };
889 let Some(fqn) = fqn else {
890 return Vec::new();
891 };
892 resolve_fqn_candidates(python, &fqn, |name| graph.index.definitions(name).collect())
893 }
894 "attribute" => namespace_qualified_declarations(graph, python, file, source, function),
895 _ => Vec::new(),
896 };
897 let mut classes: Vec<CodeUnit> = candidates.into_iter().filter(CodeUnit::is_class).collect();
900 classes.sort();
901 classes.dedup();
902 classes
903}
904
905pub fn resolve_callable_parameter_default_types(
911 graph: &PythonGraphSource<'_>,
912 python: &dyn PythonUsageSource,
913 file: &ProjectFile,
914 source: &str,
915 reference: Node<'_>,
916 local_name: &str,
917) -> Vec<CodeUnit> {
918 let site_start = reference.start_byte();
919 let site_end = reference.end_byte();
920 let mut current = reference;
921 while let Some(parent) = current.parent() {
922 current = parent;
923 if !matches!(current.kind(), "function_definition" | "lambda") {
924 continue;
925 }
926 if current
927 .child_by_field_name("body")
928 .is_none_or(|body| !(body.start_byte() <= site_start && site_end <= body.end_byte()))
929 {
930 continue;
931 }
932 let Some(parameters) = current.child_by_field_name("parameters") else {
933 return Vec::new();
934 };
935 let mut cursor = parameters.walk();
936 for parameter in parameters.named_children(&mut cursor) {
937 let name = if parameter.kind() == "identifier" {
938 Some(parameter)
939 } else {
940 parameter.child_by_field_name("name")
941 };
942 if name.is_none_or(|name| node_text(name, source) != local_name) {
943 continue;
944 }
945 let Some(value) = parameter.child_by_field_name("value") else {
946 return Vec::new();
947 };
948 return resolve_constructor_types(graph, python, file, source, value);
949 }
950 return Vec::new();
951 }
952 Vec::new()
953}
954
955fn namespace_qualified_declarations(
962 graph: &PythonGraphSource<'_>,
963 python: &dyn PythonUsageSource,
964 file: &ProjectFile,
965 source: &str,
966 node: Node<'_>,
967) -> Vec<CodeUnit> {
968 let binder = python.import_binder_of(file);
969 let Some(fqn) = namespace_constructor_fqn(&binder, source, node) else {
970 return Vec::new();
971 };
972 let mut candidates =
973 resolve_fqn_candidates(python, &fqn, |name| graph.index.definitions(name).collect());
974 candidates.sort();
975 candidates.dedup();
976 candidates
977}
978
979fn namespace_constructor_fqn(
980 binder: &ImportBinder,
981 source: &str,
982 function: Node<'_>,
983) -> Option<String> {
984 let mut attributes = Vec::new();
985 let mut current = function;
986 while current.kind() == "attribute" {
987 let attribute = current.child_by_field_name("attribute")?;
988 let text = node_text(attribute, source);
989 if text.is_empty() {
990 return None;
991 }
992 attributes.push(text);
993 current = current.child_by_field_name("object")?;
994 }
995 if current.kind() != "identifier" {
996 return None;
997 }
998 let root = node_text(current, source);
999 let binding = binder.bindings.get(root)?;
1000 if binding.kind != ImportKind::Namespace {
1001 return None;
1002 }
1003 let mut fqn = binding.module_specifier.clone();
1004 for attribute in attributes.into_iter().rev() {
1005 fqn.push('.');
1006 fqn.push_str(attribute);
1007 }
1008 Some(fqn)
1009}
1010
1011fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
1012 brokk_bifrost_core::analyzer::common::node_source_text(node, source)
1013}
1014
1015fn resolve_indexed_receiver_type(
1016 graph: &PythonGraphSource<'_>,
1017 file: &ProjectFile,
1018 raw_type: &str,
1019) -> Option<CodeUnit> {
1020 let package_matches = module_fq_for_file(graph.index, file).map_or_else(Vec::new, |module| {
1021 relational_definitions(
1022 graph,
1023 RelationalName::stable(module),
1024 RelationalDefinitionQuery::PackageTypes {
1025 simple_name: raw_type.to_string(),
1026 },
1027 )
1028 });
1029 let mut root_name = FqName::new();
1030 root_name.push(segment_interner().intern(raw_type, SegmentKind::Type));
1031 let exact_matches = relational_definitions(
1032 graph,
1033 RelationalName::stable(root_name.clone()),
1034 RelationalDefinitionQuery::ExactName,
1035 );
1036 let normalized_matches = relational_definitions(
1037 graph,
1038 RelationalName::stable(root_name),
1039 RelationalDefinitionQuery::NormalizedName,
1040 );
1041 package_matches
1042 .into_iter()
1043 .chain(exact_matches)
1044 .chain(normalized_matches)
1045 .find(|code_unit| code_unit.identifier() == raw_type && code_unit.is_class())
1046}
1047
1048fn relational_definitions(
1049 graph: &PythonGraphSource<'_>,
1050 name: RelationalName,
1051 query: RelationalDefinitionQuery,
1052) -> Vec<CodeUnit> {
1053 let question = RelationalDefinitionQuestion {
1054 language_scope: DefinitionLanguageScope::Language(Language::Python),
1060 name,
1061 query,
1062 };
1063 match graph.definitions.ask(&question) {
1064 RelationalDefinitionValue::Definitions(units) => units,
1065 _ => panic!("definition question returned the wrong result shape"),
1066 }
1067}
1068
1069fn module_fq_for_file(index: &dyn CodeUnitIndex, file: &ProjectFile) -> Option<FqName> {
1070 index
1071 .declarations(file)
1072 .into_iter()
1073 .find(|code_unit| code_unit.is_module())
1074 .map(|code_unit| code_unit.fq().clone())
1075 .or_else(|| {
1076 index
1077 .declarations(file)
1078 .into_iter()
1079 .find(|code_unit| !code_unit.package_name().is_empty())
1080 .map(|code_unit| code_unit.package_fq())
1081 })
1082}
1083
1084pub fn normalized_receiver_type(annotation: &str) -> Option<String> {
1085 let annotation = unwrap_python_string_annotation(annotation.trim());
1086 let annotation = unwrap_supported_receiver_wrapper(annotation);
1087 if annotation.is_empty()
1088 || annotation.contains('|')
1089 || annotation.contains('[')
1090 || annotation.contains(']')
1091 || annotation.contains(',')
1092 || annotation.contains('(')
1093 || annotation.contains(')')
1094 || annotation.contains('{')
1095 || annotation.contains('}')
1096 || annotation.contains(':')
1097 {
1098 return None;
1099 }
1100 Some(annotation.to_string())
1101}
1102
1103fn unwrap_python_string_annotation(annotation: &str) -> &str {
1104 if annotation.len() >= 2 {
1105 let bytes = annotation.as_bytes();
1106 let first = bytes[0];
1107 let last = bytes[annotation.len() - 1];
1108 if (first == b'\'' || first == b'"') && first == last {
1109 return annotation[1..annotation.len() - 1].trim();
1110 }
1111 }
1112 annotation
1113}
1114
1115fn unwrap_supported_receiver_wrapper(annotation: &str) -> &str {
1116 let mut current = annotation.trim();
1117 loop {
1118 let next = current
1119 .strip_prefix("Optional[")
1120 .or_else(|| current.strip_prefix("typing.Optional["))
1121 .and_then(|inner| inner.strip_suffix(']'))
1122 .map(str::trim);
1123 let Some(unwrapped) = next else {
1124 return current;
1125 };
1126 current = unwrapped;
1127 }
1128}
1129
1130pub fn receiver_annotation_matches_target(
1131 annotation: &str,
1132 edges: &[ImportEdge],
1133 target_short: &str,
1134 target_self_file: bool,
1135) -> bool {
1136 let annotation = annotation.trim();
1137 if annotation.is_empty() {
1138 return false;
1139 }
1140 if annotation.contains('|')
1141 || annotation.contains('[')
1142 || annotation.contains(']')
1143 || annotation.contains(',')
1144 || annotation.contains('(')
1145 || annotation.contains(')')
1146 {
1147 return false;
1148 }
1149 if annotation == target_short {
1150 return target_self_file || edges.iter().any(|edge| edge.local_name == target_short);
1151 }
1152
1153 let segments = parse_symbol_path(Language::Python, annotation);
1159 let Some((member, qualifier_parts)) = segments.split_last() else {
1160 return false;
1161 };
1162 if qualifier_parts.is_empty() {
1163 return false;
1164 }
1165 let qualifier = qualifier_parts.join(".");
1166 let member = member.as_str();
1167 if member != target_short {
1168 return false;
1169 }
1170 edges.iter().any(|edge| {
1171 matches!(edge.kind, ImportEdgeKind::Namespace)
1172 && (edge.local_name == qualifier
1173 || qualifier.ends_with(&format!(".{}", edge.local_name)))
1174 })
1175}
1176
1177