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 continue;
432 }
433 let local_name = import
438 .local_name()
439 .map(str::to_string)
440 .unwrap_or_else(|| name.clone());
441 let module_candidate = format!("{resolved_module}.{name}");
442 if resolve_module_code_unit(python, &module_candidate).is_some() {
443 bindings.push((
444 local_name,
445 ImportBinding {
446 module_specifier: module_candidate,
447 namespace_imported_module: None,
448 kind: ImportKind::Namespace,
449 imported_name: None,
450 },
451 ));
452 continue;
453 }
454 bindings.push((
455 local_name,
456 ImportBinding {
457 module_specifier: resolved_module,
458 namespace_imported_module: None,
459 kind: ImportKind::Named,
460 imported_name: Some(name),
461 },
462 ));
463 }
464 }
465 }
466
467 bindings
468}
469
470pub fn public_declarations_in_module(python: &dyn PythonSource, module_fq: &str) -> Vec<CodeUnit> {
471 let Some(module_code_unit) = resolve_module_code_unit(python, module_fq) else {
472 return Vec::new();
473 };
474 python
475 .direct_children(&module_code_unit)
476 .into_iter()
477 .filter(|code_unit| !code_unit.identifier().starts_with('_'))
478 .collect()
479}
480
481pub fn resolve_base_class(
482 python: &dyn PythonSource,
483 code_unit: &CodeUnit,
484 raw: &str,
485) -> Option<CodeUnit> {
486 let trimmed = raw.trim();
487 if trimmed.is_empty() {
488 return None;
489 }
490
491 let binder = python.import_binder_of(code_unit.source());
492 if let Some((head, tail)) = trimmed.split_once('.') {
493 if let Some(binding) = binder.bindings.get(head)
494 && binding.kind == ImportKind::Namespace
495 {
496 let fq_name = format!("{}.{}", binding.module_specifier, tail);
497 return python.definitions(&fq_name).next();
498 }
499 return python.definitions(trimmed).next();
500 }
501
502 if let Some(binding) = binder.bindings.get(trimmed) {
503 match binding.kind {
504 ImportKind::Namespace => {
505 return resolve_module_code_unit(python, &binding.module_specifier);
506 }
507 ImportKind::Named => {
508 let imported_name = binding.imported_name.as_ref()?;
509 let fqn = format!("{}.{}", binding.module_specifier, imported_name);
510 return resolve_exported_fqn(python, &fqn)
511 .into_iter()
512 .next()
513 .or_else(|| python.definitions(&fqn).next());
514 }
515 _ => {}
516 }
517 }
518
519 if python
520 .import_info_of(code_unit.source())
521 .iter()
522 .any(|import| import.is_wildcard)
523 && let Some(imported) = resolve_import_bindings(python, code_unit.source()).get(trimmed)
524 {
525 return Some(imported.clone());
526 }
527
528 let local_fq_name = format!("{}.{}", code_unit.package_name(), trimmed);
529 python
530 .definitions(&local_fq_name)
531 .next()
532 .or_else(|| python.definitions(trimmed).next())
533}
534
535pub fn render_skeleton_recursive(
536 index: &dyn CodeUnitIndex,
537 code_unit: &CodeUnit,
538 indent: &str,
539 header_only: bool,
540 out: &mut String,
541) {
542 if let Some(signature) = python_signature(index, code_unit, header_only) {
543 for line in signature.lines() {
544 out.push_str(indent);
545 out.push_str(line);
546 out.push('\n');
547 }
548 }
549
550 let all_children = index.direct_children(code_unit);
551 let field_children: Vec<_> = all_children
552 .iter()
553 .filter(|child| child.is_field())
554 .cloned()
555 .collect();
556 let children = if header_only {
557 field_children.clone()
558 } else {
559 all_children.clone()
560 };
561 if !children.is_empty() || code_unit.is_class() || code_unit.is_module() {
562 let child_indent = format!("{indent} ");
563 for child in children {
564 render_skeleton_recursive(index, &child, &child_indent, header_only, out);
565 }
566 if header_only && all_children.len() > field_children.len() {
567 out.push_str(&child_indent);
568 out.push_str("[...]\n");
569 }
570 }
571}
572
573fn python_signature(
574 index: &dyn CodeUnitIndex,
575 code_unit: &CodeUnit,
576 _header_only: bool,
577) -> Option<String> {
578 if code_unit.is_module() {
579 return None;
580 }
581
582 let source = index.get_source(code_unit, false)?;
583 let lines: Vec<_> = source
584 .lines()
585 .map(str::trim_end)
586 .filter(|line| !line.trim().is_empty())
587 .collect();
588 if lines.is_empty() {
589 return None;
590 }
591
592 let mut decorators = Vec::new();
593 let mut header = None;
594 for line in lines {
595 let trimmed = line.trim_start();
596 if trimmed.starts_with('@') {
597 decorators.push(trimmed.to_string());
598 continue;
599 }
600 header = Some(trimmed.to_string());
601 break;
602 }
603 let mut rendered = String::new();
604 for decorator in decorators {
605 rendered.push_str(&decorator);
606 rendered.push('\n');
607 }
608
609 let header = header?;
610 match code_unit.kind() {
611 CodeUnitType::Class => rendered.push_str(&header),
612 CodeUnitType::Function => {
613 rendered.push_str(header.trim_end_matches(':'));
614 rendered.push_str(": ...");
615 }
616 CodeUnitType::Field | CodeUnitType::Macro => rendered.push_str(header.as_str()),
617 CodeUnitType::Module | CodeUnitType::FileScope => return None,
618 }
619 Some(rendered)
620}
621
622fn import_order_requires_source(binder: &ImportBinder, local_names: &HashSet<String>) -> bool {
623 binder
624 .bindings
625 .keys()
626 .any(|bound_name| local_names.contains(bound_name))
627}