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 cursor = root.walk();
277 for node in root.named_children(&mut cursor) {
278 if node.kind() != "import_from_statement" {
279 continue;
280 }
281 for info in python_import_infos_from_node(node, source) {
282 record_single_reexport_event(python, file, &info, events, index);
283 }
284 }
285}
286
287fn collect_reexport_events_from_imports(
288 python: &dyn PythonSource,
289 file: &ProjectFile,
290 imports: &[ImportInfo],
291 events: &mut Vec<(usize, String, ExportEntry)>,
292 index: &mut ExportIndex,
293) {
294 for import in imports {
295 record_single_reexport_event(python, file, import, events, index);
296 }
297}
298
299fn record_single_reexport_event(
300 python: &dyn PythonSource,
301 file: &ProjectFile,
302 import: &ImportInfo,
303 events: &mut Vec<(usize, String, ExportEntry)>,
304 index: &mut ExportIndex,
305) {
306 let Some(PythonImportDetails::FromImport {
307 module,
308 name,
309 alias,
310 wildcard,
311 }) = python_import_details(import)
312 else {
313 return;
314 };
315 let start_byte = import
316 .path
317 .as_ref()
318 .map(|path| path.declaration_start_byte)
319 .unwrap_or(usize::MAX);
320 let resolved_module = if module.starts_with('.') {
321 resolve_python_relative_module(file, &module)
322 } else {
323 Some(module.clone())
324 };
325 let Some(resolved_module) = resolved_module else {
326 return;
327 };
328
329 if wildcard {
330 index.reexport_stars.push(ReexportStar {
331 module_specifier: resolved_module,
332 });
333 return;
334 }
335 let exported_name = alias.unwrap_or(name.clone());
336 let imported_name = format!("{resolved_module}.{name}");
337 if resolve_module_code_unit(python, &imported_name).is_some() {
338 events.push((
339 start_byte,
340 exported_name,
341 ExportEntry::ReexportedNamed {
342 module_specifier: imported_name,
343 imported_name: name,
344 },
345 ));
346 return;
347 }
348 events.push((
349 start_byte,
350 exported_name,
351 ExportEntry::ReexportedNamed {
352 module_specifier: resolved_module,
353 imported_name: name,
354 },
355 ));
356}
357
358pub fn import_binder_from_imports(
359 python: &dyn PythonSource,
360 file: &ProjectFile,
361 imports: &[ImportInfo],
362) -> ImportBinder {
363 let mut binder = ImportBinder::empty();
364
365 for import in imports {
366 let Some(details) = python_import_details(import) else {
367 continue;
368 };
369 match details {
370 PythonImportDetails::Import { module, alias } => {
371 let local_name = python_namespace_binding_name(import, alias.as_deref(), &module);
372 let module_specifier =
373 python_namespace_binding_module(import, alias.as_deref(), &module);
374 binder.bindings.insert(
375 local_name,
376 ImportBinding {
377 module_specifier,
378 namespace_imported_module: Some(module),
379 kind: ImportKind::Namespace,
380 imported_name: None,
381 },
382 );
383 }
384 PythonImportDetails::FromImport {
385 module,
386 name,
387 wildcard,
388 ..
389 } => {
390 let resolved_module = if module.starts_with('.') {
391 resolve_python_relative_module(file, &module)
392 } else {
393 Some(module.clone())
394 };
395 let Some(resolved_module) = resolved_module else {
396 continue;
397 };
398 if wildcard {
399 continue;
400 }
401 let local_name = import
406 .local_name()
407 .map(str::to_string)
408 .unwrap_or_else(|| name.clone());
409 let module_candidate = format!("{resolved_module}.{name}");
410 if resolve_module_code_unit(python, &module_candidate).is_some() {
411 binder.bindings.insert(
412 local_name,
413 ImportBinding {
414 module_specifier: module_candidate,
415 namespace_imported_module: None,
416 kind: ImportKind::Namespace,
417 imported_name: None,
418 },
419 );
420 continue;
421 }
422 binder.bindings.insert(
423 local_name,
424 ImportBinding {
425 module_specifier: resolved_module,
426 namespace_imported_module: None,
427 kind: ImportKind::Named,
428 imported_name: Some(name),
429 },
430 );
431 }
432 }
433 }
434
435 binder
436}
437
438pub fn public_declarations_in_module(python: &dyn PythonSource, module_fq: &str) -> Vec<CodeUnit> {
439 let Some(module_code_unit) = resolve_module_code_unit(python, module_fq) else {
440 return Vec::new();
441 };
442 python
443 .direct_children(&module_code_unit)
444 .into_iter()
445 .filter(|code_unit| !code_unit.identifier().starts_with('_'))
446 .collect()
447}
448
449pub fn resolve_base_class(
450 python: &dyn PythonSource,
451 code_unit: &CodeUnit,
452 raw: &str,
453) -> Option<CodeUnit> {
454 let trimmed = raw.trim();
455 if trimmed.is_empty() {
456 return None;
457 }
458
459 let binder = python.import_binder_of(code_unit.source());
460 if let Some((head, tail)) = trimmed.split_once('.') {
461 if let Some(binding) = binder.bindings.get(head)
462 && binding.kind == ImportKind::Namespace
463 {
464 let fq_name = format!("{}.{}", binding.module_specifier, tail);
465 return python.definitions(&fq_name).next();
466 }
467 return python.definitions(trimmed).next();
468 }
469
470 if let Some(binding) = binder.bindings.get(trimmed) {
471 match binding.kind {
472 ImportKind::Namespace => {
473 return resolve_module_code_unit(python, &binding.module_specifier);
474 }
475 ImportKind::Named => {
476 let imported_name = binding.imported_name.as_ref()?;
477 let fqn = format!("{}.{}", binding.module_specifier, imported_name);
478 return resolve_exported_fqn(python, &fqn)
479 .into_iter()
480 .next()
481 .or_else(|| python.definitions(&fqn).next());
482 }
483 _ => {}
484 }
485 }
486
487 if python
488 .import_info_of(code_unit.source())
489 .iter()
490 .any(|import| import.is_wildcard)
491 && let Some(imported) = resolve_import_bindings(python, code_unit.source()).get(trimmed)
492 {
493 return Some(imported.clone());
494 }
495
496 let local_fq_name = format!("{}.{}", code_unit.package_name(), trimmed);
497 python
498 .definitions(&local_fq_name)
499 .next()
500 .or_else(|| python.definitions(trimmed).next())
501}
502
503pub fn render_skeleton_recursive(
504 index: &dyn CodeUnitIndex,
505 code_unit: &CodeUnit,
506 indent: &str,
507 header_only: bool,
508 out: &mut String,
509) {
510 if let Some(signature) = python_signature(index, code_unit, header_only) {
511 for line in signature.lines() {
512 out.push_str(indent);
513 out.push_str(line);
514 out.push('\n');
515 }
516 }
517
518 let all_children = index.direct_children(code_unit);
519 let field_children: Vec<_> = all_children
520 .iter()
521 .filter(|child| child.is_field())
522 .cloned()
523 .collect();
524 let children = if header_only {
525 field_children.clone()
526 } else {
527 all_children.clone()
528 };
529 if !children.is_empty() || code_unit.is_class() || code_unit.is_module() {
530 let child_indent = format!("{indent} ");
531 for child in children {
532 render_skeleton_recursive(index, &child, &child_indent, header_only, out);
533 }
534 if header_only && all_children.len() > field_children.len() {
535 out.push_str(&child_indent);
536 out.push_str("[...]\n");
537 }
538 }
539}
540
541fn python_signature(
542 index: &dyn CodeUnitIndex,
543 code_unit: &CodeUnit,
544 _header_only: bool,
545) -> Option<String> {
546 if code_unit.is_module() {
547 return None;
548 }
549
550 let source = index.get_source(code_unit, false)?;
551 let lines: Vec<_> = source
552 .lines()
553 .map(str::trim_end)
554 .filter(|line| !line.trim().is_empty())
555 .collect();
556 if lines.is_empty() {
557 return None;
558 }
559
560 let mut decorators = Vec::new();
561 let mut header = None;
562 for line in lines {
563 let trimmed = line.trim_start();
564 if trimmed.starts_with('@') {
565 decorators.push(trimmed.to_string());
566 continue;
567 }
568 header = Some(trimmed.to_string());
569 break;
570 }
571 let mut rendered = String::new();
572 for decorator in decorators {
573 rendered.push_str(&decorator);
574 rendered.push('\n');
575 }
576
577 let header = header?;
578 match code_unit.kind() {
579 CodeUnitType::Class => rendered.push_str(&header),
580 CodeUnitType::Function => {
581 rendered.push_str(header.trim_end_matches(':'));
582 rendered.push_str(": ...");
583 }
584 CodeUnitType::Field | CodeUnitType::Macro => rendered.push_str(header.as_str()),
585 CodeUnitType::Module | CodeUnitType::FileScope => return None,
586 }
587 Some(rendered)
588}
589
590fn import_order_requires_source(binder: &ImportBinder, local_names: &HashSet<String>) -> bool {
591 binder
592 .bindings
593 .keys()
594 .any(|bound_name| local_names.contains(bound_name))
595}