1use crate::bindings::python_direct_scope_bindings_bounded;
8use crate::declarations::{parse_python_tree, py_node_text, python_module_name};
9use crate::graph_support::{
10 PythonSource, import_binder_from_imports, public_declarations_in_module,
11 resolve_module_code_unit, resolve_module_code_units_batch,
12};
13use crate::runtime_values::enclosing_execution_context;
14use crate::syntax::python_static_attribute_path;
15use brokk_bifrost_core::analyzer::common::node_source_text;
16use brokk_bifrost_core::analyzer::model::{
17 ImportInfo, StructuredImportPath, StructuredImportPathKind,
18};
19use brokk_bifrost_core::analyzer::query_token::QueryToken;
20use brokk_bifrost_core::analyzer::usages::model::{ExportEntry, ImportBinding, ImportKind};
21use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, ProjectFile};
22use brokk_bifrost_core::hash::{HashMap, HashSet};
23use std::collections::VecDeque;
24use tree_sitter::Node;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct PythonModuleReplacement {
28 pub target_module: String,
29}
30
31pub(crate) fn imported_module_assignment_at(
41 reference: Node<'_>,
42 local: &str,
43 source: &str,
44 mut is_importlib_binding: impl FnMut(&str) -> bool,
45) -> Option<String> {
46 let suite = enclosing_execution_suite(reference)?;
47 let mut resolved = None;
48 let mut cursor = suite.walk();
49 for statement in suite.named_children(&mut cursor) {
50 if statement.start_byte() >= reference.start_byte()
51 || (statement.start_byte() <= reference.start_byte()
52 && reference.end_byte() <= statement.end_byte())
53 {
54 break;
55 }
56
57 let expression = if statement.kind() == "expression_statement" {
58 statement.named_child(0).unwrap_or(statement)
59 } else {
60 statement
61 };
62 if expression.kind() == "assignment"
63 && expression.child_by_field_name("left").is_some_and(|left| {
64 left.kind() == "identifier" && node_source_text(left, source) == local
65 })
66 {
67 resolved = expression
68 .child_by_field_name("right")
69 .and_then(|right| importlib_call_module(right, source, &mut is_importlib_binding));
70 continue;
71 }
72
73 let binds_local = python_direct_scope_bindings_bounded(statement, source, || true)
74 .expect("unbounded binding collection cannot be cancelled")
75 .into_iter()
76 .any(|binding| node_source_text(binding.declaration, source) == local);
77 if binds_local {
78 resolved = None;
79 }
80 }
81 resolved
82}
83
84pub fn python_local_module_alias_path_before<E, C>(
92 reference: Node<'_>,
93 local_name: &str,
94 source: &str,
95 context: &mut C,
96 mut imported_module_path: impl FnMut(Node<'_>, &mut C) -> Result<Option<Box<[Box<str>]>>, E>,
97 mut charge_step: impl FnMut(&mut C) -> Result<(), E>,
98) -> Result<Option<Box<[Box<str>]>>, E> {
99 let mut current = reference;
100 let suite = loop {
101 let Some(parent) = current.parent() else {
102 return Ok(None);
103 };
104 match parent.kind() {
105 "function_definition" | "lambda" => break parent.child_by_field_name("body"),
106 "module" => break Some(parent),
107 "class_definition" => return Ok(None),
108 _ => current = parent,
109 }
110 };
111 let Some(suite) = suite else {
112 return Ok(None);
113 };
114
115 let mut aliases: HashMap<Box<str>, Box<[Box<str>]>> = HashMap::default();
116 let mut cursor = suite.walk();
117 for statement in suite.named_children(&mut cursor) {
118 if statement.start_byte() >= reference.start_byte()
119 || (statement.start_byte() <= reference.start_byte()
120 && reference.end_byte() <= statement.end_byte())
121 {
122 break;
123 }
124 charge_step(context)?;
125 let expression = if statement.kind() == "expression_statement" {
126 statement.named_child(0).unwrap_or(statement)
127 } else {
128 statement
129 };
130 if expression.kind() == "assignment" {
131 let (targets, value) = python_assignment_chain(expression);
132 if !targets.is_empty() && targets.iter().all(|target| target.kind() == "identifier") {
133 let module_path = value
134 .map(|value| {
135 python_module_alias_expression_path(
136 value,
137 source,
138 &aliases,
139 context,
140 &mut imported_module_path,
141 )
142 })
143 .transpose()?
144 .flatten();
145 for target in targets {
146 let name = node_source_text(target, source);
147 if let Some(module_path) = module_path.as_ref() {
148 aliases.insert(name.into(), module_path.clone());
149 } else {
150 aliases.remove(name);
151 }
152 }
153 continue;
154 }
155 }
156
157 if !matches!(
158 expression.kind(),
159 "assignment"
160 | "augmented_assignment"
161 | "delete_statement"
162 | "for_statement"
163 | "async_for_statement"
164 | "while_statement"
165 | "with_statement"
166 | "try_statement"
167 | "match_statement"
168 | "import_statement"
169 | "import_from_statement"
170 | "function_definition"
171 | "class_definition"
172 | "global_statement"
173 | "nonlocal_statement"
174 | "named_expression"
175 | "if_statement"
176 ) {
177 continue;
178 }
179 let mut binding_error = None;
180 let bindings =
181 python_direct_scope_bindings_bounded(statement, source, || {
182 match charge_step(context) {
183 Ok(()) => true,
184 Err(error) => {
185 binding_error = Some(error);
186 false
187 }
188 }
189 });
190 if let Some(error) = binding_error {
191 return Err(error);
192 }
193 let bindings = bindings.expect("bounded Python alias scan stopped without a cause");
194 for binding in bindings {
195 aliases.remove(node_source_text(binding.declaration, source));
196 }
197 }
198 Ok(aliases.get(local_name).cloned())
199}
200
201fn python_module_alias_expression_path<E, C>(
202 expression: Node<'_>,
203 source: &str,
204 aliases: &HashMap<Box<str>, Box<[Box<str>]>>,
205 context: &mut C,
206 imported_module_path: &mut impl FnMut(Node<'_>, &mut C) -> Result<Option<Box<[Box<str>]>>, E>,
207) -> Result<Option<Box<[Box<str>]>>, E> {
208 if let Some(path) = python_static_attribute_path(expression)
209 && let Some(name) = path
210 .first()
211 .map(|segment| node_source_text(*segment, source))
212 && let Some(module_path) = aliases.get(name)
213 {
214 let mut canonical = module_path.to_vec();
215 canonical.extend(path.iter().skip(1).map(|segment| {
216 node_source_text(*segment, source)
217 .to_owned()
218 .into_boxed_str()
219 }));
220 return Ok(Some(canonical.into_boxed_slice()));
221 }
222 imported_module_path(expression, context)
223}
224
225pub fn python_assignment_chain<'tree>(
229 node: Node<'tree>,
230) -> (Vec<Node<'tree>>, Option<Node<'tree>>) {
231 let mut targets = Vec::new();
232 let mut assignment = node;
233 loop {
234 let Some(target) = assignment.child_by_field_name("left") else {
235 return (targets, None);
236 };
237 targets.push(target);
238 let Some(value) = assignment.child_by_field_name("right") else {
239 return (targets, None);
240 };
241 if value.kind() != "assignment" {
242 return (targets, Some(value));
243 }
244 assignment = value;
245 }
246}
247
248fn enclosing_execution_suite(node: Node<'_>) -> Option<Node<'_>> {
249 if let Some(function) = enclosing_execution_context(node) {
250 return function.child_by_field_name("body");
251 }
252
253 let mut current = node;
254 while let Some(parent) = current.parent() {
255 if parent.kind() == "class_definition" {
256 return parent.child_by_field_name("body");
257 }
258 if parent.kind() == "module" {
259 return Some(parent);
260 }
261 current = parent;
262 }
263 None
264}
265
266fn importlib_call_module(
267 call: Node<'_>,
268 source: &str,
269 is_importlib_binding: &mut impl FnMut(&str) -> bool,
270) -> Option<String> {
271 if call.kind() != "call" {
272 return None;
273 }
274 let function = call.child_by_field_name("function")?;
275 if function.kind() != "attribute" {
276 return None;
277 }
278 let (object, attribute) = (
279 function.child_by_field_name("object")?,
280 function.child_by_field_name("attribute")?,
281 );
282 if object.kind() != "identifier"
283 || attribute.kind() != "identifier"
284 || node_source_text(attribute, source) != "import_module"
285 || !is_importlib_binding(node_source_text(object, source))
286 {
287 return None;
288 }
289
290 let arguments = call.child_by_field_name("arguments")?;
291 if arguments.named_child_count() != 1 {
292 return None;
293 }
294 let literal = arguments.named_child(0)?;
295 if literal.kind() != "string"
296 || literal
297 .parent()
298 .is_some_and(|parent| parent.kind() == "concatenated_string")
299 {
300 return None;
301 }
302 let mut content = None;
303 for index in 0..literal.named_child_count() {
304 let child = literal.named_child(index)?;
305 match child.kind() {
306 "string_start" | "string_end" => {}
307 "string_content" if content.is_none() && child.named_child_count() == 0 => {
308 content = Some(child);
309 }
310 _ => return None,
311 }
312 }
313 let content = content?;
314 let module = node_source_text(content, source);
315 (!module.is_empty() && !module.starts_with('.')).then(|| module.to_string())
316}
317
318pub(crate) fn literal_importlib_modules(
323 source: &str,
324 bindings: &HashMap<String, ImportBinding>,
325) -> HashSet<String> {
326 let Some(tree) = parse_python_tree(source) else {
327 return HashSet::default();
328 };
329 let mut modules = HashSet::default();
330 let mut stack = vec![tree.root_node()];
331 while let Some(node) = stack.pop() {
332 if node.kind() == "assignment"
333 && node
334 .child_by_field_name("left")
335 .is_some_and(|left| left.kind() == "identifier")
336 && let Some(module) = node.child_by_field_name("right").and_then(|right| {
337 importlib_call_module(right, source, &mut |local| {
338 bindings.get(local).is_some_and(|binding| {
339 binding.kind == ImportKind::Namespace
340 && binding
341 .namespace_imported_module
342 .as_deref()
343 .unwrap_or(&binding.module_specifier)
344 == "importlib"
345 })
346 })
347 })
348 {
349 modules.insert(module);
350 }
351 let mut cursor = node.walk();
352 stack.extend(node.named_children(&mut cursor));
353 }
354 modules
355}
356
357fn module_replacement_from_assignment(
365 assignment: Node<'_>,
366 source: &str,
367 bindings: &HashMap<String, ImportBinding>,
368) -> Option<PythonModuleReplacement> {
369 let (left, right) = (
370 assignment.child_by_field_name("left")?,
371 assignment.child_by_field_name("right")?,
372 );
373 if left.kind() != "subscript" || right.kind() != "identifier" {
374 return None;
375 }
376 let (value, subscript) = (
377 left.child_by_field_name("value")?,
378 left.child_by_field_name("subscript")?,
379 );
380 if value.kind() != "attribute"
381 || subscript.kind() != "identifier"
382 || node_source_text(subscript, source) != "__name__"
383 {
384 return None;
385 }
386 let (sys_local, modules) = (
387 value.child_by_field_name("object")?,
388 value.child_by_field_name("attribute")?,
389 );
390 if sys_local.kind() != "identifier"
391 || modules.kind() != "identifier"
392 || node_source_text(modules, source) != "modules"
393 {
394 return None;
395 }
396 let sys_binding = bindings.get(node_source_text(sys_local, source))?;
397 let sys_module = sys_binding
398 .namespace_imported_module
399 .as_deref()
400 .unwrap_or(&sys_binding.module_specifier);
401 if sys_binding.kind != ImportKind::Namespace || sys_module != "sys" {
402 return None;
403 }
404
405 let target_binding = bindings.get(node_source_text(right, source))?;
406 if target_binding.kind != ImportKind::Namespace {
407 return None;
408 }
409 Some(PythonModuleReplacement {
410 target_module: target_binding
411 .namespace_imported_module
412 .clone()
413 .unwrap_or_else(|| target_binding.module_specifier.clone()),
414 })
415}
416
417fn remove_direct_scope_bindings(
418 statement: Node<'_>,
419 source: &str,
420 bindings: &mut HashMap<String, ImportBinding>,
421) {
422 let mut stack = vec![statement];
423 while let Some(node) = stack.pop() {
424 for binding in python_direct_scope_bindings_bounded(node, source, || true)
425 .expect("unbounded binding collection cannot be cancelled")
426 {
427 bindings.remove(node_source_text(binding.declaration, source));
428 }
429 if matches!(
430 node.kind(),
431 "function_definition" | "class_definition" | "lambda"
432 ) {
433 continue;
434 }
435 let mut cursor = node.walk();
436 stack.extend(node.named_children(&mut cursor));
437 }
438}
439
440fn python_import_statement_nodes<'tree>(root: Node<'tree>) -> Vec<Node<'tree>> {
441 let mut pending = vec![root];
442 let mut nodes = Vec::new();
443 while let Some(node) = pending.pop() {
444 if matches!(node.kind(), "import_statement" | "import_from_statement") {
445 nodes.push(node);
446 continue;
447 }
448 let mut cursor = node.walk();
449 pending.extend(node.named_children(&mut cursor));
450 }
451 nodes
452}
453
454#[derive(Debug, Clone, PartialEq, Eq)]
455pub struct PythonImportBinding {
456 pub start_byte: usize,
457 pub scope_start_byte: usize,
458 pub scope_end_byte: usize,
459 function_scoped: bool,
460 pub local_name: String,
461 pub qualified_name: String,
462 pub consumed_attributes: usize,
463}
464
465impl PythonImportBinding {
466 pub fn is_function_scoped(&self) -> bool {
467 self.function_scoped
468 }
469}
470
471pub fn python_function_scoped_import_binding<'a>(
473 bindings: &'a [PythonImportBinding],
474 name: &str,
475 reference: Node<'_>,
476) -> Option<&'a PythonImportBinding> {
477 bindings.iter().rev().find(|binding| {
478 binding.is_function_scoped()
479 && binding.start_byte <= reference.start_byte()
480 && binding.scope_start_byte <= reference.start_byte()
481 && reference.end_byte() <= binding.scope_end_byte
482 && binding.local_name == name
483 })
484}
485
486pub fn python_import_bindings_from_tree(root: Node<'_>, source: &str) -> Vec<PythonImportBinding> {
490 let mut nodes = python_import_statement_nodes(root);
491 nodes.sort_by_key(Node::start_byte);
492 nodes
493 .into_iter()
494 .flat_map(|node| {
495 let (scope_start_byte, scope_end_byte, function_scoped) =
496 python_import_binding_scope(node, source.len());
497 python_import_infos_from_node(node, source)
498 .into_iter()
499 .filter_map(move |import| {
500 let path = import.path.as_ref()?;
501 let details = python_import_details(&import)?;
502 match details {
503 PythonImportDetails::Import { module, alias } => {
504 let consumed_attributes = if alias.is_some() {
505 0
506 } else {
507 path.segments.len().saturating_sub(1)
508 };
509 Some(PythonImportBinding {
510 start_byte: node.start_byte(),
511 scope_start_byte,
512 scope_end_byte,
513 function_scoped,
514 local_name: import.identifier.clone()?,
515 qualified_name: module,
516 consumed_attributes,
517 })
518 }
519 PythonImportDetails::FromImport {
520 module,
521 name,
522 alias: _,
523 wildcard: false,
524 } => Some(PythonImportBinding {
525 start_byte: node.start_byte(),
526 scope_start_byte,
527 scope_end_byte,
528 function_scoped,
529 local_name: import.identifier.clone()?,
530 qualified_name: format!("{module}.{name}"),
531 consumed_attributes: 0,
532 }),
533 PythonImportDetails::FromImport { wildcard: true, .. } => None,
534 }
535 })
536 })
537 .collect()
538}
539
540pub fn python_import_bindings_from_imports(
547 imports: &[ImportInfo],
548 root: Node<'_>,
549 source_len: usize,
550) -> Vec<PythonImportBinding> {
551 python_import_bindings_from_imports_bounded(imports, root, source_len, || true)
552 .expect("unbounded import projection completes")
553}
554
555pub fn python_import_bindings_from_imports_bounded(
558 imports: &[ImportInfo],
559 root: Node<'_>,
560 source_len: usize,
561 mut scope_step: impl FnMut() -> bool,
562) -> Option<Vec<PythonImportBinding>> {
563 let mut bindings = Vec::new();
564 for import in imports {
565 if !scope_step() {
566 return None;
567 }
568 let Some(path) = import.path.as_ref() else {
569 continue;
570 };
571 for _segment in &path.segments {
572 if !scope_step() {
573 return None;
574 }
575 }
576 let Some(details) = python_import_details(import) else {
577 continue;
578 };
579 let mut binding_scope = None;
580 let mut valid = true;
581 for scope in path.lexical_scopes.iter().rev() {
582 if !scope_step() {
583 return None;
584 }
585 let Some(node) = root.descendant_for_byte_range(scope.start_byte, scope.end_byte)
586 else {
587 valid = false;
588 break;
589 };
590 if node.start_byte() != scope.start_byte || node.end_byte() != scope.end_byte {
591 valid = false;
592 break;
593 }
594 if matches!(node.kind(), "function_definition" | "lambda") {
595 binding_scope = Some((scope.start_byte, scope.end_byte, true));
596 break;
597 }
598 }
599 if !valid {
600 continue;
601 }
602 let (scope_start_byte, scope_end_byte, function_scoped) =
603 binding_scope.unwrap_or((0, source_len, false));
604 let Some(local_name) = import.identifier.clone() else {
605 continue;
606 };
607 let (qualified_name, consumed_attributes) = match details {
608 PythonImportDetails::Import { module, alias } => (
609 module,
610 if alias.is_some() {
611 0
612 } else {
613 path.segments.len().saturating_sub(1)
614 },
615 ),
616 PythonImportDetails::FromImport {
617 module,
618 name,
619 wildcard: false,
620 ..
621 } => (format!("{module}.{name}"), 0),
622 PythonImportDetails::FromImport { wildcard: true, .. } => continue,
623 };
624 bindings.push(PythonImportBinding {
625 start_byte: path.declaration_start_byte,
626 scope_start_byte,
627 scope_end_byte,
628 function_scoped,
629 local_name,
630 qualified_name,
631 consumed_attributes,
632 });
633 }
634 bindings.sort_by_key(|binding| binding.start_byte);
635 Some(bindings)
636}
637
638fn python_import_binding_scope(node: Node<'_>, source_len: usize) -> (usize, usize, bool) {
639 let mut parent = node.parent();
640 while let Some(scope) = parent {
641 if matches!(scope.kind(), "function_definition" | "lambda") {
642 return (scope.start_byte(), scope.end_byte(), true);
643 }
644 parent = scope.parent();
645 }
646 (0, source_len, false)
647}
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652 use brokk_bifrost_core::analyzer::usages::model::ImportBinder;
653 use brokk_bifrost_test_macros::hardwon;
654
655 fn importlib_assignment_for(source: &str, importlib_local: &str) -> Option<String> {
656 let tree = parse_python_tree(source).expect("valid Python fixture");
657 let reference_start = source.rfind("canonical.setup").expect("reference site");
658 let mut stack = vec![tree.root_node()];
659 let mut reference = None;
660 while let Some(node) = stack.pop() {
661 if node.kind() == "identifier"
662 && node.start_byte() == reference_start
663 && node_source_text(node, source) == "canonical"
664 {
665 reference = Some(node);
666 break;
667 }
668 let mut cursor = node.walk();
669 stack.extend(node.named_children(&mut cursor));
670 }
671 imported_module_assignment_at(
672 reference.expect("canonical reference node"),
673 "canonical",
674 source,
675 |local| local == importlib_local,
676 )
677 }
678
679 #[test]
680 fn importlib_assignment_requires_a_direct_literal_dominating_write() {
681 let valid = r#"import importlib as loader
682
683def test():
684 canonical = loader.import_module("routes.contacts.contacts_routes")
685 assert canonical.setup_contacts_routes
686"#;
687 assert_eq!(
688 importlib_assignment_for(valid, "loader"),
689 Some("routes.contacts.contacts_routes".to_string())
690 );
691
692 for near_miss in [
693 r#"import importlib as loader
694def test(module_name):
695 canonical = loader.import_module(module_name)
696 assert canonical.setup_contacts_routes
697"#,
698 r#"import importlib as loader
699def test():
700 if enabled:
701 canonical = loader.import_module("routes.contacts.contacts_routes")
702 assert canonical.setup_contacts_routes
703"#,
704 r#"import importlib as loader
705def test():
706 canonical = loader.import_module("routes.contacts.contacts_routes")
707 canonical = object()
708 assert canonical.setup_contacts_routes
709"#,
710 ] {
711 assert_eq!(
712 importlib_assignment_for(near_miss, "loader"),
713 None,
714 "near miss must not infer a module receiver: {near_miss:?}"
715 );
716 }
717 assert_eq!(
718 importlib_assignment_for(valid, "shadowed_loader"),
719 None,
720 "a similarly shaped non-importlib receiver must not resolve"
721 );
722 }
723
724 fn replacement_for(source: &str, binder: &ImportBinder) -> Option<PythonModuleReplacement> {
725 let tree = parse_python_tree(source).expect("valid Python fixture");
726 let root = tree.root_node();
727 let mut replacement = None;
728 let mut cursor = root.walk();
729 for statement in root.named_children(&mut cursor) {
730 let statement = if statement.kind() == "expression_statement" {
731 statement.named_child(0).expect("fixture expression")
732 } else {
733 statement
734 };
735 if statement.kind() == "assignment"
736 && let Some(next) =
737 module_replacement_from_assignment(statement, source, &binder.bindings)
738 && replacement.replace(next).is_some()
739 {
740 return None;
741 }
742 }
743 replacement
744 }
745
746 #[test]
747 #[hardwon("59dfb80c3bb8: module replacement accepted malformed or shadowed import shapes")]
748 fn module_replacement_requires_exact_structured_import_bindings() {
749 let source = r#"import sys as _sys
750from routes.contacts import contacts_routes as _canonical
751
752_sys.modules[__name__] = _canonical
753"#;
754 let mut binder = ImportBinder::empty();
755 binder.bindings.insert(
756 "_sys".to_string(),
757 ImportBinding {
758 module_specifier: "sys".to_string(),
759 namespace_imported_module: Some("sys".to_string()),
760 kind: ImportKind::Namespace,
761 imported_name: None,
762 },
763 );
764 binder.bindings.insert(
765 "_canonical".to_string(),
766 ImportBinding {
767 module_specifier: "routes.contacts.contacts_routes".to_string(),
768 namespace_imported_module: None,
769 kind: ImportKind::Namespace,
770 imported_name: None,
771 },
772 );
773
774 assert_eq!(
775 replacement_for(source, &binder),
776 Some(PythonModuleReplacement {
777 target_module: "routes.contacts.contacts_routes".to_string(),
778 })
779 );
780
781 for near_miss in [
782 "cache.modules[__name__] = _canonical\n",
783 "_sys.modules[module_name] = _canonical\n",
784 "_sys.modules[__name__] = build()\n",
785 "def replace():\n _sys.modules[__name__] = _canonical\n",
786 ] {
787 assert_eq!(
788 replacement_for(near_miss, &binder),
789 None,
790 "near miss must not replace module identity: {near_miss:?}"
791 );
792 }
793 }
794
795 #[test]
796 #[hardwon(
797 "b58235b247fd: Python import bindings lost function scope or relative module identity"
798 )]
799 fn existing_tree_import_bindings_preserve_function_scope_and_relative_identity() {
800 let source = r#"def register():
801 from .workers.special import Worker
802 return Worker()
803"#;
804 let tree = parse_python_tree(source).expect("Python tree");
805 let bindings = python_import_bindings_from_tree(tree.root_node(), source);
806
807 assert_eq!(bindings.len(), 1, "{bindings:#?}");
808 assert!(bindings[0].is_function_scoped(), "{bindings:#?}");
809 assert_eq!(bindings[0].local_name, "Worker");
810 assert_eq!(bindings[0].qualified_name, ".workers.special.Worker");
811 assert!(bindings[0].scope_start_byte <= bindings[0].start_byte);
812 assert!(bindings[0].scope_end_byte >= source.rfind("Worker()").unwrap());
813 }
814}
815
816pub(crate) fn python_imports_by_statement(imports: &[ImportInfo]) -> HashMap<usize, &[ImportInfo]> {
819 let mut statement_imports = HashMap::default();
820 for group in imports.chunk_by(|left, right| {
821 left.path.as_ref().map(|path| path.declaration_start_byte)
822 == right.path.as_ref().map(|path| path.declaration_start_byte)
823 }) {
824 if let Some(path) = &group[0].path {
825 assert!(
826 statement_imports
827 .insert(path.declaration_start_byte, group)
828 .is_none(),
829 "canonical Python import leaves must remain in statement order"
830 );
831 }
832 }
833 statement_imports
834}
835
836pub fn module_replacement_of(
837 python: &dyn PythonSource,
838 file: &ProjectFile,
839 source: &str,
840 imports: &[ImportInfo],
841) -> Option<PythonModuleReplacement> {
842 let tree = parse_python_tree(source)?;
843 let root = tree.root_node();
844 let mut bindings: HashMap<String, ImportBinding> = HashMap::default();
845 let statement_imports = python_imports_by_statement(imports);
846 let mut replacement = None;
847 let mut cursor = root.walk();
848 for statement in root.named_children(&mut cursor) {
849 let statement = if statement.kind() == "expression_statement" {
850 let Some(expression) = statement.named_child(0) else {
851 continue;
852 };
853 expression
854 } else {
855 statement
856 };
857 match statement.kind() {
858 "import_statement" | "import_from_statement" => {
859 if let Some(imports) = statement_imports.get(&statement.start_byte()) {
860 bindings.extend(import_binder_from_imports(python, file, imports).bindings);
861 }
862 }
863 "assignment" => {
864 if let Some(next) = module_replacement_from_assignment(statement, source, &bindings)
865 && replacement.replace(next).is_some()
866 {
867 return None;
868 }
869 remove_direct_scope_bindings(statement, source, &mut bindings);
870 }
871 _ => remove_direct_scope_bindings(statement, source, &mut bindings),
872 }
873 }
874 replacement
875}
876
877pub fn resolve_import_bindings(
878 python: &dyn PythonSource,
879 token: QueryToken<'_>,
880 file: &ProjectFile,
881) -> HashMap<String, CodeUnit> {
882 let imports = python.import_info_of(token, file);
883 let mut bindings = HashMap::default();
884 for resolved in resolve_imports_batched(python, file, &imports) {
885 for (binding, code_unit) in resolved {
886 bindings.insert(binding, code_unit);
887 }
888 }
889 bindings
890}
891
892pub fn resolve_imports_batched(
899 python: &dyn PythonSource,
900 file: &ProjectFile,
901 imports: &[ImportInfo],
902) -> Vec<Vec<(String, CodeUnit)>> {
903 let primary_fqns: Vec<Option<String>> = imports
904 .iter()
905 .map(|import| primary_module_fqn(python, file, import))
906 .collect();
907 let to_resolve: Vec<String> = primary_fqns.iter().flatten().cloned().collect();
908 let mut batch_results =
909 resolve_module_code_units_batch(python, Some(file), &to_resolve).into_iter();
910
911 imports
912 .iter()
913 .zip(primary_fqns.iter())
914 .map(|(import, primary_fqn)| {
915 let hint = primary_fqn.as_ref().map(|_| batch_results.next().unwrap());
916 resolve_import_with_hint(python, file, import, hint.as_ref())
917 })
918 .collect()
919}
920
921pub fn resolve_import_files_batched(
931 python: &dyn PythonSource,
932 file: &ProjectFile,
933 imports: &[ImportInfo],
934) -> HashSet<ProjectFile> {
935 let mut module_fqns = Vec::new();
936 for import in imports {
937 match python_import_details(import) {
938 Some(PythonImportDetails::Import { module, .. }) => module_fqns.push(module),
939 Some(PythonImportDetails::FromImport {
940 module,
941 name,
942 wildcard,
943 ..
944 }) => {
945 let resolved_module = if module.starts_with('.') {
946 resolve_python_relative_module(python.project(), file, &module)
947 } else {
948 Some(module)
949 };
950 let Some(resolved_module) = resolved_module else {
951 continue;
952 };
953 if !wildcard {
954 module_fqns.push(format!("{resolved_module}.{name}"));
955 }
956 module_fqns.push(resolved_module);
957 }
958 None => {}
959 }
960 }
961 module_fqns.sort();
962 module_fqns.dedup();
963
964 python
965 .path_module_fqns_batch(&module_fqns)
966 .into_iter()
967 .flatten()
968 .flatten()
969 .filter(CodeUnit::is_module)
970 .map(|unit| unit.source().clone())
971 .collect()
972}
973
974fn primary_module_fqn(
978 python: &dyn PythonSource,
979 file: &ProjectFile,
980 import: &ImportInfo,
981) -> Option<String> {
982 match python_import_details(import)? {
983 PythonImportDetails::Import { module, alias } => Some(python_namespace_binding_module(
984 import,
985 alias.as_deref(),
986 &module,
987 )),
988 PythonImportDetails::FromImport {
989 module,
990 name,
991 wildcard,
992 ..
993 } => {
994 if wildcard {
995 return None;
996 }
997 let resolved_module = resolve_python_module_specifier(python.project(), file, &module);
998 resolved_module.map(|resolved_module| format!("{resolved_module}.{name}"))
999 }
1000 }
1001}
1002
1003fn resolve_import_with_hint(
1006 python: &dyn PythonSource,
1007 file: &ProjectFile,
1008 import: &ImportInfo,
1009 primary_hint: Option<&Option<CodeUnit>>,
1010) -> Vec<(String, CodeUnit)> {
1011 if let Some(details) = python_import_details(import) {
1012 match details {
1013 PythonImportDetails::Import { module, alias } => {
1014 let binding = python_namespace_binding_name(import, alias.as_deref(), &module);
1015 let bound_module =
1016 python_namespace_binding_module(import, alias.as_deref(), &module);
1017 let resolved = match primary_hint {
1018 Some(hint) => hint.clone(),
1019 None => resolve_module_code_unit(python, Some(file), &bound_module),
1020 };
1021 if let Some(module_code_unit) = resolved {
1022 return vec![(binding, module_code_unit)];
1023 }
1024 }
1025 PythonImportDetails::FromImport {
1026 module,
1027 name,
1028 alias,
1029 wildcard,
1030 } => {
1031 let resolved_module =
1032 resolve_python_module_specifier(python.project(), file, &module);
1033 let Some(resolved_module) = resolved_module else {
1034 return Vec::new();
1035 };
1036 if wildcard {
1037 return public_declarations_in_module(python, file, &resolved_module)
1038 .into_iter()
1039 .map(|code_unit| (code_unit.identifier().to_string(), code_unit))
1040 .collect();
1041 }
1042
1043 let binding = alias.clone().unwrap_or_else(|| name.clone());
1044 let module_candidate = format!("{resolved_module}.{name}");
1045 let exported =
1046 resolve_exported_name_from_module(python, Some(file), &resolved_module, &name);
1047 if !exported.is_empty() {
1048 return exported
1049 .into_iter()
1050 .map(|code_unit| (binding.clone(), code_unit))
1051 .collect();
1052 }
1053 let resolved = match primary_hint {
1054 Some(hint) => hint.clone(),
1055 None => resolve_module_code_unit(python, Some(file), &module_candidate),
1056 };
1057 if let Some(code_unit) = resolved {
1058 return vec![(binding, code_unit)];
1059 }
1060 let definitions: Vec<_> = python.definitions(&module_candidate).collect();
1061 if !definitions.is_empty() {
1062 return definitions
1063 .into_iter()
1064 .map(|code_unit| (binding.clone(), code_unit))
1065 .collect();
1066 }
1067 let package_candidate: Vec<_> = python
1068 .definitions(&format!("{resolved_module}.{name}"))
1069 .collect();
1070 if !package_candidate.is_empty() {
1071 return package_candidate
1072 .into_iter()
1073 .map(|code_unit| (binding.clone(), code_unit))
1074 .collect();
1075 }
1076 }
1077 }
1078 }
1079 Vec::new()
1080}
1081
1082pub fn resolve_exported_fqn(
1086 python: &dyn PythonSource,
1087 importer: Option<&ProjectFile>,
1088 fqn: &str,
1089) -> Vec<CodeUnit> {
1090 let Some((module, name)) = fqn.rsplit_once('.') else {
1091 return Vec::new();
1092 };
1093 resolve_exported_name_from_module(python, importer, module, name)
1094}
1095
1096fn resolve_direct_named_exported_fqn(
1105 python: &dyn PythonSource,
1106 importer: Option<&ProjectFile>,
1107 fqn: &str,
1108) -> Option<Vec<CodeUnit>> {
1109 let (module, name) = fqn.rsplit_once('.')?;
1110 let mut results = Vec::new();
1111 let mut queue = VecDeque::from([(importer.cloned(), module.to_string(), name.to_string())]);
1112 let mut visited = HashSet::default();
1113
1114 while let Some((importer, module, export_name)) = queue.pop_front() {
1115 if !visited.insert((module.clone(), export_name.clone())) {
1116 continue;
1117 }
1118 let module_unit = resolve_module_code_unit(python, importer.as_ref(), &module)?;
1119 let file = module_unit.source();
1120 let local = local_export_declarations(python, file, &export_name);
1121 let binder = python.import_binder_of(file);
1122 let binding = binder.bindings.get(&export_name);
1123 if !local.is_empty() && binding.is_some() {
1124 return None;
1125 }
1126 if !local.is_empty() {
1127 results.extend(local);
1128 continue;
1129 }
1130 let binding = binding?;
1131 if binding.kind != ImportKind::Named {
1132 return None;
1133 }
1134 let imported_name = binding.imported_name.as_ref()?;
1135 queue.push_back((
1136 Some(file.clone()),
1137 binding.module_specifier.clone(),
1138 imported_name.clone(),
1139 ));
1140 }
1141
1142 results.sort_by(|left, right| {
1143 left.source()
1144 .cmp(right.source())
1145 .then_with(|| left.fq_name().cmp(&right.fq_name()))
1146 });
1147 results.dedup();
1148 (!results.is_empty()).then_some(results)
1149}
1150
1151pub fn resolve_fqn_candidates(
1160 python: &dyn PythonSource,
1161 importer: Option<&ProjectFile>,
1162 fqn: &str,
1163 exact: impl FnOnce(&str) -> Vec<CodeUnit>,
1164) -> Vec<CodeUnit> {
1165 if let Some(candidates) = resolve_direct_named_exported_fqn(python, importer, fqn) {
1166 return candidates;
1167 }
1168 let candidates = resolve_exported_fqn(python, importer, fqn);
1169 if !candidates.is_empty() {
1170 return candidates;
1171 }
1172 exact(fqn)
1173}
1174
1175fn resolve_exported_name_from_module(
1176 python: &dyn PythonSource,
1177 importer: Option<&ProjectFile>,
1178 module: &str,
1179 name: &str,
1180) -> Vec<CodeUnit> {
1181 let Some(module_unit) = resolve_module_code_unit(python, importer, module) else {
1182 return Vec::new();
1183 };
1184 resolve_exported_name(python, module_unit.source(), name)
1185}
1186
1187fn resolve_exported_name(
1188 python: &dyn PythonSource,
1189 module_file: &ProjectFile,
1190 name: &str,
1191) -> Vec<CodeUnit> {
1192 let mut results = Vec::new();
1193 let mut queue = VecDeque::from([(module_file.clone(), name.to_string())]);
1194 let mut visited = HashSet::default();
1195
1196 while let Some((file, export_name)) = queue.pop_front() {
1197 if !visited.insert((file.clone(), export_name.clone())) {
1198 continue;
1199 }
1200
1201 let index = python.export_index_of(&file);
1202 if let Some(entry) = index.exports_by_name.get(&export_name) {
1203 match entry {
1204 ExportEntry::Local { local_name } => {
1205 results.extend(local_export_declarations(python, &file, local_name));
1206 }
1207 ExportEntry::ReexportedNamed {
1208 module_specifier,
1209 imported_name,
1210 } => {
1211 for target_file in
1212 resolve_module_files_for_export(python, &file, module_specifier)
1213 {
1214 queue.push_back((target_file, imported_name.clone()));
1215 }
1216 }
1217 ExportEntry::ReexportedModule { module_specifier } => {
1218 results.extend(resolve_module_code_unit(
1221 python,
1222 Some(&file),
1223 module_specifier,
1224 ));
1225 }
1226 ExportEntry::Default { local_name } => {
1227 if let Some(local_name) = local_name {
1228 results.extend(local_export_declarations(python, &file, local_name));
1229 }
1230 }
1231 }
1232 continue;
1233 }
1234
1235 if !export_name.starts_with('_') {
1236 for star in &index.reexport_stars {
1237 for target_file in
1238 resolve_module_files_for_export(python, &file, &star.module_specifier)
1239 {
1240 queue.push_back((target_file, export_name.clone()));
1241 }
1242 }
1243 }
1244 }
1245
1246 results.sort_by(|left, right| {
1247 left.source()
1248 .cmp(right.source())
1249 .then_with(|| left.fq_name().cmp(&right.fq_name()))
1250 });
1251 results.dedup();
1252 results
1253}
1254
1255fn local_export_declarations(
1256 index: &dyn CodeUnitIndex,
1257 file: &ProjectFile,
1258 local_name: &str,
1259) -> Vec<CodeUnit> {
1260 index
1261 .top_level_declarations(file)
1262 .into_iter()
1263 .filter(|unit| {
1264 unit.identifier() == local_name
1265 && index
1266 .parent_of(unit)
1267 .is_some_and(|parent| parent.is_module() && parent.source() == file)
1268 })
1269 .collect()
1270}
1271
1272fn resolve_module_files_for_export(
1273 python: &dyn PythonSource,
1274 importing_file: &ProjectFile,
1275 module_specifier: &str,
1276) -> Vec<ProjectFile> {
1277 let resolved_module =
1278 resolve_python_module_specifier(python.project(), importing_file, module_specifier);
1279 let Some(resolved_module) = resolved_module else {
1280 return Vec::new();
1281 };
1282 resolve_module_code_unit(python, Some(importing_file), &resolved_module)
1286 .map(|unit| vec![unit.source().clone()])
1287 .unwrap_or_default()
1288}
1289
1290pub fn extract_package_from_python_wildcard(import: &ImportInfo) -> Option<String> {
1291 let details = python_import_details(import)?;
1292 match details {
1293 PythonImportDetails::FromImport {
1294 module, wildcard, ..
1295 } if wildcard => Some(module),
1296 _ => None,
1297 }
1298}
1299
1300#[derive(Debug, Clone)]
1301pub enum PythonImportDetails {
1302 Import {
1303 module: String,
1304 alias: Option<String>,
1305 },
1306 FromImport {
1307 module: String,
1308 name: String,
1309 alias: Option<String>,
1310 wildcard: bool,
1311 },
1312}
1313
1314#[derive(Debug, Clone)]
1321pub struct PythonImportSyntax<'tree> {
1322 pub info: ImportInfo,
1323 pub module: Node<'tree>,
1324 pub member: Option<Node<'tree>>,
1325 pub leaf: Node<'tree>,
1326 pub declaration: Node<'tree>,
1327 pub target: Option<Node<'tree>>,
1328 pub alias: Option<Node<'tree>>,
1329}
1330
1331pub fn python_import_infos_from_node(node: Node<'_>, source: &str) -> Vec<ImportInfo> {
1332 python_import_syntaxes_from_node(node, source)
1333 .into_iter()
1334 .map(|syntax| syntax.info)
1335 .collect()
1336}
1337
1338pub fn python_import_syntaxes_from_node<'tree>(
1342 node: Node<'tree>,
1343 source: &str,
1344) -> Vec<PythonImportSyntax<'tree>> {
1345 match node.kind() {
1346 "import_statement" => python_namespace_import_syntaxes(node, source),
1347 "import_from_statement" => python_from_import_syntaxes(node, source),
1348 _ => Vec::new(),
1349 }
1350}
1351
1352pub fn python_import_details(import: &ImportInfo) -> Option<PythonImportDetails> {
1353 let path = import.path.as_ref()?;
1354 match path.kind? {
1355 StructuredImportPathKind::Namespace => Some(PythonImportDetails::Import {
1356 module: join_python_import_segments(&path.segments),
1357 alias: import.alias.clone(),
1358 }),
1359 StructuredImportPathKind::StaticMember | StructuredImportPathKind::ExternCrate => None,
1361 StructuredImportPathKind::FileInclude | StructuredImportPathKind::DynamicInclude => None,
1363 StructuredImportPathKind::ImportFrom => {
1364 let (name, module_segments) = if import.is_wildcard {
1365 ("*".to_string(), path.segments.as_slice())
1366 } else {
1367 let (name, module_segments) = path.segments.split_last()?;
1368 (name.clone(), module_segments)
1369 };
1370 Some(PythonImportDetails::FromImport {
1371 module: join_python_import_segments(module_segments),
1372 name,
1373 alias: import.alias.clone(),
1374 wildcard: import.is_wildcard,
1375 })
1376 }
1377 }
1378}
1379
1380fn python_namespace_import_syntaxes<'tree>(
1381 node: Node<'tree>,
1382 source: &str,
1383) -> Vec<PythonImportSyntax<'tree>> {
1384 let mut infos = Vec::new();
1385 let mut cursor = node.walk();
1386 for imported in node.children_by_field_name("name", &mut cursor) {
1387 let (module_node, alias_node) = if imported.kind() == "aliased_import" {
1388 let Some(name) = imported.child_by_field_name("name") else {
1389 continue;
1390 };
1391 (name, imported.child_by_field_name("alias"))
1392 } else {
1393 (imported, None)
1394 };
1395 let alias = alias_node
1396 .map(|alias| py_node_text(alias, source).trim().to_string())
1397 .filter(|alias| !alias.is_empty());
1398 let segments = python_path_segments(module_node, source);
1399 if segments.is_empty() {
1400 continue;
1401 }
1402 let target = alias
1405 .is_none()
1406 .then(|| python_first_segment_node(module_node))
1407 .flatten();
1408 let binder_span = alias_node
1409 .or(target)
1410 .map(brokk_bifrost_core::analyzer::common::node_span);
1411 let module = join_python_import_segments(&segments);
1412 let identifier = alias.clone().or_else(|| segments.first().cloned());
1413 let info = ImportInfo {
1414 raw_snippet: if let Some(alias) = &alias {
1415 format!("import {module} as {alias}")
1416 } else {
1417 format!("import {module}")
1418 },
1419 is_wildcard: false,
1420 is_global: false,
1421 identifier,
1422 alias,
1423 path: Some(StructuredImportPath {
1424 segments,
1425 kind: Some(StructuredImportPathKind::Namespace),
1426 lexical_prefixes: Vec::new(),
1427 lexical_scopes: Vec::new(),
1428 declaration_start_byte: node.start_byte(),
1429 }),
1430 binder_span,
1431 };
1432 infos.push(PythonImportSyntax {
1433 info,
1434 module: module_node,
1435 member: None,
1436 leaf: imported,
1437 declaration: node,
1438 target,
1439 alias: alias_node,
1440 });
1441 }
1442 infos
1443}
1444
1445fn python_from_import_syntaxes<'tree>(
1446 node: Node<'tree>,
1447 source: &str,
1448) -> Vec<PythonImportSyntax<'tree>> {
1449 let Some(module_node) = node.child_by_field_name("module_name") else {
1450 return Vec::new();
1451 };
1452 let module_segments = python_module_segments(module_node, source);
1453 if module_segments.is_empty() {
1454 return Vec::new();
1455 }
1456
1457 let mut infos = Vec::new();
1458 let has_wildcard_import = {
1459 let mut cursor = node.walk();
1460 node.named_children(&mut cursor)
1461 .any(|child| child.kind() == "wildcard_import")
1462 };
1463 let mut cursor = node.walk();
1464 let imported_names: Vec<_> = node.children_by_field_name("name", &mut cursor).collect();
1465 if has_wildcard_import {
1466 let module = join_python_import_segments(&module_segments);
1467 let info = ImportInfo {
1468 raw_snippet: format!("from {module} import *"),
1469 is_wildcard: true,
1470 is_global: false,
1471 identifier: None,
1472 alias: None,
1473 path: Some(StructuredImportPath {
1474 segments: module_segments,
1475 kind: Some(StructuredImportPathKind::ImportFrom),
1476 lexical_prefixes: Vec::new(),
1477 lexical_scopes: Vec::new(),
1478 declaration_start_byte: node.start_byte(),
1479 }),
1480 binder_span: None,
1481 };
1482 infos.push(PythonImportSyntax {
1483 info,
1484 module: module_node,
1485 member: None,
1486 leaf: node,
1487 declaration: node,
1488 target: None,
1489 alias: None,
1490 });
1491 return infos;
1492 }
1493 if imported_names.is_empty() {
1494 return infos;
1495 }
1496
1497 for imported in imported_names {
1498 let (name_node, alias_node) = if imported.kind() == "aliased_import" {
1499 let Some(name) = imported.child_by_field_name("name") else {
1500 continue;
1501 };
1502 (name, imported.child_by_field_name("alias"))
1503 } else {
1504 (imported, None)
1505 };
1506 let alias = alias_node
1507 .map(|alias| py_node_text(alias, source).trim().to_string())
1508 .filter(|alias| !alias.is_empty());
1509 let name_segments = python_path_segments(name_node, source);
1510 if name_segments.is_empty() {
1511 continue;
1512 }
1513 let target = alias
1516 .is_none()
1517 .then(|| {
1518 (name_segments.len() == 1)
1519 .then(|| python_first_segment_node(name_node))
1520 .flatten()
1521 })
1522 .flatten();
1523 let binder_span = alias_node
1524 .or(target)
1525 .map(brokk_bifrost_core::analyzer::common::node_span);
1526 let imported_name = join_python_import_segments(&name_segments);
1527 let mut segments = module_segments.clone();
1528 segments.extend(name_segments);
1529 let module = join_python_import_segments(&module_segments);
1530 let info = ImportInfo {
1531 raw_snippet: if let Some(alias) = &alias {
1532 format!("from {module} import {imported_name} as {alias}")
1533 } else {
1534 format!("from {module} import {imported_name}")
1535 },
1536 is_wildcard: false,
1537 is_global: false,
1538 identifier: Some(alias.clone().unwrap_or_else(|| imported_name.clone())),
1539 alias,
1540 path: Some(StructuredImportPath {
1541 segments,
1542 kind: Some(StructuredImportPathKind::ImportFrom),
1543 lexical_prefixes: Vec::new(),
1544 lexical_scopes: Vec::new(),
1545 declaration_start_byte: node.start_byte(),
1546 }),
1547 binder_span,
1548 };
1549 infos.push(PythonImportSyntax {
1550 info,
1551 module: module_node,
1552 member: Some(name_node),
1553 leaf: imported,
1554 declaration: node,
1555 target,
1556 alias: alias_node,
1557 });
1558 }
1559 infos
1560}
1561
1562fn python_module_segments(module: Node<'_>, source: &str) -> Vec<String> {
1563 if module.kind() == "relative_import" {
1564 let mut cursor = module.walk();
1565 let mut prefix = String::new();
1566 let mut path_node = None;
1567 for child in module.named_children(&mut cursor) {
1568 match child.kind() {
1569 "import_prefix" if prefix.is_empty() => {
1570 prefix = py_node_text(child, source).trim().to_string();
1571 }
1572 "dotted_name" if path_node.is_none() => {
1573 path_node = Some(child);
1574 }
1575 _ => {}
1576 }
1577 }
1578 let mut segments = path_node
1579 .map(|path| python_path_segments(path, source))
1580 .unwrap_or_default();
1581 if !prefix.is_empty() {
1582 if let Some(first) = segments.first_mut() {
1583 first.insert_str(0, &prefix);
1584 } else {
1585 segments.push(prefix);
1586 }
1587 }
1588 return segments;
1589 }
1590 python_path_segments(module, source)
1591}
1592
1593fn python_first_segment_node(node: Node<'_>) -> Option<Node<'_>> {
1597 match node.kind() {
1598 "identifier" => Some(node),
1599 "dotted_name" => {
1600 let mut cursor = node.walk();
1601 node.named_children(&mut cursor)
1602 .find(|child| child.kind() == "identifier")
1603 }
1604 _ => None,
1605 }
1606}
1607
1608pub(crate) fn python_import_identifier_nodes(node: Node<'_>) -> Vec<Node<'_>> {
1611 use brokk_bifrost_core::analyzer::tree_walk::{WalkControl, walk_named_tree_preorder};
1612 let mut segments = Vec::new();
1613 walk_named_tree_preorder(node, true, |node| {
1614 if node.kind() == "identifier" {
1615 segments.push(node);
1616 }
1617 WalkControl::Continue
1618 });
1619 segments
1620}
1621
1622fn python_path_segments(node: Node<'_>, source: &str) -> Vec<String> {
1623 python_import_identifier_nodes(node)
1624 .into_iter()
1625 .map(|node| py_node_text(node, source).trim().to_string())
1626 .collect()
1627}
1628
1629fn join_python_import_segments(segments: &[String]) -> String {
1630 let Some((first, rest)) = segments.split_first() else {
1631 return String::new();
1632 };
1633 if first.starts_with('.') && !rest.is_empty() {
1634 format!("{first}.{}", rest.join("."))
1635 } else {
1636 segments.join(".")
1637 }
1638}
1639
1640pub fn python_namespace_binding_name(
1641 import: &ImportInfo,
1642 alias: Option<&str>,
1643 module: &str,
1644) -> String {
1645 import
1646 .identifier
1647 .clone()
1648 .or_else(|| alias.map(str::to_string))
1649 .unwrap_or_else(|| module.to_string())
1650}
1651
1652pub fn python_namespace_binding_module(
1653 import: &ImportInfo,
1654 alias: Option<&str>,
1655 module: &str,
1656) -> String {
1657 if alias.is_some() {
1658 return module.to_string();
1659 }
1660 import
1661 .path
1662 .as_ref()
1663 .and_then(|path| path.segments.first().cloned())
1664 .unwrap_or_else(|| module.to_string())
1665}
1666
1667pub fn resolve_python_relative_module(
1668 project: &dyn brokk_bifrost_core::analyzer::project::Project,
1669 source_file: &ProjectFile,
1670 module_expr: &str,
1671) -> Option<String> {
1672 resolve_python_relative_module_from_package(
1673 &python_current_package(project, source_file),
1674 module_expr,
1675 )
1676}
1677
1678pub fn resolve_python_module_specifier(
1680 project: &dyn brokk_bifrost_core::analyzer::project::Project,
1681 importing_file: &ProjectFile,
1682 module_specifier: &str,
1683) -> Option<String> {
1684 if module_specifier.starts_with('.') {
1685 resolve_python_relative_module(project, importing_file, module_specifier)
1686 } else {
1687 Some(module_specifier.to_string())
1688 }
1689}
1690
1691pub fn resolve_python_relative_module_from_package(
1695 current_package: &str,
1696 module_expr: &str,
1697) -> Option<String> {
1698 let level = module_expr.chars().take_while(|ch| *ch == '.').count();
1699 let suffix = module_expr[level..].trim_matches('.');
1700 let mut parts: Vec<_> = current_package
1701 .split('.')
1702 .filter(|part| !part.is_empty())
1703 .map(str::to_string)
1704 .collect();
1705 if level == 0 {
1706 return Some(module_expr.to_string());
1707 }
1708 if level > 0 {
1709 if level - 1 > parts.len() {
1710 return None;
1711 }
1712 parts.truncate(parts.len() - (level - 1));
1713 }
1714 if !suffix.is_empty() {
1715 parts.extend(suffix.split('.').map(str::to_string));
1716 }
1717 Some(parts.join("."))
1718}
1719
1720fn python_current_package(
1721 project: &dyn brokk_bifrost_core::analyzer::project::Project,
1722 source_file: &ProjectFile,
1723) -> String {
1724 let module = match project.selected_module_name(source_file) {
1725 Ok(Some(name))
1726 if crate::declarations::python_module_components_from_name(source_file, &name)
1727 .is_some() =>
1728 {
1729 name
1730 }
1731 Ok(Some(name)) => {
1732 tracing::warn!(
1733 file = %source_file.abs_path().display(),
1734 published_name = %name,
1735 mounted_path = ?source_file.rel_path(),
1736 "published Python module is not a suffix of its mounted path; deriving module identity from package probes"
1737 );
1738 python_module_name(project, source_file)
1739 }
1740 Ok(None) => python_module_name(project, source_file),
1741 Err(error) => {
1742 tracing::warn!(
1743 file = %source_file.abs_path().display(),
1744 mounted_path = ?source_file.rel_path(),
1745 %error,
1746 "cannot read published Python module; deriving module identity from package probes"
1747 );
1748 python_module_name(project, source_file)
1749 }
1750 };
1751 if source_file
1752 .rel_path()
1753 .file_name()
1754 .and_then(|name| name.to_str())
1755 == Some("__init__.py")
1756 {
1757 module
1758 } else {
1759 module
1760 .rsplit_once('.')
1761 .map(|(package, _)| package.to_string())
1762 .unwrap_or_default()
1763 }
1764}