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::usages::model::{ExportEntry, ImportBinding, ImportKind};
18use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, ProjectFile};
19use brokk_bifrost_core::hash::{HashMap, HashSet};
20use std::collections::VecDeque;
21use tree_sitter::Node;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct PythonModuleReplacement {
25 pub target_module: String,
26}
27
28fn module_replacement_from_assignment(
36 assignment: Node<'_>,
37 source: &str,
38 bindings: &HashMap<String, ImportBinding>,
39) -> Option<PythonModuleReplacement> {
40 let (left, right) = (
41 assignment.child_by_field_name("left")?,
42 assignment.child_by_field_name("right")?,
43 );
44 if left.kind() != "subscript" || right.kind() != "identifier" {
45 return None;
46 }
47 let (value, subscript) = (
48 left.child_by_field_name("value")?,
49 left.child_by_field_name("subscript")?,
50 );
51 if value.kind() != "attribute"
52 || subscript.kind() != "identifier"
53 || node_source_text(subscript, source) != "__name__"
54 {
55 return None;
56 }
57 let (sys_local, modules) = (
58 value.child_by_field_name("object")?,
59 value.child_by_field_name("attribute")?,
60 );
61 if sys_local.kind() != "identifier"
62 || modules.kind() != "identifier"
63 || node_source_text(modules, source) != "modules"
64 {
65 return None;
66 }
67 let sys_binding = bindings.get(node_source_text(sys_local, source))?;
68 let sys_module = sys_binding
69 .namespace_imported_module
70 .as_deref()
71 .unwrap_or(&sys_binding.module_specifier);
72 if sys_binding.kind != ImportKind::Namespace || sys_module != "sys" {
73 return None;
74 }
75
76 let target_binding = bindings.get(node_source_text(right, source))?;
77 if target_binding.kind != ImportKind::Namespace {
78 return None;
79 }
80 Some(PythonModuleReplacement {
81 target_module: target_binding
82 .namespace_imported_module
83 .clone()
84 .unwrap_or_else(|| target_binding.module_specifier.clone()),
85 })
86}
87
88fn remove_direct_scope_bindings(
89 statement: Node<'_>,
90 source: &str,
91 bindings: &mut HashMap<String, ImportBinding>,
92) {
93 let mut stack = vec![statement];
94 while let Some(node) = stack.pop() {
95 for binding in python_direct_scope_bindings_bounded(node, source, || true)
96 .expect("unbounded binding collection cannot be cancelled")
97 {
98 bindings.remove(node_source_text(binding.declaration, source));
99 }
100 if matches!(
101 node.kind(),
102 "function_definition" | "class_definition" | "lambda"
103 ) {
104 continue;
105 }
106 let mut cursor = node.walk();
107 stack.extend(node.named_children(&mut cursor));
108 }
109}
110
111pub fn parse_python_import_infos(source: &str) -> Vec<ImportInfo> {
115 let mut parser = tree_sitter::Parser::new();
116 parser
117 .set_language(&tree_sitter_python::LANGUAGE.into())
118 .expect("failed to load Python parser");
119 let Some(tree) = parser.parse(source, None) else {
120 return Vec::new();
121 };
122 let mut pending = vec![tree.root_node()];
123 let mut imports = Vec::new();
124 while let Some(node) = pending.pop() {
125 if matches!(node.kind(), "import_statement" | "import_from_statement") {
126 imports.extend(python_import_infos_from_node(node, source));
127 continue;
128 }
129 let mut cursor = node.walk();
130 pending.extend(node.named_children(&mut cursor));
131 }
132 imports
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct PythonImportBinding {
137 pub start_byte: usize,
138 pub scope_start_byte: usize,
139 pub scope_end_byte: usize,
140 function_scoped: bool,
141 pub local_name: String,
142 pub qualified_name: String,
143 pub consumed_attributes: usize,
144}
145
146impl PythonImportBinding {
147 pub fn is_function_scoped(&self) -> bool {
148 self.function_scoped
149 }
150}
151
152pub fn parse_python_import_bindings(source: &str) -> Vec<PythonImportBinding> {
156 let mut parser = tree_sitter::Parser::new();
157 parser
158 .set_language(&tree_sitter_python::LANGUAGE.into())
159 .expect("failed to load Python parser");
160 let Some(tree) = parser.parse(source, None) else {
161 return Vec::new();
162 };
163 let mut pending = vec![tree.root_node()];
164 let mut nodes = Vec::new();
165 while let Some(node) = pending.pop() {
166 if matches!(node.kind(), "import_statement" | "import_from_statement") {
167 nodes.push(node);
168 continue;
169 }
170 let mut cursor = node.walk();
171 pending.extend(node.named_children(&mut cursor));
172 }
173 nodes.sort_by_key(Node::start_byte);
174 nodes
175 .into_iter()
176 .flat_map(|node| {
177 let (scope_start_byte, scope_end_byte, function_scoped) =
178 python_import_binding_scope(node, source.len());
179 python_import_infos_from_node(node, source)
180 .into_iter()
181 .filter_map(move |import| {
182 let path = import.path.as_ref()?;
183 let details = python_import_details(&import)?;
184 match details {
185 PythonImportDetails::Import { module, alias } => {
186 let consumed_attributes = if alias.is_some() {
187 0
188 } else {
189 path.segments.len().saturating_sub(1)
190 };
191 Some(PythonImportBinding {
192 start_byte: node.start_byte(),
193 scope_start_byte,
194 scope_end_byte,
195 function_scoped,
196 local_name: alias.or_else(|| path.segments.first().cloned())?,
197 qualified_name: module,
198 consumed_attributes,
199 })
200 }
201 PythonImportDetails::FromImport {
202 module,
203 name,
204 alias,
205 wildcard: false,
206 } => Some(PythonImportBinding {
207 start_byte: node.start_byte(),
208 scope_start_byte,
209 scope_end_byte,
210 function_scoped,
211 local_name: alias.unwrap_or(name.clone()),
212 qualified_name: format!("{module}.{name}"),
213 consumed_attributes: 0,
214 }),
215 PythonImportDetails::FromImport { wildcard: true, .. } => None,
216 }
217 })
218 })
219 .collect()
220}
221
222fn python_import_binding_scope(node: Node<'_>, source_len: usize) -> (usize, usize, bool) {
223 let mut parent = node.parent();
224 while let Some(scope) = parent {
225 if matches!(scope.kind(), "function_definition" | "lambda") {
226 return (scope.start_byte(), scope.end_byte(), true);
227 }
228 parent = scope.parent();
229 }
230 (0, source_len, false)
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use brokk_bifrost_core::analyzer::usages::model::ImportBinder;
237
238 fn replacement_for(source: &str, binder: &ImportBinder) -> Option<PythonModuleReplacement> {
239 let tree = parse_python_tree(source).expect("valid Python fixture");
240 let root = tree.root_node();
241 let mut replacement = None;
242 let mut cursor = root.walk();
243 for statement in root.named_children(&mut cursor) {
244 let statement = if statement.kind() == "expression_statement" {
245 statement.named_child(0).expect("fixture expression")
246 } else {
247 statement
248 };
249 if statement.kind() == "assignment"
250 && let Some(next) =
251 module_replacement_from_assignment(statement, source, &binder.bindings)
252 && replacement.replace(next).is_some()
253 {
254 return None;
255 }
256 }
257 replacement
258 }
259
260 #[test]
261 fn module_replacement_requires_exact_structured_import_bindings() {
262 let source = r#"import sys as _sys
263from routes.contacts import contacts_routes as _canonical
264
265_sys.modules[__name__] = _canonical
266"#;
267 let mut binder = ImportBinder::empty();
268 binder.bindings.insert(
269 "_sys".to_string(),
270 ImportBinding {
271 module_specifier: "sys".to_string(),
272 namespace_imported_module: Some("sys".to_string()),
273 kind: ImportKind::Namespace,
274 imported_name: None,
275 },
276 );
277 binder.bindings.insert(
278 "_canonical".to_string(),
279 ImportBinding {
280 module_specifier: "routes.contacts.contacts_routes".to_string(),
281 namespace_imported_module: None,
282 kind: ImportKind::Namespace,
283 imported_name: None,
284 },
285 );
286
287 assert_eq!(
288 replacement_for(source, &binder),
289 Some(PythonModuleReplacement {
290 target_module: "routes.contacts.contacts_routes".to_string(),
291 })
292 );
293
294 for near_miss in [
295 "cache.modules[__name__] = _canonical\n",
296 "_sys.modules[module_name] = _canonical\n",
297 "_sys.modules[__name__] = build()\n",
298 "def replace():\n _sys.modules[__name__] = _canonical\n",
299 ] {
300 assert_eq!(
301 replacement_for(near_miss, &binder),
302 None,
303 "near miss must not replace module identity: {near_miss:?}"
304 );
305 }
306 }
307}
308
309pub fn module_replacement_of(
310 python: &dyn PythonSource,
311 file: &ProjectFile,
312 source: &str,
313) -> Option<PythonModuleReplacement> {
314 let tree = parse_python_tree(source)?;
315 let root = tree.root_node();
316 let mut bindings: HashMap<String, ImportBinding> = HashMap::default();
317 let mut replacement = None;
318 let mut cursor = root.walk();
319 for statement in root.named_children(&mut cursor) {
320 let statement = if statement.kind() == "expression_statement" {
321 let Some(expression) = statement.named_child(0) else {
322 continue;
323 };
324 expression
325 } else {
326 statement
327 };
328 match statement.kind() {
329 "import_statement" | "import_from_statement" => {
330 let imports = python_import_infos_from_node(statement, source);
331 bindings.extend(import_binder_from_imports(python, file, &imports).bindings);
332 }
333 "assignment" => {
334 if let Some(next) = module_replacement_from_assignment(statement, source, &bindings)
335 && replacement.replace(next).is_some()
336 {
337 return None;
338 }
339 remove_direct_scope_bindings(statement, source, &mut bindings);
340 }
341 _ => remove_direct_scope_bindings(statement, source, &mut bindings),
342 }
343 }
344 replacement
345}
346
347pub fn resolve_import_bindings(
348 python: &dyn PythonSource,
349 file: &ProjectFile,
350) -> HashMap<String, CodeUnit> {
351 let imports = python.import_info_of(file);
352 let mut bindings = HashMap::default();
353 for resolved in resolve_imports_batched(python, file, &imports) {
354 for (binding, code_unit) in resolved {
355 bindings.insert(binding, code_unit);
356 }
357 }
358 bindings
359}
360
361pub fn resolve_imports_batched(
368 python: &dyn PythonSource,
369 file: &ProjectFile,
370 imports: &[ImportInfo],
371) -> Vec<Vec<(String, CodeUnit)>> {
372 let primary_fqns: Vec<Option<String>> = imports
373 .iter()
374 .map(|import| primary_module_fqn(file, import))
375 .collect();
376 let to_resolve: Vec<String> = primary_fqns.iter().flatten().cloned().collect();
377 let mut batch_results = resolve_module_code_units_batch(python, &to_resolve).into_iter();
378
379 imports
380 .iter()
381 .zip(primary_fqns.iter())
382 .map(|(import, primary_fqn)| {
383 let hint = primary_fqn.as_ref().map(|_| batch_results.next().unwrap());
384 resolve_import_with_hint(python, file, import, hint.as_ref())
385 })
386 .collect()
387}
388
389fn primary_module_fqn(file: &ProjectFile, import: &ImportInfo) -> Option<String> {
393 match python_import_details(import)? {
394 PythonImportDetails::Import { module, alias } => Some(python_namespace_binding_module(
395 import,
396 alias.as_deref(),
397 &module,
398 )),
399 PythonImportDetails::FromImport {
400 module,
401 name,
402 wildcard,
403 ..
404 } => {
405 if wildcard {
406 return None;
407 }
408 let resolved_module = if module.starts_with('.') {
409 resolve_python_relative_module(file, &module)
410 } else {
411 Some(module)
412 };
413 resolved_module.map(|resolved_module| format!("{resolved_module}.{name}"))
414 }
415 }
416}
417
418pub fn resolve_import(
419 python: &dyn PythonSource,
420 file: &ProjectFile,
421 import: &ImportInfo,
422) -> Vec<(String, CodeUnit)> {
423 resolve_import_with_hint(python, file, import, None)
424}
425
426fn resolve_import_with_hint(
429 python: &dyn PythonSource,
430 file: &ProjectFile,
431 import: &ImportInfo,
432 primary_hint: Option<&Option<CodeUnit>>,
433) -> Vec<(String, CodeUnit)> {
434 if let Some(details) = python_import_details(import) {
435 match details {
436 PythonImportDetails::Import { module, alias } => {
437 let binding = python_namespace_binding_name(import, alias.as_deref(), &module);
438 let bound_module =
439 python_namespace_binding_module(import, alias.as_deref(), &module);
440 let resolved = match primary_hint {
441 Some(hint) => hint.clone(),
442 None => resolve_module_code_unit(python, &bound_module),
443 };
444 if let Some(module_code_unit) = resolved {
445 return vec![(binding, module_code_unit)];
446 }
447 }
448 PythonImportDetails::FromImport {
449 module,
450 name,
451 alias,
452 wildcard,
453 } => {
454 let resolved_module = if module.starts_with('.') {
455 resolve_python_relative_module(file, &module)
456 } else {
457 Some(module)
458 };
459 let Some(resolved_module) = resolved_module else {
460 return Vec::new();
461 };
462 if wildcard {
463 return public_declarations_in_module(python, &resolved_module)
464 .into_iter()
465 .map(|code_unit| (code_unit.identifier().to_string(), code_unit))
466 .collect();
467 }
468
469 let binding = alias.clone().unwrap_or_else(|| name.clone());
470 let module_candidate = format!("{resolved_module}.{name}");
471 let resolved = match primary_hint {
472 Some(hint) => hint.clone(),
473 None => resolve_module_code_unit(python, &module_candidate),
474 };
475 if let Some(code_unit) = resolved {
476 return vec![(binding, code_unit)];
477 }
478 let exported = resolve_exported_name_from_module(python, &resolved_module, &name);
479 if !exported.is_empty() {
480 return exported
481 .into_iter()
482 .map(|code_unit| (binding.clone(), code_unit))
483 .collect();
484 }
485 let definitions: Vec<_> = python.definitions(&module_candidate).collect();
486 if !definitions.is_empty() {
487 return definitions
488 .into_iter()
489 .map(|code_unit| (binding.clone(), code_unit))
490 .collect();
491 }
492 let package_candidate: Vec<_> = python
493 .definitions(&format!("{resolved_module}.{name}"))
494 .collect();
495 if !package_candidate.is_empty() {
496 return package_candidate
497 .into_iter()
498 .map(|code_unit| (binding.clone(), code_unit))
499 .collect();
500 }
501 }
502 }
503 }
504 Vec::new()
505}
506
507pub fn resolve_exported_fqn(python: &dyn PythonSource, fqn: &str) -> Vec<CodeUnit> {
508 let Some((module, name)) = fqn.rsplit_once('.') else {
509 return Vec::new();
510 };
511 resolve_exported_name_from_module(python, module, name)
512}
513
514fn resolve_direct_named_exported_fqn(
519 python: &dyn PythonSource,
520 fqn: &str,
521) -> Option<Vec<CodeUnit>> {
522 let (module, name) = fqn.rsplit_once('.')?;
523 let mut results = Vec::new();
524 let mut queue = VecDeque::from([(module.to_string(), name.to_string())]);
525 let mut visited = HashSet::default();
526
527 while let Some((module, export_name)) = queue.pop_front() {
528 if !visited.insert((module.clone(), export_name.clone())) {
529 continue;
530 }
531 let module_unit = resolve_module_code_unit(python, &module)?;
532 let file = module_unit.source();
533 let local = local_export_declarations(python, file, &export_name);
534 let binder = python.import_binder_of(file);
535 let binding = binder.bindings.get(&export_name);
536 if !local.is_empty() && binding.is_some() {
537 return None;
538 }
539 if !local.is_empty() {
540 results.extend(local);
541 continue;
542 }
543 let binding = binding?;
544 if binding.kind != ImportKind::Named {
545 return None;
546 }
547 let imported_name = binding.imported_name.as_ref()?;
548 queue.push_back((binding.module_specifier.clone(), imported_name.clone()));
549 }
550
551 results.sort_by(|left, right| {
552 left.source()
553 .cmp(right.source())
554 .then_with(|| left.fq_name().cmp(&right.fq_name()))
555 });
556 results.dedup();
557 (!results.is_empty()).then_some(results)
558}
559
560pub fn resolve_fqn_candidates(
565 python: &dyn PythonSource,
566 fqn: &str,
567 exact: impl FnOnce(&str) -> Vec<CodeUnit>,
568) -> Vec<CodeUnit> {
569 if let Some(candidates) = resolve_direct_named_exported_fqn(python, fqn) {
570 return candidates;
571 }
572 let candidates = resolve_exported_fqn(python, fqn);
573 if !candidates.is_empty() {
574 return candidates;
575 }
576 exact(fqn)
577}
578
579fn resolve_exported_name_from_module(
580 python: &dyn PythonSource,
581 module: &str,
582 name: &str,
583) -> Vec<CodeUnit> {
584 let Some(module_unit) = resolve_module_code_unit(python, module) else {
585 return Vec::new();
586 };
587 resolve_exported_name(python, module_unit.source(), name)
588}
589
590fn resolve_exported_name(
591 python: &dyn PythonSource,
592 module_file: &ProjectFile,
593 name: &str,
594) -> Vec<CodeUnit> {
595 let mut results = Vec::new();
596 let mut queue = VecDeque::from([(module_file.clone(), name.to_string())]);
597 let mut visited = HashSet::default();
598
599 while let Some((file, export_name)) = queue.pop_front() {
600 if !visited.insert((file.clone(), export_name.clone())) {
601 continue;
602 }
603
604 let index = python.export_index_of(&file);
605 if let Some(entry) = index.exports_by_name.get(&export_name) {
606 match entry {
607 ExportEntry::Local { local_name } => {
608 results.extend(local_export_declarations(python, &file, local_name));
609 }
610 ExportEntry::ReexportedNamed {
611 module_specifier,
612 imported_name,
613 } => {
614 for target_file in
615 resolve_module_files_for_export(python, &file, module_specifier)
616 {
617 queue.push_back((target_file, imported_name.clone()));
618 }
619 }
620 ExportEntry::ReexportedModule { module_specifier } => {
621 results.extend(resolve_module_code_unit(python, module_specifier));
624 }
625 ExportEntry::Default { local_name } => {
626 if let Some(local_name) = local_name {
627 results.extend(local_export_declarations(python, &file, local_name));
628 }
629 }
630 }
631 continue;
632 }
633
634 if !export_name.starts_with('_') {
635 for star in &index.reexport_stars {
636 for target_file in
637 resolve_module_files_for_export(python, &file, &star.module_specifier)
638 {
639 queue.push_back((target_file, export_name.clone()));
640 }
641 }
642 }
643 }
644
645 results.sort_by(|left, right| {
646 left.source()
647 .cmp(right.source())
648 .then_with(|| left.fq_name().cmp(&right.fq_name()))
649 });
650 results.dedup();
651 results
652}
653
654fn local_export_declarations(
655 index: &dyn CodeUnitIndex,
656 file: &ProjectFile,
657 local_name: &str,
658) -> Vec<CodeUnit> {
659 index
660 .top_level_declarations(file)
661 .into_iter()
662 .filter(|unit| {
663 unit.identifier() == local_name
664 && index
665 .parent_of(unit)
666 .is_some_and(|parent| parent.is_module() && parent.source() == file)
667 })
668 .collect()
669}
670
671fn resolve_module_files_for_export(
672 python: &dyn PythonSource,
673 importing_file: &ProjectFile,
674 module_specifier: &str,
675) -> Vec<ProjectFile> {
676 let resolved_module = if module_specifier.starts_with('.') {
677 resolve_python_relative_module(importing_file, module_specifier)
678 } else {
679 Some(module_specifier.to_string())
680 };
681 let Some(resolved_module) = resolved_module else {
682 return Vec::new();
683 };
684 resolve_module_code_unit(python, &resolved_module)
688 .map(|unit| vec![unit.source().clone()])
689 .unwrap_or_default()
690}
691
692pub fn extract_package_from_python_wildcard(import: &ImportInfo) -> Option<String> {
693 let details = python_import_details(import)?;
694 match details {
695 PythonImportDetails::FromImport {
696 module, wildcard, ..
697 } if wildcard => Some(module),
698 _ => None,
699 }
700}
701
702#[derive(Debug, Clone)]
703pub enum PythonImportDetails {
704 Import {
705 module: String,
706 alias: Option<String>,
707 },
708 FromImport {
709 module: String,
710 name: String,
711 alias: Option<String>,
712 wildcard: bool,
713 },
714}
715
716pub fn python_import_infos_from_node(node: Node<'_>, source: &str) -> Vec<ImportInfo> {
717 match node.kind() {
718 "import_statement" => python_namespace_import_infos(node, source),
719 "import_from_statement" => python_from_import_infos(node, source),
720 _ => Vec::new(),
721 }
722}
723
724pub fn python_import_details(import: &ImportInfo) -> Option<PythonImportDetails> {
725 let path = import.path.as_ref()?;
726 match path.kind? {
727 StructuredImportPathKind::Namespace => Some(PythonImportDetails::Import {
728 module: join_python_import_segments(&path.segments),
729 alias: import.alias.clone(),
730 }),
731 StructuredImportPathKind::StaticMember => None,
733 StructuredImportPathKind::ImportFrom => {
734 let (name, module_segments) = if import.is_wildcard {
735 ("*".to_string(), path.segments.as_slice())
736 } else {
737 let (name, module_segments) = path.segments.split_last()?;
738 (name.clone(), module_segments)
739 };
740 Some(PythonImportDetails::FromImport {
741 module: join_python_import_segments(module_segments),
742 name,
743 alias: import.alias.clone(),
744 wildcard: import.is_wildcard,
745 })
746 }
747 }
748}
749
750fn python_namespace_import_infos(node: Node<'_>, source: &str) -> Vec<ImportInfo> {
751 let mut infos = Vec::new();
752 let mut cursor = node.walk();
753 for imported in node.children_by_field_name("name", &mut cursor) {
754 let (module_node, alias_node) = if imported.kind() == "aliased_import" {
755 let Some(name) = imported.child_by_field_name("name") else {
756 continue;
757 };
758 (name, imported.child_by_field_name("alias"))
759 } else {
760 (imported, None)
761 };
762 let alias = alias_node
763 .map(|alias| py_node_text(alias, source).trim().to_string())
764 .filter(|alias| !alias.is_empty());
765 let segments = python_path_segments(module_node, source);
766 if segments.is_empty() {
767 continue;
768 }
769 let binder_span = alias
772 .is_some()
773 .then_some(alias_node)
774 .flatten()
775 .or_else(|| python_first_segment_node(module_node))
776 .map(brokk_bifrost_core::analyzer::common::node_span);
777 let module = join_python_import_segments(&segments);
778 let identifier = alias.clone().or_else(|| segments.first().cloned());
779 infos.push(ImportInfo {
780 raw_snippet: if let Some(alias) = &alias {
781 format!("import {module} as {alias}")
782 } else {
783 format!("import {module}")
784 },
785 is_wildcard: false,
786 is_global: false,
787 identifier,
788 alias,
789 path: Some(StructuredImportPath {
790 segments,
791 kind: Some(StructuredImportPathKind::Namespace),
792 lexical_prefixes: Vec::new(),
793 lexical_scopes: Vec::new(),
794 declaration_start_byte: node.start_byte(),
795 }),
796 binder_span,
797 });
798 }
799 infos
800}
801
802fn python_from_import_infos(node: Node<'_>, source: &str) -> Vec<ImportInfo> {
803 let Some(module_node) = node.child_by_field_name("module_name") else {
804 return Vec::new();
805 };
806 let module_segments = python_module_segments(module_node, source);
807 if module_segments.is_empty() {
808 return Vec::new();
809 }
810
811 let mut infos = Vec::new();
812 let has_wildcard_import = {
813 let mut cursor = node.walk();
814 node.named_children(&mut cursor)
815 .any(|child| child.kind() == "wildcard_import")
816 };
817 let mut cursor = node.walk();
818 let imported_names: Vec<_> = node.children_by_field_name("name", &mut cursor).collect();
819 if has_wildcard_import {
820 let module = join_python_import_segments(&module_segments);
821 infos.push(ImportInfo {
822 raw_snippet: format!("from {module} import *"),
823 is_wildcard: true,
824 is_global: false,
825 identifier: None,
826 alias: None,
827 path: Some(StructuredImportPath {
828 segments: module_segments,
829 kind: Some(StructuredImportPathKind::ImportFrom),
830 lexical_prefixes: Vec::new(),
831 lexical_scopes: Vec::new(),
832 declaration_start_byte: node.start_byte(),
833 }),
834 binder_span: None,
835 });
836 return infos;
837 }
838 if imported_names.is_empty() {
839 return infos;
840 }
841
842 for imported in imported_names {
843 let (name_node, alias_node) = if imported.kind() == "aliased_import" {
844 let Some(name) = imported.child_by_field_name("name") else {
845 continue;
846 };
847 (name, imported.child_by_field_name("alias"))
848 } else {
849 (imported, None)
850 };
851 let alias = alias_node
852 .map(|alias| py_node_text(alias, source).trim().to_string())
853 .filter(|alias| !alias.is_empty());
854 let name_segments = python_path_segments(name_node, source);
855 if name_segments.is_empty() {
856 continue;
857 }
858 let binder_span = alias
861 .is_some()
862 .then_some(alias_node)
863 .flatten()
864 .or_else(|| {
865 (name_segments.len() == 1)
866 .then(|| python_first_segment_node(name_node))
867 .flatten()
868 })
869 .map(brokk_bifrost_core::analyzer::common::node_span);
870 let imported_name = join_python_import_segments(&name_segments);
871 let mut segments = module_segments.clone();
872 segments.extend(name_segments);
873 let module = join_python_import_segments(&module_segments);
874 infos.push(ImportInfo {
875 raw_snippet: if let Some(alias) = &alias {
876 format!("from {module} import {imported_name} as {alias}")
877 } else {
878 format!("from {module} import {imported_name}")
879 },
880 is_wildcard: false,
881 is_global: false,
882 identifier: Some(alias.clone().unwrap_or_else(|| imported_name.clone())),
883 alias,
884 path: Some(StructuredImportPath {
885 segments,
886 kind: Some(StructuredImportPathKind::ImportFrom),
887 lexical_prefixes: Vec::new(),
888 lexical_scopes: Vec::new(),
889 declaration_start_byte: node.start_byte(),
890 }),
891 binder_span,
892 });
893 }
894 infos
895}
896
897fn python_module_segments(module: Node<'_>, source: &str) -> Vec<String> {
898 if module.kind() == "relative_import" {
899 let mut cursor = module.walk();
900 let mut prefix = String::new();
901 let mut path_node = None;
902 for child in module.named_children(&mut cursor) {
903 match child.kind() {
904 "import_prefix" if prefix.is_empty() => {
905 prefix = py_node_text(child, source).trim().to_string();
906 }
907 "dotted_name" if path_node.is_none() => {
908 path_node = Some(child);
909 }
910 _ => {}
911 }
912 }
913 let mut segments = path_node
914 .map(|path| python_path_segments(path, source))
915 .unwrap_or_default();
916 if !prefix.is_empty() {
917 if let Some(first) = segments.first_mut() {
918 first.insert_str(0, &prefix);
919 } else {
920 segments.push(prefix);
921 }
922 }
923 return segments;
924 }
925 python_path_segments(module, source)
926}
927
928fn python_first_segment_node(node: Node<'_>) -> Option<Node<'_>> {
932 match node.kind() {
933 "identifier" => Some(node),
934 "dotted_name" => {
935 let mut cursor = node.walk();
936 node.named_children(&mut cursor)
937 .find(|child| child.kind() == "identifier")
938 }
939 _ => None,
940 }
941}
942
943fn python_path_segments(node: Node<'_>, source: &str) -> Vec<String> {
944 match node.kind() {
945 "identifier" => vec![py_node_text(node, source).trim().to_string()],
946 "dotted_name" => {
947 let mut segments = Vec::new();
948 let mut cursor = node.walk();
949 for child in node.named_children(&mut cursor) {
950 segments.extend(python_path_segments(child, source));
951 }
952 segments
953 }
954 _ => {
955 let mut segments = Vec::new();
956 let mut cursor = node.walk();
957 for child in node.named_children(&mut cursor) {
958 segments.extend(python_path_segments(child, source));
959 }
960 segments
961 }
962 }
963}
964
965fn join_python_import_segments(segments: &[String]) -> String {
966 let Some((first, rest)) = segments.split_first() else {
967 return String::new();
968 };
969 if first.starts_with('.') && !rest.is_empty() {
970 format!("{first}.{}", rest.join("."))
971 } else {
972 segments.join(".")
973 }
974}
975
976pub fn python_namespace_binding_name(
977 import: &ImportInfo,
978 alias: Option<&str>,
979 module: &str,
980) -> String {
981 import
982 .identifier
983 .clone()
984 .or_else(|| alias.map(str::to_string))
985 .unwrap_or_else(|| module.to_string())
986}
987
988pub fn python_namespace_binding_module(
989 import: &ImportInfo,
990 alias: Option<&str>,
991 module: &str,
992) -> String {
993 if alias.is_some() {
994 return module.to_string();
995 }
996 import
997 .path
998 .as_ref()
999 .and_then(|path| path.segments.first().cloned())
1000 .unwrap_or_else(|| module.to_string())
1001}
1002
1003pub fn resolve_python_relative_module(
1004 source_file: &ProjectFile,
1005 module_expr: &str,
1006) -> Option<String> {
1007 resolve_python_relative_module_from_package(&python_current_package(source_file), module_expr)
1008}
1009
1010pub fn resolve_python_relative_module_from_package(
1014 current_package: &str,
1015 module_expr: &str,
1016) -> Option<String> {
1017 let level = module_expr.chars().take_while(|ch| *ch == '.').count();
1018 let suffix = module_expr[level..].trim_matches('.');
1019 let mut parts: Vec<_> = current_package
1020 .split('.')
1021 .filter(|part| !part.is_empty())
1022 .map(str::to_string)
1023 .collect();
1024 if level == 0 {
1025 return Some(module_expr.to_string());
1026 }
1027 if level > 0 {
1028 if level - 1 > parts.len() {
1029 return None;
1030 }
1031 parts.truncate(parts.len() - (level - 1));
1032 }
1033 if !suffix.is_empty() {
1034 parts.extend(suffix.split('.').map(str::to_string));
1035 }
1036 Some(parts.join("."))
1037}
1038
1039fn python_current_package(source_file: &ProjectFile) -> String {
1040 let module = python_module_name(source_file);
1041 if source_file
1042 .rel_path()
1043 .file_name()
1044 .and_then(|name| name.to_str())
1045 == Some("__init__.py")
1046 {
1047 module
1048 } else {
1049 module
1050 .rsplit_once('.')
1051 .map(|(package, _)| package.to_string())
1052 .unwrap_or_default()
1053 }
1054}