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
387pub fn python_import_bindings_from_imports(
394 imports: &[ImportInfo],
395 root: Node<'_>,
396 source_len: usize,
397) -> Vec<PythonImportBinding> {
398 let mut bindings = imports
399 .iter()
400 .filter_map(|import| {
401 let path = import.path.as_ref()?;
402 let details = python_import_details(import)?;
403 let mut binding_scope = None;
404 for scope in path.lexical_scopes.iter().rev() {
405 let node = root.descendant_for_byte_range(scope.start_byte, scope.end_byte)?;
406 if node.start_byte() != scope.start_byte || node.end_byte() != scope.end_byte {
407 return None;
408 }
409 if matches!(node.kind(), "function_definition" | "lambda") {
410 binding_scope = Some((scope.start_byte, scope.end_byte, true));
411 break;
412 }
413 }
414 let (scope_start_byte, scope_end_byte, function_scoped) =
415 binding_scope.unwrap_or((0, source_len, false));
416 let start_byte = path.declaration_start_byte;
417 match details {
418 PythonImportDetails::Import { module, alias } => {
419 let consumed_attributes = if alias.is_some() {
420 0
421 } else {
422 path.segments.len().saturating_sub(1)
423 };
424 Some(PythonImportBinding {
425 start_byte,
426 scope_start_byte,
427 scope_end_byte,
428 function_scoped,
429 local_name: alias.or_else(|| path.segments.first().cloned())?,
430 qualified_name: module,
431 consumed_attributes,
432 })
433 }
434 PythonImportDetails::FromImport {
435 module,
436 name,
437 alias,
438 wildcard: false,
439 } => Some(PythonImportBinding {
440 start_byte,
441 scope_start_byte,
442 scope_end_byte,
443 function_scoped,
444 local_name: alias.unwrap_or_else(|| name.clone()),
445 qualified_name: format!("{module}.{name}"),
446 consumed_attributes: 0,
447 }),
448 PythonImportDetails::FromImport { wildcard: true, .. } => None,
449 }
450 })
451 .collect::<Vec<_>>();
452 bindings.sort_by_key(|binding| binding.start_byte);
453 bindings
454}
455
456fn python_import_binding_scope(node: Node<'_>, source_len: usize) -> (usize, usize, bool) {
457 let mut parent = node.parent();
458 while let Some(scope) = parent {
459 if matches!(scope.kind(), "function_definition" | "lambda") {
460 return (scope.start_byte(), scope.end_byte(), true);
461 }
462 parent = scope.parent();
463 }
464 (0, source_len, false)
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use brokk_bifrost_core::analyzer::usages::model::ImportBinder;
471
472 fn importlib_assignment_for(source: &str, importlib_local: &str) -> Option<String> {
473 let tree = parse_python_tree(source).expect("valid Python fixture");
474 let reference_start = source.rfind("canonical.setup").expect("reference site");
475 let mut stack = vec![tree.root_node()];
476 let mut reference = None;
477 while let Some(node) = stack.pop() {
478 if node.kind() == "identifier"
479 && node.start_byte() == reference_start
480 && node_source_text(node, source) == "canonical"
481 {
482 reference = Some(node);
483 break;
484 }
485 let mut cursor = node.walk();
486 stack.extend(node.named_children(&mut cursor));
487 }
488 imported_module_assignment_at(
489 reference.expect("canonical reference node"),
490 "canonical",
491 source,
492 |local| local == importlib_local,
493 )
494 }
495
496 #[test]
497 fn existing_tree_import_bindings_preserve_function_scope_and_relative_identity() {
498 let source = r#"def register():
499 from .workers.special import Worker
500 return Worker()
501"#;
502 let tree = parse_python_tree(source).expect("Python tree");
503 let bindings = python_import_bindings_from_tree(tree.root_node(), source);
504
505 assert_eq!(bindings.len(), 1, "{bindings:#?}");
506 assert!(bindings[0].is_function_scoped(), "{bindings:#?}");
507 assert_eq!(bindings[0].local_name, "Worker");
508 assert_eq!(bindings[0].qualified_name, ".workers.special.Worker");
509 assert!(bindings[0].scope_start_byte <= bindings[0].start_byte);
510 assert!(bindings[0].scope_end_byte >= source.rfind("Worker()").unwrap());
511 }
512
513 #[test]
514 fn importlib_assignment_requires_a_direct_literal_dominating_write() {
515 let valid = r#"import importlib as loader
516
517def test():
518 canonical = loader.import_module("routes.contacts.contacts_routes")
519 assert canonical.setup_contacts_routes
520"#;
521 assert_eq!(
522 importlib_assignment_for(valid, "loader"),
523 Some("routes.contacts.contacts_routes".to_string())
524 );
525
526 for near_miss in [
527 r#"import importlib as loader
528def test(module_name):
529 canonical = loader.import_module(module_name)
530 assert canonical.setup_contacts_routes
531"#,
532 r#"import importlib as loader
533def test():
534 if enabled:
535 canonical = loader.import_module("routes.contacts.contacts_routes")
536 assert canonical.setup_contacts_routes
537"#,
538 r#"import importlib as loader
539def test():
540 canonical = loader.import_module("routes.contacts.contacts_routes")
541 canonical = object()
542 assert canonical.setup_contacts_routes
543"#,
544 ] {
545 assert_eq!(
546 importlib_assignment_for(near_miss, "loader"),
547 None,
548 "near miss must not infer a module receiver: {near_miss:?}"
549 );
550 }
551 assert_eq!(
552 importlib_assignment_for(valid, "shadowed_loader"),
553 None,
554 "a similarly shaped non-importlib receiver must not resolve"
555 );
556 }
557
558 fn replacement_for(source: &str, binder: &ImportBinder) -> Option<PythonModuleReplacement> {
559 let tree = parse_python_tree(source).expect("valid Python fixture");
560 let root = tree.root_node();
561 let mut replacement = None;
562 let mut cursor = root.walk();
563 for statement in root.named_children(&mut cursor) {
564 let statement = if statement.kind() == "expression_statement" {
565 statement.named_child(0).expect("fixture expression")
566 } else {
567 statement
568 };
569 if statement.kind() == "assignment"
570 && let Some(next) =
571 module_replacement_from_assignment(statement, source, &binder.bindings)
572 && replacement.replace(next).is_some()
573 {
574 return None;
575 }
576 }
577 replacement
578 }
579
580 #[test]
581 fn module_replacement_requires_exact_structured_import_bindings() {
582 let source = r#"import sys as _sys
583from routes.contacts import contacts_routes as _canonical
584
585_sys.modules[__name__] = _canonical
586"#;
587 let mut binder = ImportBinder::empty();
588 binder.bindings.insert(
589 "_sys".to_string(),
590 ImportBinding {
591 module_specifier: "sys".to_string(),
592 namespace_imported_module: Some("sys".to_string()),
593 kind: ImportKind::Namespace,
594 imported_name: None,
595 },
596 );
597 binder.bindings.insert(
598 "_canonical".to_string(),
599 ImportBinding {
600 module_specifier: "routes.contacts.contacts_routes".to_string(),
601 namespace_imported_module: None,
602 kind: ImportKind::Namespace,
603 imported_name: None,
604 },
605 );
606
607 assert_eq!(
608 replacement_for(source, &binder),
609 Some(PythonModuleReplacement {
610 target_module: "routes.contacts.contacts_routes".to_string(),
611 })
612 );
613
614 for near_miss in [
615 "cache.modules[__name__] = _canonical\n",
616 "_sys.modules[module_name] = _canonical\n",
617 "_sys.modules[__name__] = build()\n",
618 "def replace():\n _sys.modules[__name__] = _canonical\n",
619 ] {
620 assert_eq!(
621 replacement_for(near_miss, &binder),
622 None,
623 "near miss must not replace module identity: {near_miss:?}"
624 );
625 }
626 }
627}
628
629pub(crate) fn python_imports_by_statement(imports: &[ImportInfo]) -> HashMap<usize, &[ImportInfo]> {
632 let mut statement_imports = HashMap::default();
633 for group in imports.chunk_by(|left, right| {
634 left.path.as_ref().map(|path| path.declaration_start_byte)
635 == right.path.as_ref().map(|path| path.declaration_start_byte)
636 }) {
637 if let Some(path) = &group[0].path {
638 assert!(
639 statement_imports
640 .insert(path.declaration_start_byte, group)
641 .is_none(),
642 "canonical Python import leaves must remain in statement order"
643 );
644 }
645 }
646 statement_imports
647}
648
649pub fn module_replacement_of(
650 python: &dyn PythonSource,
651 file: &ProjectFile,
652 source: &str,
653 imports: &[ImportInfo],
654) -> Option<PythonModuleReplacement> {
655 let tree = parse_python_tree(source)?;
656 let root = tree.root_node();
657 let mut bindings: HashMap<String, ImportBinding> = HashMap::default();
658 let statement_imports = python_imports_by_statement(imports);
659 let mut replacement = None;
660 let mut cursor = root.walk();
661 for statement in root.named_children(&mut cursor) {
662 let statement = if statement.kind() == "expression_statement" {
663 let Some(expression) = statement.named_child(0) else {
664 continue;
665 };
666 expression
667 } else {
668 statement
669 };
670 match statement.kind() {
671 "import_statement" | "import_from_statement" => {
672 if let Some(imports) = statement_imports.get(&statement.start_byte()) {
673 bindings.extend(import_binder_from_imports(python, file, imports).bindings);
674 }
675 }
676 "assignment" => {
677 if let Some(next) = module_replacement_from_assignment(statement, source, &bindings)
678 && replacement.replace(next).is_some()
679 {
680 return None;
681 }
682 remove_direct_scope_bindings(statement, source, &mut bindings);
683 }
684 _ => remove_direct_scope_bindings(statement, source, &mut bindings),
685 }
686 }
687 replacement
688}
689
690pub fn resolve_import_bindings(
691 python: &dyn PythonSource,
692 token: QueryToken<'_>,
693 file: &ProjectFile,
694) -> HashMap<String, CodeUnit> {
695 let imports = python.import_info_of(token, file);
696 let mut bindings = HashMap::default();
697 for resolved in resolve_imports_batched(python, file, &imports) {
698 for (binding, code_unit) in resolved {
699 bindings.insert(binding, code_unit);
700 }
701 }
702 bindings
703}
704
705pub fn resolve_imports_batched(
712 python: &dyn PythonSource,
713 file: &ProjectFile,
714 imports: &[ImportInfo],
715) -> Vec<Vec<(String, CodeUnit)>> {
716 let primary_fqns: Vec<Option<String>> = imports
717 .iter()
718 .map(|import| primary_module_fqn(file, import))
719 .collect();
720 let to_resolve: Vec<String> = primary_fqns.iter().flatten().cloned().collect();
721 let mut batch_results =
722 resolve_module_code_units_batch(python, Some(file), &to_resolve).into_iter();
723
724 imports
725 .iter()
726 .zip(primary_fqns.iter())
727 .map(|(import, primary_fqn)| {
728 let hint = primary_fqn.as_ref().map(|_| batch_results.next().unwrap());
729 resolve_import_with_hint(python, file, import, hint.as_ref())
730 })
731 .collect()
732}
733
734pub fn resolve_import_files_batched(
744 python: &dyn PythonSource,
745 file: &ProjectFile,
746 imports: &[ImportInfo],
747) -> HashSet<ProjectFile> {
748 let mut module_fqns = Vec::new();
749 for import in imports {
750 match python_import_details(import) {
751 Some(PythonImportDetails::Import { module, .. }) => module_fqns.push(module),
752 Some(PythonImportDetails::FromImport {
753 module,
754 name,
755 wildcard,
756 ..
757 }) => {
758 let resolved_module = if module.starts_with('.') {
759 resolve_python_relative_module(file, &module)
760 } else {
761 Some(module)
762 };
763 let Some(resolved_module) = resolved_module else {
764 continue;
765 };
766 if !wildcard {
767 module_fqns.push(format!("{resolved_module}.{name}"));
768 }
769 module_fqns.push(resolved_module);
770 }
771 None => {}
772 }
773 }
774 module_fqns.sort();
775 module_fqns.dedup();
776
777 python
778 .path_module_fqns_batch(&module_fqns)
779 .into_iter()
780 .flatten()
781 .flatten()
782 .filter(CodeUnit::is_module)
783 .map(|unit| unit.source().clone())
784 .collect()
785}
786
787fn primary_module_fqn(file: &ProjectFile, import: &ImportInfo) -> Option<String> {
791 match python_import_details(import)? {
792 PythonImportDetails::Import { module, alias } => Some(python_namespace_binding_module(
793 import,
794 alias.as_deref(),
795 &module,
796 )),
797 PythonImportDetails::FromImport {
798 module,
799 name,
800 wildcard,
801 ..
802 } => {
803 if wildcard {
804 return None;
805 }
806 let resolved_module = if module.starts_with('.') {
807 resolve_python_relative_module(file, &module)
808 } else {
809 Some(module)
810 };
811 resolved_module.map(|resolved_module| format!("{resolved_module}.{name}"))
812 }
813 }
814}
815
816pub fn resolve_import(
817 python: &dyn PythonSource,
818 file: &ProjectFile,
819 import: &ImportInfo,
820) -> Vec<(String, CodeUnit)> {
821 resolve_import_with_hint(python, file, import, None)
822}
823
824fn resolve_import_with_hint(
827 python: &dyn PythonSource,
828 file: &ProjectFile,
829 import: &ImportInfo,
830 primary_hint: Option<&Option<CodeUnit>>,
831) -> Vec<(String, CodeUnit)> {
832 if let Some(details) = python_import_details(import) {
833 match details {
834 PythonImportDetails::Import { module, alias } => {
835 let binding = python_namespace_binding_name(import, alias.as_deref(), &module);
836 let bound_module =
837 python_namespace_binding_module(import, alias.as_deref(), &module);
838 let resolved = match primary_hint {
839 Some(hint) => hint.clone(),
840 None => resolve_module_code_unit(python, Some(file), &bound_module),
841 };
842 if let Some(module_code_unit) = resolved {
843 return vec![(binding, module_code_unit)];
844 }
845 }
846 PythonImportDetails::FromImport {
847 module,
848 name,
849 alias,
850 wildcard,
851 } => {
852 let resolved_module = if module.starts_with('.') {
853 resolve_python_relative_module(file, &module)
854 } else {
855 Some(module)
856 };
857 let Some(resolved_module) = resolved_module else {
858 return Vec::new();
859 };
860 if wildcard {
861 return public_declarations_in_module(python, file, &resolved_module)
862 .into_iter()
863 .map(|code_unit| (code_unit.identifier().to_string(), code_unit))
864 .collect();
865 }
866
867 let binding = alias.clone().unwrap_or_else(|| name.clone());
868 let module_candidate = format!("{resolved_module}.{name}");
869 let resolved = match primary_hint {
870 Some(hint) => hint.clone(),
871 None => resolve_module_code_unit(python, Some(file), &module_candidate),
872 };
873 if let Some(code_unit) = resolved {
874 return vec![(binding, code_unit)];
875 }
876 let exported =
877 resolve_exported_name_from_module(python, Some(file), &resolved_module, &name);
878 if !exported.is_empty() {
879 return exported
880 .into_iter()
881 .map(|code_unit| (binding.clone(), code_unit))
882 .collect();
883 }
884 let definitions: Vec<_> = python.definitions(&module_candidate).collect();
885 if !definitions.is_empty() {
886 return definitions
887 .into_iter()
888 .map(|code_unit| (binding.clone(), code_unit))
889 .collect();
890 }
891 let package_candidate: Vec<_> = python
892 .definitions(&format!("{resolved_module}.{name}"))
893 .collect();
894 if !package_candidate.is_empty() {
895 return package_candidate
896 .into_iter()
897 .map(|code_unit| (binding.clone(), code_unit))
898 .collect();
899 }
900 }
901 }
902 }
903 Vec::new()
904}
905
906pub fn resolve_exported_fqn(
910 python: &dyn PythonSource,
911 importer: Option<&ProjectFile>,
912 fqn: &str,
913) -> Vec<CodeUnit> {
914 let Some((module, name)) = fqn.rsplit_once('.') else {
915 return Vec::new();
916 };
917 resolve_exported_name_from_module(python, importer, module, name)
918}
919
920fn resolve_direct_named_exported_fqn(
929 python: &dyn PythonSource,
930 importer: Option<&ProjectFile>,
931 fqn: &str,
932) -> Option<Vec<CodeUnit>> {
933 let (module, name) = fqn.rsplit_once('.')?;
934 let mut results = Vec::new();
935 let mut queue = VecDeque::from([(importer.cloned(), module.to_string(), name.to_string())]);
936 let mut visited = HashSet::default();
937
938 while let Some((importer, module, export_name)) = queue.pop_front() {
939 if !visited.insert((module.clone(), export_name.clone())) {
940 continue;
941 }
942 let module_unit = resolve_module_code_unit(python, importer.as_ref(), &module)?;
943 let file = module_unit.source();
944 let local = local_export_declarations(python, file, &export_name);
945 let binder = python.import_binder_of(file);
946 let binding = binder.bindings.get(&export_name);
947 if !local.is_empty() && binding.is_some() {
948 return None;
949 }
950 if !local.is_empty() {
951 results.extend(local);
952 continue;
953 }
954 let binding = binding?;
955 if binding.kind != ImportKind::Named {
956 return None;
957 }
958 let imported_name = binding.imported_name.as_ref()?;
959 queue.push_back((
960 Some(file.clone()),
961 binding.module_specifier.clone(),
962 imported_name.clone(),
963 ));
964 }
965
966 results.sort_by(|left, right| {
967 left.source()
968 .cmp(right.source())
969 .then_with(|| left.fq_name().cmp(&right.fq_name()))
970 });
971 results.dedup();
972 (!results.is_empty()).then_some(results)
973}
974
975pub fn resolve_fqn_candidates(
984 python: &dyn PythonSource,
985 importer: Option<&ProjectFile>,
986 fqn: &str,
987 exact: impl FnOnce(&str) -> Vec<CodeUnit>,
988) -> Vec<CodeUnit> {
989 if let Some(candidates) = resolve_direct_named_exported_fqn(python, importer, fqn) {
990 return candidates;
991 }
992 let candidates = resolve_exported_fqn(python, importer, fqn);
993 if !candidates.is_empty() {
994 return candidates;
995 }
996 exact(fqn)
997}
998
999fn resolve_exported_name_from_module(
1000 python: &dyn PythonSource,
1001 importer: Option<&ProjectFile>,
1002 module: &str,
1003 name: &str,
1004) -> Vec<CodeUnit> {
1005 let Some(module_unit) = resolve_module_code_unit(python, importer, module) else {
1006 return Vec::new();
1007 };
1008 resolve_exported_name(python, module_unit.source(), name)
1009}
1010
1011fn resolve_exported_name(
1012 python: &dyn PythonSource,
1013 module_file: &ProjectFile,
1014 name: &str,
1015) -> Vec<CodeUnit> {
1016 let mut results = Vec::new();
1017 let mut queue = VecDeque::from([(module_file.clone(), name.to_string())]);
1018 let mut visited = HashSet::default();
1019
1020 while let Some((file, export_name)) = queue.pop_front() {
1021 if !visited.insert((file.clone(), export_name.clone())) {
1022 continue;
1023 }
1024
1025 let index = python.export_index_of(&file);
1026 if let Some(entry) = index.exports_by_name.get(&export_name) {
1027 match entry {
1028 ExportEntry::Local { local_name } => {
1029 results.extend(local_export_declarations(python, &file, local_name));
1030 }
1031 ExportEntry::ReexportedNamed {
1032 module_specifier,
1033 imported_name,
1034 } => {
1035 for target_file in
1036 resolve_module_files_for_export(python, &file, module_specifier)
1037 {
1038 queue.push_back((target_file, imported_name.clone()));
1039 }
1040 }
1041 ExportEntry::ReexportedModule { module_specifier } => {
1042 results.extend(resolve_module_code_unit(
1045 python,
1046 Some(&file),
1047 module_specifier,
1048 ));
1049 }
1050 ExportEntry::Default { local_name } => {
1051 if let Some(local_name) = local_name {
1052 results.extend(local_export_declarations(python, &file, local_name));
1053 }
1054 }
1055 }
1056 continue;
1057 }
1058
1059 if !export_name.starts_with('_') {
1060 for star in &index.reexport_stars {
1061 for target_file in
1062 resolve_module_files_for_export(python, &file, &star.module_specifier)
1063 {
1064 queue.push_back((target_file, export_name.clone()));
1065 }
1066 }
1067 }
1068 }
1069
1070 results.sort_by(|left, right| {
1071 left.source()
1072 .cmp(right.source())
1073 .then_with(|| left.fq_name().cmp(&right.fq_name()))
1074 });
1075 results.dedup();
1076 results
1077}
1078
1079fn local_export_declarations(
1080 index: &dyn CodeUnitIndex,
1081 file: &ProjectFile,
1082 local_name: &str,
1083) -> Vec<CodeUnit> {
1084 index
1085 .top_level_declarations(file)
1086 .into_iter()
1087 .filter(|unit| {
1088 unit.identifier() == local_name
1089 && index
1090 .parent_of(unit)
1091 .is_some_and(|parent| parent.is_module() && parent.source() == file)
1092 })
1093 .collect()
1094}
1095
1096fn resolve_module_files_for_export(
1097 python: &dyn PythonSource,
1098 importing_file: &ProjectFile,
1099 module_specifier: &str,
1100) -> Vec<ProjectFile> {
1101 let resolved_module = if module_specifier.starts_with('.') {
1102 resolve_python_relative_module(importing_file, module_specifier)
1103 } else {
1104 Some(module_specifier.to_string())
1105 };
1106 let Some(resolved_module) = resolved_module else {
1107 return Vec::new();
1108 };
1109 resolve_module_code_unit(python, Some(importing_file), &resolved_module)
1113 .map(|unit| vec![unit.source().clone()])
1114 .unwrap_or_default()
1115}
1116
1117pub fn extract_package_from_python_wildcard(import: &ImportInfo) -> Option<String> {
1118 let details = python_import_details(import)?;
1119 match details {
1120 PythonImportDetails::FromImport {
1121 module, wildcard, ..
1122 } if wildcard => Some(module),
1123 _ => None,
1124 }
1125}
1126
1127#[derive(Debug, Clone)]
1128pub enum PythonImportDetails {
1129 Import {
1130 module: String,
1131 alias: Option<String>,
1132 },
1133 FromImport {
1134 module: String,
1135 name: String,
1136 alias: Option<String>,
1137 wildcard: bool,
1138 },
1139}
1140
1141#[derive(Debug, Clone)]
1148pub struct PythonImportSyntax<'tree> {
1149 pub info: ImportInfo,
1150 pub declaration: Node<'tree>,
1151 pub target: Option<Node<'tree>>,
1152 pub alias: Option<Node<'tree>>,
1153}
1154
1155pub fn python_import_infos_from_node(node: Node<'_>, source: &str) -> Vec<ImportInfo> {
1156 python_import_syntaxes_from_node(node, source)
1157 .into_iter()
1158 .map(|syntax| syntax.info)
1159 .collect()
1160}
1161
1162pub fn python_import_syntaxes_from_node<'tree>(
1166 node: Node<'tree>,
1167 source: &str,
1168) -> Vec<PythonImportSyntax<'tree>> {
1169 match node.kind() {
1170 "import_statement" => python_namespace_import_syntaxes(node, source),
1171 "import_from_statement" => python_from_import_syntaxes(node, source),
1172 _ => Vec::new(),
1173 }
1174}
1175
1176pub fn python_import_details(import: &ImportInfo) -> Option<PythonImportDetails> {
1177 let path = import.path.as_ref()?;
1178 match path.kind? {
1179 StructuredImportPathKind::Namespace => Some(PythonImportDetails::Import {
1180 module: join_python_import_segments(&path.segments),
1181 alias: import.alias.clone(),
1182 }),
1183 StructuredImportPathKind::StaticMember | StructuredImportPathKind::ExternCrate => None,
1185 StructuredImportPathKind::ImportFrom => {
1186 let (name, module_segments) = if import.is_wildcard {
1187 ("*".to_string(), path.segments.as_slice())
1188 } else {
1189 let (name, module_segments) = path.segments.split_last()?;
1190 (name.clone(), module_segments)
1191 };
1192 Some(PythonImportDetails::FromImport {
1193 module: join_python_import_segments(module_segments),
1194 name,
1195 alias: import.alias.clone(),
1196 wildcard: import.is_wildcard,
1197 })
1198 }
1199 }
1200}
1201
1202fn python_namespace_import_syntaxes<'tree>(
1203 node: Node<'tree>,
1204 source: &str,
1205) -> Vec<PythonImportSyntax<'tree>> {
1206 let mut infos = Vec::new();
1207 let mut cursor = node.walk();
1208 for imported in node.children_by_field_name("name", &mut cursor) {
1209 let (module_node, alias_node) = if imported.kind() == "aliased_import" {
1210 let Some(name) = imported.child_by_field_name("name") else {
1211 continue;
1212 };
1213 (name, imported.child_by_field_name("alias"))
1214 } else {
1215 (imported, None)
1216 };
1217 let alias = alias_node
1218 .map(|alias| py_node_text(alias, source).trim().to_string())
1219 .filter(|alias| !alias.is_empty());
1220 let segments = python_path_segments(module_node, source);
1221 if segments.is_empty() {
1222 continue;
1223 }
1224 let target = alias
1227 .is_none()
1228 .then(|| python_first_segment_node(module_node))
1229 .flatten();
1230 let binder_span = alias_node
1231 .or(target)
1232 .map(brokk_bifrost_core::analyzer::common::node_span);
1233 let module = join_python_import_segments(&segments);
1234 let identifier = alias.clone().or_else(|| segments.first().cloned());
1235 let info = ImportInfo {
1236 raw_snippet: if let Some(alias) = &alias {
1237 format!("import {module} as {alias}")
1238 } else {
1239 format!("import {module}")
1240 },
1241 is_wildcard: false,
1242 is_global: false,
1243 identifier,
1244 alias,
1245 path: Some(StructuredImportPath {
1246 segments,
1247 kind: Some(StructuredImportPathKind::Namespace),
1248 lexical_prefixes: Vec::new(),
1249 lexical_scopes: Vec::new(),
1250 declaration_start_byte: node.start_byte(),
1251 }),
1252 binder_span,
1253 };
1254 infos.push(PythonImportSyntax {
1255 info,
1256 declaration: node,
1257 target,
1258 alias: alias_node,
1259 });
1260 }
1261 infos
1262}
1263
1264fn python_from_import_syntaxes<'tree>(
1265 node: Node<'tree>,
1266 source: &str,
1267) -> Vec<PythonImportSyntax<'tree>> {
1268 let Some(module_node) = node.child_by_field_name("module_name") else {
1269 return Vec::new();
1270 };
1271 let module_segments = python_module_segments(module_node, source);
1272 if module_segments.is_empty() {
1273 return Vec::new();
1274 }
1275
1276 let mut infos = Vec::new();
1277 let has_wildcard_import = {
1278 let mut cursor = node.walk();
1279 node.named_children(&mut cursor)
1280 .any(|child| child.kind() == "wildcard_import")
1281 };
1282 let mut cursor = node.walk();
1283 let imported_names: Vec<_> = node.children_by_field_name("name", &mut cursor).collect();
1284 if has_wildcard_import {
1285 let module = join_python_import_segments(&module_segments);
1286 let info = ImportInfo {
1287 raw_snippet: format!("from {module} import *"),
1288 is_wildcard: true,
1289 is_global: false,
1290 identifier: None,
1291 alias: None,
1292 path: Some(StructuredImportPath {
1293 segments: module_segments,
1294 kind: Some(StructuredImportPathKind::ImportFrom),
1295 lexical_prefixes: Vec::new(),
1296 lexical_scopes: Vec::new(),
1297 declaration_start_byte: node.start_byte(),
1298 }),
1299 binder_span: None,
1300 };
1301 infos.push(PythonImportSyntax {
1302 info,
1303 declaration: node,
1304 target: None,
1305 alias: None,
1306 });
1307 return infos;
1308 }
1309 if imported_names.is_empty() {
1310 return infos;
1311 }
1312
1313 for imported in imported_names {
1314 let (name_node, alias_node) = if imported.kind() == "aliased_import" {
1315 let Some(name) = imported.child_by_field_name("name") else {
1316 continue;
1317 };
1318 (name, imported.child_by_field_name("alias"))
1319 } else {
1320 (imported, None)
1321 };
1322 let alias = alias_node
1323 .map(|alias| py_node_text(alias, source).trim().to_string())
1324 .filter(|alias| !alias.is_empty());
1325 let name_segments = python_path_segments(name_node, source);
1326 if name_segments.is_empty() {
1327 continue;
1328 }
1329 let target = alias
1332 .is_none()
1333 .then(|| {
1334 (name_segments.len() == 1)
1335 .then(|| python_first_segment_node(name_node))
1336 .flatten()
1337 })
1338 .flatten();
1339 let binder_span = alias_node
1340 .or(target)
1341 .map(brokk_bifrost_core::analyzer::common::node_span);
1342 let imported_name = join_python_import_segments(&name_segments);
1343 let mut segments = module_segments.clone();
1344 segments.extend(name_segments);
1345 let module = join_python_import_segments(&module_segments);
1346 let info = ImportInfo {
1347 raw_snippet: if let Some(alias) = &alias {
1348 format!("from {module} import {imported_name} as {alias}")
1349 } else {
1350 format!("from {module} import {imported_name}")
1351 },
1352 is_wildcard: false,
1353 is_global: false,
1354 identifier: Some(alias.clone().unwrap_or_else(|| imported_name.clone())),
1355 alias,
1356 path: Some(StructuredImportPath {
1357 segments,
1358 kind: Some(StructuredImportPathKind::ImportFrom),
1359 lexical_prefixes: Vec::new(),
1360 lexical_scopes: Vec::new(),
1361 declaration_start_byte: node.start_byte(),
1362 }),
1363 binder_span,
1364 };
1365 infos.push(PythonImportSyntax {
1366 info,
1367 declaration: node,
1368 target,
1369 alias: alias_node,
1370 });
1371 }
1372 infos
1373}
1374
1375fn python_module_segments(module: Node<'_>, source: &str) -> Vec<String> {
1376 if module.kind() == "relative_import" {
1377 let mut cursor = module.walk();
1378 let mut prefix = String::new();
1379 let mut path_node = None;
1380 for child in module.named_children(&mut cursor) {
1381 match child.kind() {
1382 "import_prefix" if prefix.is_empty() => {
1383 prefix = py_node_text(child, source).trim().to_string();
1384 }
1385 "dotted_name" if path_node.is_none() => {
1386 path_node = Some(child);
1387 }
1388 _ => {}
1389 }
1390 }
1391 let mut segments = path_node
1392 .map(|path| python_path_segments(path, source))
1393 .unwrap_or_default();
1394 if !prefix.is_empty() {
1395 if let Some(first) = segments.first_mut() {
1396 first.insert_str(0, &prefix);
1397 } else {
1398 segments.push(prefix);
1399 }
1400 }
1401 return segments;
1402 }
1403 python_path_segments(module, source)
1404}
1405
1406fn python_first_segment_node(node: Node<'_>) -> Option<Node<'_>> {
1410 match node.kind() {
1411 "identifier" => Some(node),
1412 "dotted_name" => {
1413 let mut cursor = node.walk();
1414 node.named_children(&mut cursor)
1415 .find(|child| child.kind() == "identifier")
1416 }
1417 _ => None,
1418 }
1419}
1420
1421fn python_path_segments(node: Node<'_>, source: &str) -> Vec<String> {
1422 match node.kind() {
1423 "identifier" => vec![py_node_text(node, source).trim().to_string()],
1424 "dotted_name" => {
1425 let mut segments = Vec::new();
1426 let mut cursor = node.walk();
1427 for child in node.named_children(&mut cursor) {
1428 segments.extend(python_path_segments(child, source));
1429 }
1430 segments
1431 }
1432 _ => {
1433 let mut segments = Vec::new();
1434 let mut cursor = node.walk();
1435 for child in node.named_children(&mut cursor) {
1436 segments.extend(python_path_segments(child, source));
1437 }
1438 segments
1439 }
1440 }
1441}
1442
1443fn join_python_import_segments(segments: &[String]) -> String {
1444 let Some((first, rest)) = segments.split_first() else {
1445 return String::new();
1446 };
1447 if first.starts_with('.') && !rest.is_empty() {
1448 format!("{first}.{}", rest.join("."))
1449 } else {
1450 segments.join(".")
1451 }
1452}
1453
1454pub fn python_namespace_binding_name(
1455 import: &ImportInfo,
1456 alias: Option<&str>,
1457 module: &str,
1458) -> String {
1459 import
1460 .identifier
1461 .clone()
1462 .or_else(|| alias.map(str::to_string))
1463 .unwrap_or_else(|| module.to_string())
1464}
1465
1466pub fn python_namespace_binding_module(
1467 import: &ImportInfo,
1468 alias: Option<&str>,
1469 module: &str,
1470) -> String {
1471 if alias.is_some() {
1472 return module.to_string();
1473 }
1474 import
1475 .path
1476 .as_ref()
1477 .and_then(|path| path.segments.first().cloned())
1478 .unwrap_or_else(|| module.to_string())
1479}
1480
1481pub fn resolve_python_relative_module(
1482 source_file: &ProjectFile,
1483 module_expr: &str,
1484) -> Option<String> {
1485 resolve_python_relative_module_from_package(&python_current_package(source_file), module_expr)
1486}
1487
1488pub fn resolve_python_relative_module_from_package(
1492 current_package: &str,
1493 module_expr: &str,
1494) -> Option<String> {
1495 let level = module_expr.chars().take_while(|ch| *ch == '.').count();
1496 let suffix = module_expr[level..].trim_matches('.');
1497 let mut parts: Vec<_> = current_package
1498 .split('.')
1499 .filter(|part| !part.is_empty())
1500 .map(str::to_string)
1501 .collect();
1502 if level == 0 {
1503 return Some(module_expr.to_string());
1504 }
1505 if level > 0 {
1506 if level - 1 > parts.len() {
1507 return None;
1508 }
1509 parts.truncate(parts.len() - (level - 1));
1510 }
1511 if !suffix.is_empty() {
1512 parts.extend(suffix.split('.').map(str::to_string));
1513 }
1514 Some(parts.join("."))
1515}
1516
1517fn python_current_package(source_file: &ProjectFile) -> String {
1518 let module = python_module_name(source_file);
1519 if source_file
1520 .rel_path()
1521 .file_name()
1522 .and_then(|name| name.to_str())
1523 == Some("__init__.py")
1524 {
1525 module
1526 } else {
1527 module
1528 .rsplit_once('.')
1529 .map(|(package, _)| package.to_string())
1530 .unwrap_or_default()
1531 }
1532}