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