1use brokk_bifrost_core::analyzer::capabilities::ImportAnalysisProvider;
13use brokk_bifrost_core::analyzer::model::{CodeUnitType, ImportInfo};
14use brokk_bifrost_core::analyzer::prepared_syntax::{IndexedFileFacts, PreparedSyntaxTree};
15use brokk_bifrost_core::analyzer::usages::model::{
16 ExportEntry, ExportIndex, ImportBinder, ImportBinding, ImportKind, ReexportStar,
17};
18use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, ProjectFile};
19use brokk_bifrost_core::hash::HashSet;
20use std::collections::BTreeSet;
21use std::sync::Arc;
22
23use crate::declarations::{collect_python_identifiers, parse_python_tree};
24use crate::imports::{
25 PythonImportDetails, python_import_details, python_import_infos_from_node,
26 python_namespace_binding_module, python_namespace_binding_name, resolve_exported_fqn,
27 resolve_import_bindings, resolve_python_relative_module,
28};
29use crate::usage_index::PythonUsageIndex;
30
31pub trait PythonSource: CodeUnitIndex + ImportAnalysisProvider {
42 fn path_module_fqn(&self, module_fq: &str) -> Option<Vec<CodeUnit>>;
45
46 fn path_module_fqns_batch(&self, module_fqs: &[String]) -> Vec<Option<Vec<CodeUnit>>>;
49
50 fn definition_fqn(&self, fqn: &str) -> Vec<CodeUnit>;
51
52 fn import_binder_of(&self, file: &ProjectFile) -> Arc<ImportBinder>;
57
58 fn export_index_of(&self, file: &ProjectFile) -> Arc<ExportIndex>;
59
60 fn prepared_syntax(&self, file: &ProjectFile) -> Option<Arc<PreparedSyntaxTree>>;
70
71 fn visit_file_facts(
74 &self,
75 files: &[ProjectFile],
76 visit: &mut dyn FnMut(&ProjectFile, Option<&dyn IndexedFileFacts>),
77 );
78}
79
80pub trait PythonUsageSource: PythonSource {
83 fn usage_index(&self) -> Arc<PythonUsageIndex>;
84}
85
86pub fn extract_type_identifiers(source: &str) -> BTreeSet<String> {
87 let Some(tree) = parse_python_tree(source) else {
88 return BTreeSet::new();
89 };
90 let mut identifiers = HashSet::default();
91 collect_python_identifiers(tree.root_node(), source, &mut identifiers);
92 identifiers.into_iter().collect()
93}
94
95pub fn resolve_module_code_unit(python: &dyn PythonSource, module_fq: &str) -> Option<CodeUnit> {
96 if let Some(units) = python.path_module_fqn(module_fq) {
97 return units.into_iter().find(|code_unit| code_unit.is_module());
98 }
99 python
100 .definition_fqn(module_fq)
101 .into_iter()
102 .find(CodeUnit::is_module)
103}
104
105pub fn resolve_module_code_units_batch(
111 python: &dyn PythonSource,
112 module_fqs: &[String],
113) -> Vec<Option<CodeUnit>> {
114 let path_results = python.path_module_fqns_batch(module_fqs);
115 let mut results: Vec<Option<CodeUnit>> = vec![None; module_fqs.len()];
116 let mut needs_definition_fallback = Vec::new();
117 for (i, units) in path_results.into_iter().enumerate() {
118 match units {
119 Some(units) => results[i] = units.into_iter().find(CodeUnit::is_module),
120 None => needs_definition_fallback.push(i),
121 }
122 }
123 for i in needs_definition_fallback {
124 results[i] = python
125 .definition_fqn(&module_fqs[i])
126 .into_iter()
127 .find(CodeUnit::is_module);
128 }
129 results
130}
131
132pub fn compute_export_index_of(python: &dyn PythonSource, file: &ProjectFile) -> ExportIndex {
133 let mut index = ExportIndex::empty();
134 let mut events = Vec::new();
135 let declarations = python.top_level_declarations(file);
136 collect_local_export_events(
137 declarations.iter(),
138 |code_unit| {
139 python
140 .ranges(code_unit)
141 .iter()
142 .map(|range| range.start_byte)
143 .min()
144 .unwrap_or(usize::MAX)
145 },
146 &mut events,
147 );
148
149 if let Ok(source) = file.read_to_string()
150 && let Some(tree) = parse_python_tree(&source)
151 {
152 collect_reexport_events(
153 python,
154 file,
155 tree.root_node(),
156 &source,
157 &mut events,
158 &mut index,
159 );
160 } else {
161 let imports = python.import_info_of(file);
162 collect_reexport_events_from_imports(python, file, &imports, &mut events, &mut index);
163 }
164
165 finish_export_index(events, index)
166}
167
168pub fn export_index_from_file_facts(
169 python: &dyn PythonSource,
170 file: &ProjectFile,
171 facts: &dyn IndexedFileFacts,
172 module_name: &str,
173 binder: &ImportBinder,
174) -> ExportIndex {
175 let mut index = ExportIndex::empty();
176 let mut events = Vec::new();
177 let mut local_names = collect_local_export_events(
178 facts.top_level_declarations().iter(),
179 |code_unit| {
180 facts
181 .declaration_ranges(code_unit)
182 .into_iter()
183 .flatten()
184 .map(|range| range.start_byte)
185 .min()
186 .unwrap_or(usize::MAX)
187 },
188 &mut events,
189 );
190
191 if !facts
192 .top_level_declarations()
193 .iter()
194 .any(CodeUnit::is_module)
195 && let Some(identifier) = module_name.rsplit('.').next()
196 && !identifier.is_empty()
197 && !identifier.starts_with('_')
198 {
199 local_names.insert(identifier.to_string());
200 events.push((
201 0,
202 identifier.to_string(),
203 ExportEntry::Local {
204 local_name: identifier.to_string(),
205 },
206 ));
207 }
208
209 if import_order_requires_source(binder, &local_names)
210 && let Ok(source) = file.read_to_string()
211 && let Some(tree) = parse_python_tree(&source)
212 {
213 collect_reexport_events(
214 python,
215 file,
216 tree.root_node(),
217 &source,
218 &mut events,
219 &mut index,
220 );
221 } else {
222 collect_reexport_events_from_imports(
223 python,
224 file,
225 facts.imports(),
226 &mut events,
227 &mut index,
228 );
229 }
230
231 finish_export_index(events, index)
232}
233
234fn collect_local_export_events<'a>(
235 declarations: impl IntoIterator<Item = &'a CodeUnit>,
236 mut start_byte: impl FnMut(&CodeUnit) -> usize,
237 events: &mut Vec<(usize, String, ExportEntry)>,
238) -> HashSet<String> {
239 let mut local_names = HashSet::default();
240 for code_unit in declarations {
241 let identifier = code_unit.identifier().trim();
242 if identifier.is_empty() {
243 continue;
244 }
245 local_names.insert(identifier.to_string());
246 events.push((
247 start_byte(code_unit),
248 identifier.to_string(),
249 ExportEntry::Local {
250 local_name: identifier.to_string(),
251 },
252 ));
253 }
254 local_names
255}
256
257fn finish_export_index(
258 mut events: Vec<(usize, String, ExportEntry)>,
259 mut index: ExportIndex,
260) -> ExportIndex {
261 events.sort_by_key(|(start_byte, _, _)| *start_byte);
262 for (_, exported_name, entry) in events {
263 index.exports_by_name.insert(exported_name, entry);
264 }
265 index
266}
267
268fn collect_reexport_events(
269 python: &dyn PythonSource,
270 file: &ProjectFile,
271 root: tree_sitter::Node<'_>,
272 source: &str,
273 events: &mut Vec<(usize, String, ExportEntry)>,
274 index: &mut ExportIndex,
275) {
276 let mut stack = vec![root];
281 while let Some(node) = stack.pop() {
282 let mut cursor = node.walk();
283 for child in node.named_children(&mut cursor) {
284 match child.kind() {
285 "import_from_statement" => {
286 for info in python_import_infos_from_node(child, source) {
287 record_single_reexport_event(python, file, &info, events, index);
288 }
289 }
290 "function_definition" | "class_definition" => {}
291 _ => stack.push(child),
292 }
293 }
294 }
295}
296
297fn collect_reexport_events_from_imports(
298 python: &dyn PythonSource,
299 file: &ProjectFile,
300 imports: &[ImportInfo],
301 events: &mut Vec<(usize, String, ExportEntry)>,
302 index: &mut ExportIndex,
303) {
304 for import in imports {
305 record_single_reexport_event(python, file, import, events, index);
306 }
307}
308
309fn record_single_reexport_event(
310 python: &dyn PythonSource,
311 file: &ProjectFile,
312 import: &ImportInfo,
313 events: &mut Vec<(usize, String, ExportEntry)>,
314 index: &mut ExportIndex,
315) {
316 let Some(PythonImportDetails::FromImport {
317 module,
318 name,
319 alias,
320 wildcard,
321 }) = python_import_details(import)
322 else {
323 return;
324 };
325 let start_byte = import
326 .path
327 .as_ref()
328 .map(|path| path.declaration_start_byte)
329 .unwrap_or(usize::MAX);
330 let resolved_module = if module.starts_with('.') {
331 resolve_python_relative_module(file, &module)
332 } else {
333 Some(module.clone())
334 };
335 let Some(resolved_module) = resolved_module else {
336 return;
337 };
338
339 if wildcard {
340 index.reexport_stars.push(ReexportStar {
341 module_specifier: resolved_module,
342 });
343 return;
344 }
345 let exported_name = alias.unwrap_or(name.clone());
346 let module_candidate = format!("{resolved_module}.{name}");
352 if resolve_module_code_unit(python, &module_candidate).is_some() {
353 events.push((
354 start_byte,
355 exported_name,
356 ExportEntry::ReexportedModule {
357 module_specifier: module_candidate,
358 },
359 ));
360 return;
361 }
362 events.push((
363 start_byte,
364 exported_name,
365 ExportEntry::ReexportedNamed {
366 module_specifier: resolved_module,
367 imported_name: name,
368 },
369 ));
370}
371
372pub fn import_binder_from_imports(
373 python: &dyn PythonSource,
374 file: &ProjectFile,
375 imports: &[ImportInfo],
376) -> ImportBinder {
377 let mut binder = ImportBinder::empty();
378
379 for (local_name, binding) in import_bindings_from_imports(python, file, imports) {
380 binder.bindings.insert(local_name, binding);
381 }
382
383 binder
384}
385
386pub fn import_bindings_from_imports(
391 python: &dyn PythonSource,
392 file: &ProjectFile,
393 imports: &[ImportInfo],
394) -> Vec<(String, ImportBinding)> {
395 let mut bindings = Vec::new();
396
397 for import in imports {
398 let Some(details) = python_import_details(import) else {
399 continue;
400 };
401 match details {
402 PythonImportDetails::Import { module, alias } => {
403 let local_name = python_namespace_binding_name(import, alias.as_deref(), &module);
404 let module_specifier =
405 python_namespace_binding_module(import, alias.as_deref(), &module);
406 bindings.push((
407 local_name,
408 ImportBinding {
409 module_specifier,
410 namespace_imported_module: Some(module),
411 kind: ImportKind::Namespace,
412 imported_name: None,
413 },
414 ));
415 }
416 PythonImportDetails::FromImport {
417 module,
418 name,
419 wildcard,
420 ..
421 } => {
422 let resolved_module = if module.starts_with('.') {
423 resolve_python_relative_module(file, &module)
424 } else {
425 Some(module.clone())
426 };
427 let Some(resolved_module) = resolved_module else {
428 continue;
429 };
430 if wildcard {
431 bindings.extend(
436 public_declarations_in_module(python, &resolved_module)
437 .into_iter()
438 .map(|declaration| {
439 let name = declaration.identifier().to_string();
440 (
441 name.clone(),
442 ImportBinding {
443 module_specifier: resolved_module.clone(),
444 namespace_imported_module: None,
445 kind: ImportKind::Named,
446 imported_name: Some(name),
447 },
448 )
449 }),
450 );
451 continue;
452 }
453 let local_name = import
458 .local_name()
459 .map(str::to_string)
460 .unwrap_or_else(|| name.clone());
461 let module_candidate = format!("{resolved_module}.{name}");
462 if resolve_module_code_unit(python, &module_candidate).is_some() {
463 bindings.push((
464 local_name,
465 ImportBinding {
466 module_specifier: module_candidate,
467 namespace_imported_module: None,
468 kind: ImportKind::Namespace,
469 imported_name: None,
470 },
471 ));
472 continue;
473 }
474 bindings.push((
475 local_name,
476 ImportBinding {
477 module_specifier: resolved_module,
478 namespace_imported_module: None,
479 kind: ImportKind::Named,
480 imported_name: Some(name),
481 },
482 ));
483 }
484 }
485 }
486
487 bindings
488}
489
490pub fn public_declarations_in_module(python: &dyn PythonSource, module_fq: &str) -> Vec<CodeUnit> {
491 let Some(module_code_unit) = resolve_module_code_unit(python, module_fq) else {
492 return Vec::new();
493 };
494 python
495 .direct_children(&module_code_unit)
496 .into_iter()
497 .filter(|code_unit| !code_unit.identifier().starts_with('_'))
498 .collect()
499}
500
501pub fn resolve_base_class(
502 python: &dyn PythonSource,
503 code_unit: &CodeUnit,
504 raw: &str,
505) -> Option<CodeUnit> {
506 let trimmed = raw.trim();
507 if trimmed.is_empty() {
508 return None;
509 }
510
511 let binder = python.import_binder_of(code_unit.source());
512 if let Some((head, tail)) = trimmed.split_once('.') {
513 if let Some(binding) = binder.bindings.get(head)
514 && binding.kind == ImportKind::Namespace
515 {
516 let fq_name = format!("{}.{}", binding.module_specifier, tail);
517 return python.definitions(&fq_name).next();
518 }
519 return python.definitions(trimmed).next();
520 }
521
522 if let Some(binding) = binder.bindings.get(trimmed) {
523 match binding.kind {
524 ImportKind::Namespace => {
525 return resolve_module_code_unit(python, &binding.module_specifier);
526 }
527 ImportKind::Named => {
528 let imported_name = binding.imported_name.as_ref()?;
529 let fqn = format!("{}.{}", binding.module_specifier, imported_name);
530 return resolve_exported_fqn(python, &fqn)
531 .into_iter()
532 .next()
533 .or_else(|| python.definitions(&fqn).next());
534 }
535 _ => {}
536 }
537 }
538
539 if python
540 .import_info_of(code_unit.source())
541 .iter()
542 .any(|import| import.is_wildcard)
543 && let Some(imported) = resolve_import_bindings(python, code_unit.source()).get(trimmed)
544 {
545 return Some(imported.clone());
546 }
547
548 let local_fq_name = format!("{}.{}", code_unit.package_name(), trimmed);
549 python
550 .definitions(&local_fq_name)
551 .next()
552 .or_else(|| python.definitions(trimmed).next())
553}
554
555pub fn render_skeleton_recursive(
556 index: &dyn CodeUnitIndex,
557 code_unit: &CodeUnit,
558 indent: &str,
559 header_only: bool,
560 out: &mut String,
561) {
562 if let Some(signature) = python_signature(index, code_unit, header_only) {
563 for line in signature.lines() {
564 out.push_str(indent);
565 out.push_str(line);
566 out.push('\n');
567 }
568 }
569
570 let all_children = index.direct_children(code_unit);
571 let field_children: Vec<_> = all_children
572 .iter()
573 .filter(|child| child.is_field())
574 .cloned()
575 .collect();
576 let children = if header_only {
577 field_children.clone()
578 } else {
579 all_children.clone()
580 };
581 if !children.is_empty() || code_unit.is_class() || code_unit.is_module() {
582 let child_indent = format!("{indent} ");
583 for child in children {
584 render_skeleton_recursive(index, &child, &child_indent, header_only, out);
585 }
586 if header_only && all_children.len() > field_children.len() {
587 out.push_str(&child_indent);
588 out.push_str("[...]\n");
589 }
590 }
591}
592
593fn python_signature(
594 index: &dyn CodeUnitIndex,
595 code_unit: &CodeUnit,
596 _header_only: bool,
597) -> Option<String> {
598 if code_unit.is_module() {
599 return None;
600 }
601
602 let source = index.get_source(code_unit, false)?;
603 let lines: Vec<_> = source
604 .lines()
605 .map(str::trim_end)
606 .filter(|line| !line.trim().is_empty())
607 .collect();
608 if lines.is_empty() {
609 return None;
610 }
611
612 let mut decorators = Vec::new();
613 let mut header = None;
614 for line in lines {
615 let trimmed = line.trim_start();
616 if trimmed.starts_with('@') {
617 decorators.push(trimmed.to_string());
618 continue;
619 }
620 header = Some(trimmed.to_string());
621 break;
622 }
623 let mut rendered = String::new();
624 for decorator in decorators {
625 rendered.push_str(&decorator);
626 rendered.push('\n');
627 }
628
629 let header = header?;
630 match code_unit.kind() {
631 CodeUnitType::Class => rendered.push_str(&header),
632 CodeUnitType::Function => {
633 rendered.push_str(header.trim_end_matches(':'));
634 rendered.push_str(": ...");
635 }
636 CodeUnitType::Field | CodeUnitType::Macro => rendered.push_str(header.as_str()),
637 CodeUnitType::Module | CodeUnitType::FileScope => return None,
638 }
639 Some(rendered)
640}
641
642fn import_order_requires_source(binder: &ImportBinder, local_names: &HashSet<String>) -> bool {
643 binder
644 .bindings
645 .keys()
646 .any(|bound_name| local_names.contains(bound_name))
647}