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