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