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