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 brokk_bifrost_core::analyzer::common::node_source_text;
14use brokk_bifrost_core::analyzer::model::{
15 ImportInfo, StructuredImportPath, StructuredImportPathKind,
16};
17use brokk_bifrost_core::analyzer::query_token::QueryToken;
18use brokk_bifrost_core::analyzer::usages::model::{ExportEntry, ImportBinding, ImportKind};
19use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, ProjectFile};
20use brokk_bifrost_core::hash::{HashMap, HashSet};
21use std::collections::VecDeque;
22use tree_sitter::Node;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct PythonModuleReplacement {
26 pub target_module: String,
27}
28
29pub(crate) fn imported_module_assignment_at(
39 reference: Node<'_>,
40 local: &str,
41 source: &str,
42 mut is_importlib_binding: impl FnMut(&str) -> bool,
43) -> Option<String> {
44 let suite = enclosing_execution_suite(reference)?;
45 let mut resolved = None;
46 let mut cursor = suite.walk();
47 for statement in suite.named_children(&mut cursor) {
48 if statement.start_byte() >= reference.start_byte()
49 || (statement.start_byte() <= reference.start_byte()
50 && reference.end_byte() <= statement.end_byte())
51 {
52 break;
53 }
54
55 let expression = if statement.kind() == "expression_statement" {
56 statement.named_child(0).unwrap_or(statement)
57 } else {
58 statement
59 };
60 if expression.kind() == "assignment"
61 && expression.child_by_field_name("left").is_some_and(|left| {
62 left.kind() == "identifier" && node_source_text(left, source) == local
63 })
64 {
65 resolved = expression
66 .child_by_field_name("right")
67 .and_then(|right| importlib_call_module(right, source, &mut is_importlib_binding));
68 continue;
69 }
70
71 let binds_local = python_direct_scope_bindings_bounded(statement, source, || true)
72 .expect("unbounded binding collection cannot be cancelled")
73 .into_iter()
74 .any(|binding| node_source_text(binding.declaration, source) == local);
75 if binds_local {
76 resolved = None;
77 }
78 }
79 resolved
80}
81
82fn enclosing_execution_suite(mut node: Node<'_>) -> Option<Node<'_>> {
83 while let Some(parent) = node.parent() {
84 if matches!(parent.kind(), "function_definition" | "lambda") {
85 return parent.child_by_field_name("body");
86 }
87 if parent.kind() == "module" {
88 return Some(parent);
89 }
90 node = parent;
91 }
92 None
93}
94
95fn importlib_call_module(
96 call: Node<'_>,
97 source: &str,
98 is_importlib_binding: &mut impl FnMut(&str) -> bool,
99) -> Option<String> {
100 if call.kind() != "call" {
101 return None;
102 }
103 let function = call.child_by_field_name("function")?;
104 if function.kind() != "attribute" {
105 return None;
106 }
107 let (object, attribute) = (
108 function.child_by_field_name("object")?,
109 function.child_by_field_name("attribute")?,
110 );
111 if object.kind() != "identifier"
112 || attribute.kind() != "identifier"
113 || node_source_text(attribute, source) != "import_module"
114 || !is_importlib_binding(node_source_text(object, source))
115 {
116 return None;
117 }
118
119 let arguments = call.child_by_field_name("arguments")?;
120 if arguments.named_child_count() != 1 {
121 return None;
122 }
123 let literal = arguments.named_child(0)?;
124 if literal.kind() != "string"
125 || literal
126 .parent()
127 .is_some_and(|parent| parent.kind() == "concatenated_string")
128 {
129 return None;
130 }
131 let mut content = None;
132 for index in 0..literal.named_child_count() {
133 let child = literal.named_child(index)?;
134 match child.kind() {
135 "string_start" | "string_end" => {}
136 "string_content" if content.is_none() && child.named_child_count() == 0 => {
137 content = Some(child);
138 }
139 _ => return None,
140 }
141 }
142 let content = content?;
143 let module = node_source_text(content, source);
144 (!module.is_empty() && !module.starts_with('.')).then(|| module.to_string())
145}
146
147pub(crate) fn literal_importlib_modules(
152 source: &str,
153 bindings: &HashMap<String, ImportBinding>,
154) -> HashSet<String> {
155 let Some(tree) = parse_python_tree(source) else {
156 return HashSet::default();
157 };
158 let mut modules = HashSet::default();
159 let mut stack = vec![tree.root_node()];
160 while let Some(node) = stack.pop() {
161 if node.kind() == "assignment"
162 && node
163 .child_by_field_name("left")
164 .is_some_and(|left| left.kind() == "identifier")
165 && let Some(module) = node.child_by_field_name("right").and_then(|right| {
166 importlib_call_module(right, source, &mut |local| {
167 bindings.get(local).is_some_and(|binding| {
168 binding.kind == ImportKind::Namespace
169 && binding
170 .namespace_imported_module
171 .as_deref()
172 .unwrap_or(&binding.module_specifier)
173 == "importlib"
174 })
175 })
176 })
177 {
178 modules.insert(module);
179 }
180 let mut cursor = node.walk();
181 stack.extend(node.named_children(&mut cursor));
182 }
183 modules
184}
185
186fn module_replacement_from_assignment(
194 assignment: Node<'_>,
195 source: &str,
196 bindings: &HashMap<String, ImportBinding>,
197) -> Option<PythonModuleReplacement> {
198 let (left, right) = (
199 assignment.child_by_field_name("left")?,
200 assignment.child_by_field_name("right")?,
201 );
202 if left.kind() != "subscript" || right.kind() != "identifier" {
203 return None;
204 }
205 let (value, subscript) = (
206 left.child_by_field_name("value")?,
207 left.child_by_field_name("subscript")?,
208 );
209 if value.kind() != "attribute"
210 || subscript.kind() != "identifier"
211 || node_source_text(subscript, source) != "__name__"
212 {
213 return None;
214 }
215 let (sys_local, modules) = (
216 value.child_by_field_name("object")?,
217 value.child_by_field_name("attribute")?,
218 );
219 if sys_local.kind() != "identifier"
220 || modules.kind() != "identifier"
221 || node_source_text(modules, source) != "modules"
222 {
223 return None;
224 }
225 let sys_binding = bindings.get(node_source_text(sys_local, source))?;
226 let sys_module = sys_binding
227 .namespace_imported_module
228 .as_deref()
229 .unwrap_or(&sys_binding.module_specifier);
230 if sys_binding.kind != ImportKind::Namespace || sys_module != "sys" {
231 return None;
232 }
233
234 let target_binding = bindings.get(node_source_text(right, source))?;
235 if target_binding.kind != ImportKind::Namespace {
236 return None;
237 }
238 Some(PythonModuleReplacement {
239 target_module: target_binding
240 .namespace_imported_module
241 .clone()
242 .unwrap_or_else(|| target_binding.module_specifier.clone()),
243 })
244}
245
246fn remove_direct_scope_bindings(
247 statement: Node<'_>,
248 source: &str,
249 bindings: &mut HashMap<String, ImportBinding>,
250) {
251 let mut stack = vec![statement];
252 while let Some(node) = stack.pop() {
253 for binding in python_direct_scope_bindings_bounded(node, source, || true)
254 .expect("unbounded binding collection cannot be cancelled")
255 {
256 bindings.remove(node_source_text(binding.declaration, source));
257 }
258 if matches!(
259 node.kind(),
260 "function_definition" | "class_definition" | "lambda"
261 ) {
262 continue;
263 }
264 let mut cursor = node.walk();
265 stack.extend(node.named_children(&mut cursor));
266 }
267}
268
269pub fn parse_python_import_infos(source: &str) -> Vec<ImportInfo> {
273 let mut parser = tree_sitter::Parser::new();
274 parser
275 .set_language(&tree_sitter_python::LANGUAGE.into())
276 .expect("failed to load Python parser");
277 let Some(tree) = parser.parse(source, None) else {
278 return Vec::new();
279 };
280 let mut pending = vec![tree.root_node()];
281 let mut imports = Vec::new();
282 while let Some(node) = pending.pop() {
283 if matches!(node.kind(), "import_statement" | "import_from_statement") {
284 imports.extend(python_import_infos_from_node(node, source));
285 continue;
286 }
287 let mut cursor = node.walk();
288 pending.extend(node.named_children(&mut cursor));
289 }
290 imports
291}
292
293#[derive(Debug, Clone, PartialEq, Eq)]
294pub struct PythonImportBinding {
295 pub start_byte: usize,
296 pub scope_start_byte: usize,
297 pub scope_end_byte: usize,
298 function_scoped: bool,
299 pub local_name: String,
300 pub qualified_name: String,
301 pub consumed_attributes: usize,
302}
303
304impl PythonImportBinding {
305 pub fn is_function_scoped(&self) -> bool {
306 self.function_scoped
307 }
308}
309
310pub fn parse_python_import_bindings(source: &str) -> Vec<PythonImportBinding> {
314 let mut parser = tree_sitter::Parser::new();
315 parser
316 .set_language(&tree_sitter_python::LANGUAGE.into())
317 .expect("failed to load Python parser");
318 let Some(tree) = parser.parse(source, None) else {
319 return Vec::new();
320 };
321 python_import_bindings_from_tree(tree.root_node(), source)
322}
323
324pub fn python_import_bindings_from_tree(root: Node<'_>, source: &str) -> Vec<PythonImportBinding> {
328 let mut pending = vec![root];
329 let mut nodes = Vec::new();
330 while let Some(node) = pending.pop() {
331 if matches!(node.kind(), "import_statement" | "import_from_statement") {
332 nodes.push(node);
333 continue;
334 }
335 let mut cursor = node.walk();
336 pending.extend(node.named_children(&mut cursor));
337 }
338 nodes.sort_by_key(Node::start_byte);
339 nodes
340 .into_iter()
341 .flat_map(|node| {
342 let (scope_start_byte, scope_end_byte, function_scoped) =
343 python_import_binding_scope(node, source.len());
344 python_import_infos_from_node(node, source)
345 .into_iter()
346 .filter_map(move |import| {
347 let path = import.path.as_ref()?;
348 let details = python_import_details(&import)?;
349 match details {
350 PythonImportDetails::Import { module, alias } => {
351 let consumed_attributes = if alias.is_some() {
352 0
353 } else {
354 path.segments.len().saturating_sub(1)
355 };
356 Some(PythonImportBinding {
357 start_byte: node.start_byte(),
358 scope_start_byte,
359 scope_end_byte,
360 function_scoped,
361 local_name: alias.or_else(|| path.segments.first().cloned())?,
362 qualified_name: module,
363 consumed_attributes,
364 })
365 }
366 PythonImportDetails::FromImport {
367 module,
368 name,
369 alias,
370 wildcard: false,
371 } => Some(PythonImportBinding {
372 start_byte: node.start_byte(),
373 scope_start_byte,
374 scope_end_byte,
375 function_scoped,
376 local_name: alias.unwrap_or(name.clone()),
377 qualified_name: format!("{module}.{name}"),
378 consumed_attributes: 0,
379 }),
380 PythonImportDetails::FromImport { wildcard: true, .. } => None,
381 }
382 })
383 })
384 .collect()
385}
386
387fn python_import_binding_scope(node: Node<'_>, source_len: usize) -> (usize, usize, bool) {
388 let mut parent = node.parent();
389 while let Some(scope) = parent {
390 if matches!(scope.kind(), "function_definition" | "lambda") {
391 return (scope.start_byte(), scope.end_byte(), true);
392 }
393 parent = scope.parent();
394 }
395 (0, source_len, false)
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use brokk_bifrost_core::analyzer::usages::model::ImportBinder;
402
403 fn importlib_assignment_for(source: &str, importlib_local: &str) -> Option<String> {
404 let tree = parse_python_tree(source).expect("valid Python fixture");
405 let reference_start = source.rfind("canonical.setup").expect("reference site");
406 let mut stack = vec![tree.root_node()];
407 let mut reference = None;
408 while let Some(node) = stack.pop() {
409 if node.kind() == "identifier"
410 && node.start_byte() == reference_start
411 && node_source_text(node, source) == "canonical"
412 {
413 reference = Some(node);
414 break;
415 }
416 let mut cursor = node.walk();
417 stack.extend(node.named_children(&mut cursor));
418 }
419 imported_module_assignment_at(
420 reference.expect("canonical reference node"),
421 "canonical",
422 source,
423 |local| local == importlib_local,
424 )
425 }
426
427 #[test]
428 fn existing_tree_import_bindings_preserve_function_scope_and_relative_identity() {
429 let source = r#"def register():
430 from .workers.special import Worker
431 return Worker()
432"#;
433 let tree = parse_python_tree(source).expect("Python tree");
434 let bindings = python_import_bindings_from_tree(tree.root_node(), source);
435
436 assert_eq!(bindings.len(), 1, "{bindings:#?}");
437 assert!(bindings[0].is_function_scoped(), "{bindings:#?}");
438 assert_eq!(bindings[0].local_name, "Worker");
439 assert_eq!(bindings[0].qualified_name, ".workers.special.Worker");
440 assert!(bindings[0].scope_start_byte <= bindings[0].start_byte);
441 assert!(bindings[0].scope_end_byte >= source.rfind("Worker()").unwrap());
442 }
443
444 #[test]
445 fn importlib_assignment_requires_a_direct_literal_dominating_write() {
446 let valid = r#"import importlib as loader
447
448def test():
449 canonical = loader.import_module("routes.contacts.contacts_routes")
450 assert canonical.setup_contacts_routes
451"#;
452 assert_eq!(
453 importlib_assignment_for(valid, "loader"),
454 Some("routes.contacts.contacts_routes".to_string())
455 );
456
457 for near_miss in [
458 r#"import importlib as loader
459def test(module_name):
460 canonical = loader.import_module(module_name)
461 assert canonical.setup_contacts_routes
462"#,
463 r#"import importlib as loader
464def test():
465 if enabled:
466 canonical = loader.import_module("routes.contacts.contacts_routes")
467 assert canonical.setup_contacts_routes
468"#,
469 r#"import importlib as loader
470def test():
471 canonical = loader.import_module("routes.contacts.contacts_routes")
472 canonical = object()
473 assert canonical.setup_contacts_routes
474"#,
475 ] {
476 assert_eq!(
477 importlib_assignment_for(near_miss, "loader"),
478 None,
479 "near miss must not infer a module receiver: {near_miss:?}"
480 );
481 }
482 assert_eq!(
483 importlib_assignment_for(valid, "shadowed_loader"),
484 None,
485 "a similarly shaped non-importlib receiver must not resolve"
486 );
487 }
488
489 fn replacement_for(source: &str, binder: &ImportBinder) -> Option<PythonModuleReplacement> {
490 let tree = parse_python_tree(source).expect("valid Python fixture");
491 let root = tree.root_node();
492 let mut replacement = None;
493 let mut cursor = root.walk();
494 for statement in root.named_children(&mut cursor) {
495 let statement = if statement.kind() == "expression_statement" {
496 statement.named_child(0).expect("fixture expression")
497 } else {
498 statement
499 };
500 if statement.kind() == "assignment"
501 && let Some(next) =
502 module_replacement_from_assignment(statement, source, &binder.bindings)
503 && replacement.replace(next).is_some()
504 {
505 return None;
506 }
507 }
508 replacement
509 }
510
511 #[test]
512 fn module_replacement_requires_exact_structured_import_bindings() {
513 let source = r#"import sys as _sys
514from routes.contacts import contacts_routes as _canonical
515
516_sys.modules[__name__] = _canonical
517"#;
518 let mut binder = ImportBinder::empty();
519 binder.bindings.insert(
520 "_sys".to_string(),
521 ImportBinding {
522 module_specifier: "sys".to_string(),
523 namespace_imported_module: Some("sys".to_string()),
524 kind: ImportKind::Namespace,
525 imported_name: None,
526 },
527 );
528 binder.bindings.insert(
529 "_canonical".to_string(),
530 ImportBinding {
531 module_specifier: "routes.contacts.contacts_routes".to_string(),
532 namespace_imported_module: None,
533 kind: ImportKind::Namespace,
534 imported_name: None,
535 },
536 );
537
538 assert_eq!(
539 replacement_for(source, &binder),
540 Some(PythonModuleReplacement {
541 target_module: "routes.contacts.contacts_routes".to_string(),
542 })
543 );
544
545 for near_miss in [
546 "cache.modules[__name__] = _canonical\n",
547 "_sys.modules[module_name] = _canonical\n",
548 "_sys.modules[__name__] = build()\n",
549 "def replace():\n _sys.modules[__name__] = _canonical\n",
550 ] {
551 assert_eq!(
552 replacement_for(near_miss, &binder),
553 None,
554 "near miss must not replace module identity: {near_miss:?}"
555 );
556 }
557 }
558}
559
560pub fn module_replacement_of(
561 python: &dyn PythonSource,
562 file: &ProjectFile,
563 source: &str,
564) -> Option<PythonModuleReplacement> {
565 let tree = parse_python_tree(source)?;
566 let root = tree.root_node();
567 let mut bindings: HashMap<String, ImportBinding> = HashMap::default();
568 let mut replacement = None;
569 let mut cursor = root.walk();
570 for statement in root.named_children(&mut cursor) {
571 let statement = if statement.kind() == "expression_statement" {
572 let Some(expression) = statement.named_child(0) else {
573 continue;
574 };
575 expression
576 } else {
577 statement
578 };
579 match statement.kind() {
580 "import_statement" | "import_from_statement" => {
581 let imports = python_import_infos_from_node(statement, source);
582 bindings.extend(import_binder_from_imports(python, file, &imports).bindings);
583 }
584 "assignment" => {
585 if let Some(next) = module_replacement_from_assignment(statement, source, &bindings)
586 && replacement.replace(next).is_some()
587 {
588 return None;
589 }
590 remove_direct_scope_bindings(statement, source, &mut bindings);
591 }
592 _ => remove_direct_scope_bindings(statement, source, &mut bindings),
593 }
594 }
595 replacement
596}
597
598pub fn resolve_import_bindings(
599 python: &dyn PythonSource,
600 token: QueryToken<'_>,
601 file: &ProjectFile,
602) -> HashMap<String, CodeUnit> {
603 let imports = python.import_info_of(token, file);
604 let mut bindings = HashMap::default();
605 for resolved in resolve_imports_batched(python, file, &imports) {
606 for (binding, code_unit) in resolved {
607 bindings.insert(binding, code_unit);
608 }
609 }
610 bindings
611}
612
613pub fn resolve_imports_batched(
620 python: &dyn PythonSource,
621 file: &ProjectFile,
622 imports: &[ImportInfo],
623) -> Vec<Vec<(String, CodeUnit)>> {
624 let primary_fqns: Vec<Option<String>> = imports
625 .iter()
626 .map(|import| primary_module_fqn(file, import))
627 .collect();
628 let to_resolve: Vec<String> = primary_fqns.iter().flatten().cloned().collect();
629 let mut batch_results =
630 resolve_module_code_units_batch(python, Some(file), &to_resolve).into_iter();
631
632 imports
633 .iter()
634 .zip(primary_fqns.iter())
635 .map(|(import, primary_fqn)| {
636 let hint = primary_fqn.as_ref().map(|_| batch_results.next().unwrap());
637 resolve_import_with_hint(python, file, import, hint.as_ref())
638 })
639 .collect()
640}
641
642pub fn resolve_import_files_batched(
652 python: &dyn PythonSource,
653 file: &ProjectFile,
654 imports: &[ImportInfo],
655) -> HashSet<ProjectFile> {
656 let mut module_fqns = Vec::new();
657 for import in imports {
658 match python_import_details(import) {
659 Some(PythonImportDetails::Import { module, .. }) => module_fqns.push(module),
660 Some(PythonImportDetails::FromImport {
661 module,
662 name,
663 wildcard,
664 ..
665 }) => {
666 let resolved_module = if module.starts_with('.') {
667 resolve_python_relative_module(file, &module)
668 } else {
669 Some(module)
670 };
671 let Some(resolved_module) = resolved_module else {
672 continue;
673 };
674 if !wildcard {
675 module_fqns.push(format!("{resolved_module}.{name}"));
676 }
677 module_fqns.push(resolved_module);
678 }
679 None => {}
680 }
681 }
682 module_fqns.sort();
683 module_fqns.dedup();
684
685 python
686 .path_module_fqns_batch(&module_fqns)
687 .into_iter()
688 .flatten()
689 .flatten()
690 .filter(CodeUnit::is_module)
691 .map(|unit| unit.source().clone())
692 .collect()
693}
694
695fn primary_module_fqn(file: &ProjectFile, import: &ImportInfo) -> Option<String> {
699 match python_import_details(import)? {
700 PythonImportDetails::Import { module, alias } => Some(python_namespace_binding_module(
701 import,
702 alias.as_deref(),
703 &module,
704 )),
705 PythonImportDetails::FromImport {
706 module,
707 name,
708 wildcard,
709 ..
710 } => {
711 if wildcard {
712 return None;
713 }
714 let resolved_module = if module.starts_with('.') {
715 resolve_python_relative_module(file, &module)
716 } else {
717 Some(module)
718 };
719 resolved_module.map(|resolved_module| format!("{resolved_module}.{name}"))
720 }
721 }
722}
723
724pub fn resolve_import(
725 python: &dyn PythonSource,
726 file: &ProjectFile,
727 import: &ImportInfo,
728) -> Vec<(String, CodeUnit)> {
729 resolve_import_with_hint(python, file, import, None)
730}
731
732fn resolve_import_with_hint(
735 python: &dyn PythonSource,
736 file: &ProjectFile,
737 import: &ImportInfo,
738 primary_hint: Option<&Option<CodeUnit>>,
739) -> Vec<(String, CodeUnit)> {
740 if let Some(details) = python_import_details(import) {
741 match details {
742 PythonImportDetails::Import { module, alias } => {
743 let binding = python_namespace_binding_name(import, alias.as_deref(), &module);
744 let bound_module =
745 python_namespace_binding_module(import, alias.as_deref(), &module);
746 let resolved = match primary_hint {
747 Some(hint) => hint.clone(),
748 None => resolve_module_code_unit(python, Some(file), &bound_module),
749 };
750 if let Some(module_code_unit) = resolved {
751 return vec![(binding, module_code_unit)];
752 }
753 }
754 PythonImportDetails::FromImport {
755 module,
756 name,
757 alias,
758 wildcard,
759 } => {
760 let resolved_module = if module.starts_with('.') {
761 resolve_python_relative_module(file, &module)
762 } else {
763 Some(module)
764 };
765 let Some(resolved_module) = resolved_module else {
766 return Vec::new();
767 };
768 if wildcard {
769 return public_declarations_in_module(python, file, &resolved_module)
770 .into_iter()
771 .map(|code_unit| (code_unit.identifier().to_string(), code_unit))
772 .collect();
773 }
774
775 let binding = alias.clone().unwrap_or_else(|| name.clone());
776 let module_candidate = format!("{resolved_module}.{name}");
777 let resolved = match primary_hint {
778 Some(hint) => hint.clone(),
779 None => resolve_module_code_unit(python, Some(file), &module_candidate),
780 };
781 if let Some(code_unit) = resolved {
782 return vec![(binding, code_unit)];
783 }
784 let exported =
785 resolve_exported_name_from_module(python, Some(file), &resolved_module, &name);
786 if !exported.is_empty() {
787 return exported
788 .into_iter()
789 .map(|code_unit| (binding.clone(), code_unit))
790 .collect();
791 }
792 let definitions: Vec<_> = python.definitions(&module_candidate).collect();
793 if !definitions.is_empty() {
794 return definitions
795 .into_iter()
796 .map(|code_unit| (binding.clone(), code_unit))
797 .collect();
798 }
799 let package_candidate: Vec<_> = python
800 .definitions(&format!("{resolved_module}.{name}"))
801 .collect();
802 if !package_candidate.is_empty() {
803 return package_candidate
804 .into_iter()
805 .map(|code_unit| (binding.clone(), code_unit))
806 .collect();
807 }
808 }
809 }
810 }
811 Vec::new()
812}
813
814pub fn resolve_exported_fqn(
818 python: &dyn PythonSource,
819 importer: Option<&ProjectFile>,
820 fqn: &str,
821) -> Vec<CodeUnit> {
822 let Some((module, name)) = fqn.rsplit_once('.') else {
823 return Vec::new();
824 };
825 resolve_exported_name_from_module(python, importer, module, name)
826}
827
828fn resolve_direct_named_exported_fqn(
837 python: &dyn PythonSource,
838 importer: Option<&ProjectFile>,
839 fqn: &str,
840) -> Option<Vec<CodeUnit>> {
841 let (module, name) = fqn.rsplit_once('.')?;
842 let mut results = Vec::new();
843 let mut queue = VecDeque::from([(importer.cloned(), module.to_string(), name.to_string())]);
844 let mut visited = HashSet::default();
845
846 while let Some((importer, module, export_name)) = queue.pop_front() {
847 if !visited.insert((module.clone(), export_name.clone())) {
848 continue;
849 }
850 let module_unit = resolve_module_code_unit(python, importer.as_ref(), &module)?;
851 let file = module_unit.source();
852 let local = local_export_declarations(python, file, &export_name);
853 let binder = python.import_binder_of(file);
854 let binding = binder.bindings.get(&export_name);
855 if !local.is_empty() && binding.is_some() {
856 return None;
857 }
858 if !local.is_empty() {
859 results.extend(local);
860 continue;
861 }
862 let binding = binding?;
863 if binding.kind != ImportKind::Named {
864 return None;
865 }
866 let imported_name = binding.imported_name.as_ref()?;
867 queue.push_back((
868 Some(file.clone()),
869 binding.module_specifier.clone(),
870 imported_name.clone(),
871 ));
872 }
873
874 results.sort_by(|left, right| {
875 left.source()
876 .cmp(right.source())
877 .then_with(|| left.fq_name().cmp(&right.fq_name()))
878 });
879 results.dedup();
880 (!results.is_empty()).then_some(results)
881}
882
883pub fn resolve_fqn_candidates(
892 python: &dyn PythonSource,
893 importer: Option<&ProjectFile>,
894 fqn: &str,
895 exact: impl FnOnce(&str) -> Vec<CodeUnit>,
896) -> Vec<CodeUnit> {
897 if let Some(candidates) = resolve_direct_named_exported_fqn(python, importer, fqn) {
898 return candidates;
899 }
900 let candidates = resolve_exported_fqn(python, importer, fqn);
901 if !candidates.is_empty() {
902 return candidates;
903 }
904 exact(fqn)
905}
906
907fn resolve_exported_name_from_module(
908 python: &dyn PythonSource,
909 importer: Option<&ProjectFile>,
910 module: &str,
911 name: &str,
912) -> Vec<CodeUnit> {
913 let Some(module_unit) = resolve_module_code_unit(python, importer, module) else {
914 return Vec::new();
915 };
916 resolve_exported_name(python, module_unit.source(), name)
917}
918
919fn resolve_exported_name(
920 python: &dyn PythonSource,
921 module_file: &ProjectFile,
922 name: &str,
923) -> Vec<CodeUnit> {
924 let mut results = Vec::new();
925 let mut queue = VecDeque::from([(module_file.clone(), name.to_string())]);
926 let mut visited = HashSet::default();
927
928 while let Some((file, export_name)) = queue.pop_front() {
929 if !visited.insert((file.clone(), export_name.clone())) {
930 continue;
931 }
932
933 let index = python.export_index_of(&file);
934 if let Some(entry) = index.exports_by_name.get(&export_name) {
935 match entry {
936 ExportEntry::Local { local_name } => {
937 results.extend(local_export_declarations(python, &file, local_name));
938 }
939 ExportEntry::ReexportedNamed {
940 module_specifier,
941 imported_name,
942 } => {
943 for target_file in
944 resolve_module_files_for_export(python, &file, module_specifier)
945 {
946 queue.push_back((target_file, imported_name.clone()));
947 }
948 }
949 ExportEntry::ReexportedModule { module_specifier } => {
950 results.extend(resolve_module_code_unit(
953 python,
954 Some(&file),
955 module_specifier,
956 ));
957 }
958 ExportEntry::Default { local_name } => {
959 if let Some(local_name) = local_name {
960 results.extend(local_export_declarations(python, &file, local_name));
961 }
962 }
963 }
964 continue;
965 }
966
967 if !export_name.starts_with('_') {
968 for star in &index.reexport_stars {
969 for target_file in
970 resolve_module_files_for_export(python, &file, &star.module_specifier)
971 {
972 queue.push_back((target_file, export_name.clone()));
973 }
974 }
975 }
976 }
977
978 results.sort_by(|left, right| {
979 left.source()
980 .cmp(right.source())
981 .then_with(|| left.fq_name().cmp(&right.fq_name()))
982 });
983 results.dedup();
984 results
985}
986
987fn local_export_declarations(
988 index: &dyn CodeUnitIndex,
989 file: &ProjectFile,
990 local_name: &str,
991) -> Vec<CodeUnit> {
992 index
993 .top_level_declarations(file)
994 .into_iter()
995 .filter(|unit| {
996 unit.identifier() == local_name
997 && index
998 .parent_of(unit)
999 .is_some_and(|parent| parent.is_module() && parent.source() == file)
1000 })
1001 .collect()
1002}
1003
1004fn resolve_module_files_for_export(
1005 python: &dyn PythonSource,
1006 importing_file: &ProjectFile,
1007 module_specifier: &str,
1008) -> Vec<ProjectFile> {
1009 let resolved_module = if module_specifier.starts_with('.') {
1010 resolve_python_relative_module(importing_file, module_specifier)
1011 } else {
1012 Some(module_specifier.to_string())
1013 };
1014 let Some(resolved_module) = resolved_module else {
1015 return Vec::new();
1016 };
1017 resolve_module_code_unit(python, Some(importing_file), &resolved_module)
1021 .map(|unit| vec![unit.source().clone()])
1022 .unwrap_or_default()
1023}
1024
1025pub fn extract_package_from_python_wildcard(import: &ImportInfo) -> Option<String> {
1026 let details = python_import_details(import)?;
1027 match details {
1028 PythonImportDetails::FromImport {
1029 module, wildcard, ..
1030 } if wildcard => Some(module),
1031 _ => None,
1032 }
1033}
1034
1035#[derive(Debug, Clone)]
1036pub enum PythonImportDetails {
1037 Import {
1038 module: String,
1039 alias: Option<String>,
1040 },
1041 FromImport {
1042 module: String,
1043 name: String,
1044 alias: Option<String>,
1045 wildcard: bool,
1046 },
1047}
1048
1049pub fn python_import_infos_from_node(node: Node<'_>, source: &str) -> Vec<ImportInfo> {
1050 match node.kind() {
1051 "import_statement" => python_namespace_import_infos(node, source),
1052 "import_from_statement" => python_from_import_infos(node, source),
1053 _ => Vec::new(),
1054 }
1055}
1056
1057pub fn python_import_details(import: &ImportInfo) -> Option<PythonImportDetails> {
1058 let path = import.path.as_ref()?;
1059 match path.kind? {
1060 StructuredImportPathKind::Namespace => Some(PythonImportDetails::Import {
1061 module: join_python_import_segments(&path.segments),
1062 alias: import.alias.clone(),
1063 }),
1064 StructuredImportPathKind::StaticMember => None,
1066 StructuredImportPathKind::ImportFrom => {
1067 let (name, module_segments) = if import.is_wildcard {
1068 ("*".to_string(), path.segments.as_slice())
1069 } else {
1070 let (name, module_segments) = path.segments.split_last()?;
1071 (name.clone(), module_segments)
1072 };
1073 Some(PythonImportDetails::FromImport {
1074 module: join_python_import_segments(module_segments),
1075 name,
1076 alias: import.alias.clone(),
1077 wildcard: import.is_wildcard,
1078 })
1079 }
1080 }
1081}
1082
1083fn python_namespace_import_infos(node: Node<'_>, source: &str) -> Vec<ImportInfo> {
1084 let mut infos = Vec::new();
1085 let mut cursor = node.walk();
1086 for imported in node.children_by_field_name("name", &mut cursor) {
1087 let (module_node, alias_node) = if imported.kind() == "aliased_import" {
1088 let Some(name) = imported.child_by_field_name("name") else {
1089 continue;
1090 };
1091 (name, imported.child_by_field_name("alias"))
1092 } else {
1093 (imported, None)
1094 };
1095 let alias = alias_node
1096 .map(|alias| py_node_text(alias, source).trim().to_string())
1097 .filter(|alias| !alias.is_empty());
1098 let segments = python_path_segments(module_node, source);
1099 if segments.is_empty() {
1100 continue;
1101 }
1102 let binder_span = alias
1105 .is_some()
1106 .then_some(alias_node)
1107 .flatten()
1108 .or_else(|| python_first_segment_node(module_node))
1109 .map(brokk_bifrost_core::analyzer::common::node_span);
1110 let module = join_python_import_segments(&segments);
1111 let identifier = alias.clone().or_else(|| segments.first().cloned());
1112 infos.push(ImportInfo {
1113 raw_snippet: if let Some(alias) = &alias {
1114 format!("import {module} as {alias}")
1115 } else {
1116 format!("import {module}")
1117 },
1118 is_wildcard: false,
1119 is_global: false,
1120 identifier,
1121 alias,
1122 path: Some(StructuredImportPath {
1123 segments,
1124 kind: Some(StructuredImportPathKind::Namespace),
1125 lexical_prefixes: Vec::new(),
1126 lexical_scopes: Vec::new(),
1127 declaration_start_byte: node.start_byte(),
1128 }),
1129 binder_span,
1130 });
1131 }
1132 infos
1133}
1134
1135fn python_from_import_infos(node: Node<'_>, source: &str) -> Vec<ImportInfo> {
1136 let Some(module_node) = node.child_by_field_name("module_name") else {
1137 return Vec::new();
1138 };
1139 let module_segments = python_module_segments(module_node, source);
1140 if module_segments.is_empty() {
1141 return Vec::new();
1142 }
1143
1144 let mut infos = Vec::new();
1145 let has_wildcard_import = {
1146 let mut cursor = node.walk();
1147 node.named_children(&mut cursor)
1148 .any(|child| child.kind() == "wildcard_import")
1149 };
1150 let mut cursor = node.walk();
1151 let imported_names: Vec<_> = node.children_by_field_name("name", &mut cursor).collect();
1152 if has_wildcard_import {
1153 let module = join_python_import_segments(&module_segments);
1154 infos.push(ImportInfo {
1155 raw_snippet: format!("from {module} import *"),
1156 is_wildcard: true,
1157 is_global: false,
1158 identifier: None,
1159 alias: None,
1160 path: Some(StructuredImportPath {
1161 segments: module_segments,
1162 kind: Some(StructuredImportPathKind::ImportFrom),
1163 lexical_prefixes: Vec::new(),
1164 lexical_scopes: Vec::new(),
1165 declaration_start_byte: node.start_byte(),
1166 }),
1167 binder_span: None,
1168 });
1169 return infos;
1170 }
1171 if imported_names.is_empty() {
1172 return infos;
1173 }
1174
1175 for imported in imported_names {
1176 let (name_node, alias_node) = if imported.kind() == "aliased_import" {
1177 let Some(name) = imported.child_by_field_name("name") else {
1178 continue;
1179 };
1180 (name, imported.child_by_field_name("alias"))
1181 } else {
1182 (imported, None)
1183 };
1184 let alias = alias_node
1185 .map(|alias| py_node_text(alias, source).trim().to_string())
1186 .filter(|alias| !alias.is_empty());
1187 let name_segments = python_path_segments(name_node, source);
1188 if name_segments.is_empty() {
1189 continue;
1190 }
1191 let binder_span = alias
1194 .is_some()
1195 .then_some(alias_node)
1196 .flatten()
1197 .or_else(|| {
1198 (name_segments.len() == 1)
1199 .then(|| python_first_segment_node(name_node))
1200 .flatten()
1201 })
1202 .map(brokk_bifrost_core::analyzer::common::node_span);
1203 let imported_name = join_python_import_segments(&name_segments);
1204 let mut segments = module_segments.clone();
1205 segments.extend(name_segments);
1206 let module = join_python_import_segments(&module_segments);
1207 infos.push(ImportInfo {
1208 raw_snippet: if let Some(alias) = &alias {
1209 format!("from {module} import {imported_name} as {alias}")
1210 } else {
1211 format!("from {module} import {imported_name}")
1212 },
1213 is_wildcard: false,
1214 is_global: false,
1215 identifier: Some(alias.clone().unwrap_or_else(|| imported_name.clone())),
1216 alias,
1217 path: Some(StructuredImportPath {
1218 segments,
1219 kind: Some(StructuredImportPathKind::ImportFrom),
1220 lexical_prefixes: Vec::new(),
1221 lexical_scopes: Vec::new(),
1222 declaration_start_byte: node.start_byte(),
1223 }),
1224 binder_span,
1225 });
1226 }
1227 infos
1228}
1229
1230fn python_module_segments(module: Node<'_>, source: &str) -> Vec<String> {
1231 if module.kind() == "relative_import" {
1232 let mut cursor = module.walk();
1233 let mut prefix = String::new();
1234 let mut path_node = None;
1235 for child in module.named_children(&mut cursor) {
1236 match child.kind() {
1237 "import_prefix" if prefix.is_empty() => {
1238 prefix = py_node_text(child, source).trim().to_string();
1239 }
1240 "dotted_name" if path_node.is_none() => {
1241 path_node = Some(child);
1242 }
1243 _ => {}
1244 }
1245 }
1246 let mut segments = path_node
1247 .map(|path| python_path_segments(path, source))
1248 .unwrap_or_default();
1249 if !prefix.is_empty() {
1250 if let Some(first) = segments.first_mut() {
1251 first.insert_str(0, &prefix);
1252 } else {
1253 segments.push(prefix);
1254 }
1255 }
1256 return segments;
1257 }
1258 python_path_segments(module, source)
1259}
1260
1261fn python_first_segment_node(node: Node<'_>) -> Option<Node<'_>> {
1265 match node.kind() {
1266 "identifier" => Some(node),
1267 "dotted_name" => {
1268 let mut cursor = node.walk();
1269 node.named_children(&mut cursor)
1270 .find(|child| child.kind() == "identifier")
1271 }
1272 _ => None,
1273 }
1274}
1275
1276fn python_path_segments(node: Node<'_>, source: &str) -> Vec<String> {
1277 match node.kind() {
1278 "identifier" => vec![py_node_text(node, source).trim().to_string()],
1279 "dotted_name" => {
1280 let mut segments = Vec::new();
1281 let mut cursor = node.walk();
1282 for child in node.named_children(&mut cursor) {
1283 segments.extend(python_path_segments(child, source));
1284 }
1285 segments
1286 }
1287 _ => {
1288 let mut segments = Vec::new();
1289 let mut cursor = node.walk();
1290 for child in node.named_children(&mut cursor) {
1291 segments.extend(python_path_segments(child, source));
1292 }
1293 segments
1294 }
1295 }
1296}
1297
1298fn join_python_import_segments(segments: &[String]) -> String {
1299 let Some((first, rest)) = segments.split_first() else {
1300 return String::new();
1301 };
1302 if first.starts_with('.') && !rest.is_empty() {
1303 format!("{first}.{}", rest.join("."))
1304 } else {
1305 segments.join(".")
1306 }
1307}
1308
1309pub fn python_namespace_binding_name(
1310 import: &ImportInfo,
1311 alias: Option<&str>,
1312 module: &str,
1313) -> String {
1314 import
1315 .identifier
1316 .clone()
1317 .or_else(|| alias.map(str::to_string))
1318 .unwrap_or_else(|| module.to_string())
1319}
1320
1321pub fn python_namespace_binding_module(
1322 import: &ImportInfo,
1323 alias: Option<&str>,
1324 module: &str,
1325) -> String {
1326 if alias.is_some() {
1327 return module.to_string();
1328 }
1329 import
1330 .path
1331 .as_ref()
1332 .and_then(|path| path.segments.first().cloned())
1333 .unwrap_or_else(|| module.to_string())
1334}
1335
1336pub fn resolve_python_relative_module(
1337 source_file: &ProjectFile,
1338 module_expr: &str,
1339) -> Option<String> {
1340 resolve_python_relative_module_from_package(&python_current_package(source_file), module_expr)
1341}
1342
1343pub fn resolve_python_relative_module_from_package(
1347 current_package: &str,
1348 module_expr: &str,
1349) -> Option<String> {
1350 let level = module_expr.chars().take_while(|ch| *ch == '.').count();
1351 let suffix = module_expr[level..].trim_matches('.');
1352 let mut parts: Vec<_> = current_package
1353 .split('.')
1354 .filter(|part| !part.is_empty())
1355 .map(str::to_string)
1356 .collect();
1357 if level == 0 {
1358 return Some(module_expr.to_string());
1359 }
1360 if level > 0 {
1361 if level - 1 > parts.len() {
1362 return None;
1363 }
1364 parts.truncate(parts.len() - (level - 1));
1365 }
1366 if !suffix.is_empty() {
1367 parts.extend(suffix.split('.').map(str::to_string));
1368 }
1369 Some(parts.join("."))
1370}
1371
1372fn python_current_package(source_file: &ProjectFile) -> String {
1373 let module = python_module_name(source_file);
1374 if source_file
1375 .rel_path()
1376 .file_name()
1377 .and_then(|name| name.to_str())
1378 == Some("__init__.py")
1379 {
1380 module
1381 } else {
1382 module
1383 .rsplit_once('.')
1384 .map(|(package, _)| package.to_string())
1385 .unwrap_or_default()
1386 }
1387}