1use brokk_bifrost_core::analyzer::query_token::QueryToken;
15use brokk_bifrost_core::analyzer::usages::local_inference::LocalBindingsSnapshot;
16use brokk_bifrost_core::analyzer::usages::model::{
17 ExportEntry, ExportIndex, ImportBinder, ImportBinding, ImportKind,
18};
19use brokk_bifrost_core::analyzer::usages::reexport_seeds;
20use brokk_bifrost_core::analyzer::usages::{ImportEdge, ImportEdgeKind};
21use brokk_bifrost_core::analyzer::{CodeUnit, Language, ProjectFile};
22use brokk_bifrost_core::hash::{HashMap, HashSet};
23use std::collections::BTreeSet;
24use std::sync::Arc;
25
26use crate::declarations::python_module_name;
27use crate::graph_support::{
28 PythonModuleSpellings, PythonSource, PythonUsageSource, export_index_from_file_facts,
29 import_binder_from_imports, import_bindings_from_imports, python_module_file_under_source_root,
30 retain_modules_for_importer,
31};
32use crate::imports::{
33 literal_importlib_modules, module_replacement_of, resolve_python_module_specifier,
34};
35
36#[derive(Debug, Default)]
38pub struct PythonUsageIndex {
39 module_index: HashMap<String, Vec<ProjectFile>>,
40 module_spellings: Arc<PythonModuleSpellings>,
41 exports_by_file: HashMap<ProjectFile, Arc<ExportIndex>>,
42 reexport_edges: HashMap<(ProjectFile, String), Vec<(ProjectFile, String)>>,
43 star_reexports: HashMap<ProjectFile, Vec<ProjectFile>>,
44 importer_reverse: HashMap<ProjectFile, Vec<ImportEdge>>,
45 literal_importlib_importers: HashMap<ProjectFile, Vec<ProjectFile>>,
46}
47
48pub type ModuleBindingTimeline = HashMap<String, Vec<ModuleBindingEvent>>;
49pub type PythonScopeFacts = HashMap<CodeUnit, LocalBindingsSnapshot<String>>;
50
51#[derive(Clone, Debug)]
52pub struct ModuleBindingEvent {
53 pub visible_from: usize,
54 pub conditional: bool,
55 pub kind: ModuleBindingEventKind,
56}
57
58#[derive(Clone, Debug)]
59pub enum ModuleBindingEventKind {
60 ImportModule {
61 module: String,
62 consumed_attributes: usize,
63 },
64 FromImport {
65 module: String,
66 imported_name: String,
67 },
68 Other,
69}
70
71pub fn visible_module_binding_suffix(
73 events: &[ModuleBindingEvent],
74 cutoff: usize,
75) -> impl Iterator<Item = &ModuleBindingEvent> {
76 let visible: Vec<_> = events
77 .iter()
78 .filter(|event| event.visible_from <= cutoff)
79 .collect();
80 let start = visible
81 .iter()
82 .rposition(|event| !event.conditional)
83 .unwrap_or(0);
84 visible.into_iter().skip(start)
85}
86
87fn resolve_module(
91 module_index: &HashMap<String, Vec<ProjectFile>>,
92 module_spellings: &PythonModuleSpellings,
93 importing_file: &ProjectFile,
94 module_specifier: &str,
95) -> Vec<ProjectFile> {
96 let Some(project) = module_spellings.project() else {
97 return Vec::new();
98 };
99 let resolved_module =
100 resolve_python_module_specifier(project, importing_file, module_specifier);
101 let Some(resolved_module) = resolved_module else {
102 return Vec::new();
103 };
104 let mut files = module_index
105 .get(&resolved_module)
106 .cloned()
107 .unwrap_or_default();
108 retain_modules_for_importer(project, importing_file, &mut files, |file| file);
111 if files.is_empty() {
112 files.extend(python_module_file_under_source_root(
115 module_spellings,
116 importing_file,
117 &resolved_module,
118 ));
119 }
120 files
121}
122
123fn is_sys_namespace_binding(binding: &ImportBinding) -> bool {
124 is_namespace_binding_for_module(binding, "sys")
125}
126
127fn is_importlib_namespace_binding(binding: &ImportBinding) -> bool {
128 is_namespace_binding_for_module(binding, "importlib")
129}
130
131fn is_namespace_binding_for_module(binding: &ImportBinding, module: &str) -> bool {
132 binding.kind == ImportKind::Namespace
133 && binding
134 .namespace_imported_module
135 .as_deref()
136 .unwrap_or(&binding.module_specifier)
137 == module
138}
139
140impl PythonUsageIndex {
141 pub fn authority_rows(&self) -> Vec<String> {
143 let mut rows = self.module_spellings.authority_rows();
144 for (name, files) in &self.module_index {
145 for file in files {
146 rows.push(format!(
147 "module:{name:?}:{:?}",
148 brokk_bifrost_core::path_utils::rel_path_string(file)
149 ));
150 }
151 }
152 for (file, exports) in &self.exports_by_file {
153 rows.push(format!(
154 "exports:{:?}:{:?}",
155 brokk_bifrost_core::path_utils::rel_path_string(file),
156 exports.exports_by_name.is_empty()
157 ));
158 for (name, export) in &exports.exports_by_name {
159 rows.push(format!(
160 "export:{:?}:{name:?}:{export:?}",
161 brokk_bifrost_core::path_utils::rel_path_string(file)
162 ));
163 }
164 for star in &exports.reexport_stars {
165 rows.push(format!(
166 "star:{:?}:{star:?}",
167 brokk_bifrost_core::path_utils::rel_path_string(file)
168 ));
169 }
170 }
171 for ((file, name), targets) in &self.reexport_edges {
172 for (target, export) in targets {
173 rows.push(format!(
174 "reexport:{:?}:{name:?}:{:?}:{export:?}",
175 brokk_bifrost_core::path_utils::rel_path_string(file),
176 brokk_bifrost_core::path_utils::rel_path_string(target)
177 ));
178 }
179 }
180 for (file, targets) in &self.star_reexports {
181 for target in targets {
182 rows.push(format!(
183 "star_target:{:?}:{:?}",
184 brokk_bifrost_core::path_utils::rel_path_string(file),
185 brokk_bifrost_core::path_utils::rel_path_string(target)
186 ));
187 }
188 }
189 for (file, edges) in &self.importer_reverse {
190 for edge in edges {
191 rows.push(format!(
192 "importer:{:?}:{:?}:{:?}:{:?}:{:?}",
193 brokk_bifrost_core::path_utils::rel_path_string(file),
194 brokk_bifrost_core::path_utils::rel_path_string(&edge.importer),
195 edge.local_name,
196 brokk_bifrost_core::path_utils::rel_path_string(&edge.target_file),
197 edge.kind
198 ));
199 }
200 }
201 for (file, importers) in &self.literal_importlib_importers {
202 for importer in importers {
203 rows.push(format!(
204 "literal:{:?}:{:?}",
205 brokk_bifrost_core::path_utils::rel_path_string(file),
206 brokk_bifrost_core::path_utils::rel_path_string(importer)
207 ));
208 }
209 }
210 rows.sort();
211 rows
212 }
213
214 pub fn build(
218 python: &dyn PythonSource,
219 token: QueryToken<'_>,
220 keep_going: &dyn Fn() -> bool,
221 ) -> Option<Self> {
222 if !keep_going() {
223 return None;
224 }
225 let _scope = brokk_bifrost_core::profiling::scope("PythonUsageIndex::build");
226 let mut files: Vec<ProjectFile> = python
227 .project()
228 .analyzable_files(Language::Python)
229 .map(|set| set.into_iter().collect())
230 .unwrap_or_default();
231 files.sort();
232 files.dedup();
233
234 let module_spellings = python.module_spellings();
235 let mut module_index: HashMap<String, Vec<ProjectFile>> = HashMap::default();
236 let mut exports_by_file: HashMap<ProjectFile, Arc<ExportIndex>> = HashMap::default();
237 let mut binders_by_file: HashMap<ProjectFile, Arc<ImportBinder>> = HashMap::default();
238 let mut import_bindings_by_file: HashMap<ProjectFile, Vec<(String, ImportBinding)>> =
239 HashMap::default();
240 let mut literal_importlib_modules_by_file: HashMap<ProjectFile, HashSet<String>> =
241 HashMap::default();
242 let mut replacement_modules: HashMap<ProjectFile, String> = HashMap::default();
243 python.visit_file_facts(&files, keep_going, &mut |file, facts| {
244 if !keep_going() {
245 return;
246 }
247 let module_name = facts
248 .and_then(|facts| {
249 facts
250 .top_level_declarations()
251 .iter()
252 .find(|unit| unit.is_module())
253 })
254 .map(|unit| unit.fq_name().to_string())
255 .unwrap_or_else(|| python_module_name(python.project(), file));
256 module_index
257 .entry(module_name.clone())
258 .or_default()
259 .push(file.clone());
260 if let Some(facts) = facts {
261 let import_bindings = import_bindings_from_imports(python, file, facts.imports());
262 let binder = Arc::new(import_binder_from_imports(python, file, facts.imports()));
263 if binder.bindings.values().any(is_importlib_namespace_binding) {
264 literal_importlib_modules_by_file.insert(
265 file.clone(),
266 literal_importlib_modules(facts.source(), &binder.bindings),
267 );
268 }
269 if binder.bindings.values().any(is_sys_namespace_binding)
270 && let Some(replacement) =
271 module_replacement_of(python, file, facts.source(), facts.imports())
272 {
273 replacement_modules.insert(file.clone(), replacement.target_module);
274 }
275 exports_by_file.insert(
276 file.clone(),
277 Arc::new(export_index_from_file_facts(
278 python,
279 file,
280 facts,
281 &module_name,
282 )),
283 );
284 import_bindings_by_file.insert(file.clone(), import_bindings);
285 binders_by_file.insert(file.clone(), binder);
286 } else {
287 exports_by_file.insert(file.clone(), python.export_index_of(file));
288 let binder = python.import_binder_of(file);
289 let imports = python.import_info_of(token, file);
290 if let Ok(source) = python.project().read_source(file) {
291 if binder.bindings.values().any(is_importlib_namespace_binding) {
292 literal_importlib_modules_by_file.insert(
293 file.clone(),
294 literal_importlib_modules(&source, &binder.bindings),
295 );
296 }
297 if binder.bindings.values().any(is_sys_namespace_binding)
298 && let Some(replacement) =
299 module_replacement_of(python, file, &source, &imports)
300 {
301 replacement_modules.insert(file.clone(), replacement.target_module);
302 }
303 }
304 import_bindings_by_file.insert(
305 file.clone(),
306 import_bindings_from_imports(python, file, &imports),
307 );
308 binders_by_file.insert(file.clone(), binder);
309 }
310 });
311 for resolved in module_index.values_mut() {
312 if !keep_going() {
313 return None;
314 }
315 resolved.sort();
316 resolved.dedup();
317 }
318
319 let mut raw_replacements: HashMap<ProjectFile, ProjectFile> = HashMap::default();
320 for (file, target_module) in replacement_modules {
321 if !keep_going() {
322 return None;
323 }
324 let mut targets =
325 resolve_module(&module_index, &module_spellings, &file, &target_module);
326 if targets.len() != 1 {
327 continue;
328 }
329 let target = targets.pop().expect("one module replacement target");
330 if target != file {
331 raw_replacements.insert(file, target);
332 }
333 }
334
335 let mut canonical_replacements: HashMap<ProjectFile, ProjectFile> = HashMap::default();
336 for file in raw_replacements.keys() {
337 if !keep_going() {
338 return None;
339 }
340 if let Some(target) = canonical_module_replacement(file, &raw_replacements) {
341 canonical_replacements.insert(file.clone(), target);
342 }
343 }
344 for resolved in module_index.values_mut() {
345 if !keep_going() {
346 return None;
347 }
348 let mut seen = HashSet::default();
349 resolved.retain_mut(|file| {
350 if let Some(canonical) = canonical_replacements.get(file) {
351 *file = canonical.clone();
352 }
353 seen.insert(file.clone())
354 });
355 }
356
357 let mut reexport_edges: HashMap<(ProjectFile, String), Vec<(ProjectFile, String)>> =
358 HashMap::default();
359 let mut star_reexports: HashMap<ProjectFile, Vec<ProjectFile>> = HashMap::default();
360 for (file, exports) in &exports_by_file {
361 if !keep_going() {
362 return None;
363 }
364 for (exported_name, entry) in &exports.exports_by_name {
365 if !keep_going() {
366 return None;
367 }
368 match entry {
369 ExportEntry::Local { local_name } => {
370 let Some(binder) = binders_by_file.get(file) else {
371 continue;
372 };
373 let Some(binding) = binder.bindings.get(local_name) else {
374 continue;
375 };
376 let Some(imported_name) = binding.imported_name.as_ref() else {
377 continue;
378 };
379 for resolved_file in resolve_module(
380 &module_index,
381 &module_spellings,
382 file,
383 &binding.module_specifier,
384 ) {
385 reexport_edges
386 .entry((resolved_file, imported_name.clone()))
387 .or_default()
388 .push((file.clone(), exported_name.clone()));
389 }
390 }
391 ExportEntry::Default { .. } | ExportEntry::ReexportedModule { .. } => {}
394 ExportEntry::ReexportedNamed {
395 module_specifier,
396 imported_name,
397 } => {
398 for resolved_file in
399 resolve_module(&module_index, &module_spellings, file, module_specifier)
400 {
401 reexport_edges
402 .entry((resolved_file, imported_name.clone()))
403 .or_default()
404 .push((file.clone(), exported_name.clone()));
405 }
406 }
407 }
408 }
409 for star in &exports.reexport_stars {
410 for resolved_file in resolve_module(
411 &module_index,
412 &module_spellings,
413 file,
414 &star.module_specifier,
415 ) {
416 star_reexports
417 .entry(resolved_file)
418 .or_default()
419 .push(file.clone());
420 }
421 }
422 }
423
424 if !keep_going() {
425 return None;
426 }
427 let importer_reverse = build_importer_reverse(
428 &module_index,
429 &module_spellings,
430 &files,
431 &import_bindings_by_file,
432 &exports_by_file,
433 keep_going,
434 )?;
435 let mut literal_importlib_importers: HashMap<ProjectFile, Vec<ProjectFile>> =
436 HashMap::default();
437 for (importer, modules) in literal_importlib_modules_by_file {
438 if !keep_going() {
439 return None;
440 }
441 for module in modules {
442 for target_file in
443 resolve_module(&module_index, &module_spellings, &importer, &module)
444 {
445 literal_importlib_importers
446 .entry(target_file.clone())
447 .or_default()
448 .push(importer.clone());
449 }
450 }
451 }
452 for importers in literal_importlib_importers.values_mut() {
453 if !keep_going() {
454 return None;
455 }
456 importers.sort();
457 importers.dedup();
458 }
459
460 if !keep_going() {
461 return None;
462 }
463 Some(Self {
464 module_index,
465 module_spellings,
466 exports_by_file,
467 reexport_edges,
468 star_reexports,
469 importer_reverse,
470 literal_importlib_importers,
471 })
472 }
473
474 pub fn seeds_for_target(
475 &self,
476 target_file: &ProjectFile,
477 target_short: &str,
478 ) -> BTreeSet<(ProjectFile, String)> {
479 let mut seeds: BTreeSet<(ProjectFile, String)> = BTreeSet::new();
480
481 if let Some(exports) = self.exports_by_file.get(target_file) {
482 for (exported_name, entry) in &exports.exports_by_name {
483 let local = match entry {
484 ExportEntry::Local { local_name } => Some(local_name.as_str()),
485 ExportEntry::Default { local_name } => local_name.as_deref(),
486 ExportEntry::ReexportedNamed { .. } | ExportEntry::ReexportedModule { .. } => {
487 None
488 }
489 };
490 if let Some(local_name) = local
491 && local_name == target_short
492 {
493 seeds.insert((target_file.clone(), exported_name.clone()));
494 }
495 }
496 }
497
498 reexport_seeds::expand_reexport_seeds(
499 seeds,
500 &self.reexport_edges,
501 &self.star_reexports,
502 |name| !name.starts_with('_'),
503 )
504 }
505
506 pub fn matching_edges_for_importer(
507 &self,
508 importer: &ProjectFile,
509 seeds: &BTreeSet<(ProjectFile, String)>,
510 ) -> Vec<ImportEdge> {
511 reexport_seeds::matching_edges_for_importer(&self.importer_reverse, importer, seeds)
512 }
513
514 pub fn importer_files_for_seeds(
515 &self,
516 seeds: &BTreeSet<(ProjectFile, String)>,
517 ) -> HashSet<ProjectFile> {
518 let mut importers = HashSet::default();
519 for (target_file, _) in seeds {
520 if let Some(edges) = self.importer_reverse.get(target_file) {
521 importers.extend(
522 edges
523 .iter()
524 .filter(|edge| reexport_seeds::edge_matches_seed(edge, seeds))
525 .map(|edge| edge.importer.clone()),
526 );
527 }
528 if let Some(dynamic_importers) = self.literal_importlib_importers.get(target_file) {
529 importers.extend(dynamic_importers.iter().cloned());
530 }
531 }
532 importers
533 }
534
535 pub fn resolve_module_files(
536 &self,
537 importing_file: &ProjectFile,
538 module_specifier: &str,
539 ) -> Vec<ProjectFile> {
540 resolve_module(
541 &self.module_index,
542 &self.module_spellings,
543 importing_file,
544 module_specifier,
545 )
546 }
547}
548
549fn canonical_module_replacement(
550 file: &ProjectFile,
551 replacements: &HashMap<ProjectFile, ProjectFile>,
552) -> Option<ProjectFile> {
553 let mut seen = BTreeSet::new();
554 let mut current = file.clone();
555 while let Some(target) = replacements.get(¤t) {
556 if !seen.insert(current) {
557 return None;
558 }
559 current = target.clone();
560 }
561 Some(current)
562}
563
564fn build_importer_reverse(
565 module_index: &HashMap<String, Vec<ProjectFile>>,
566 module_spellings: &PythonModuleSpellings,
567 files: &[ProjectFile],
568 bindings_by_file: &HashMap<ProjectFile, Vec<(String, ImportBinding)>>,
569 exports_by_file: &HashMap<ProjectFile, Arc<ExportIndex>>,
570 keep_going: &dyn Fn() -> bool,
571) -> Option<HashMap<ProjectFile, Vec<ImportEdge>>> {
572 let mut reverse: HashMap<ProjectFile, Vec<ImportEdge>> = HashMap::default();
573 for file in files {
574 if !keep_going() {
575 return None;
576 }
577 let Some(bindings) = bindings_by_file.get(file) else {
578 continue;
579 };
580 for (local_name, binding) in bindings {
581 let imported_module = binding
582 .namespace_imported_module
583 .as_deref()
584 .unwrap_or(&binding.module_specifier);
585 for target_file in resolve_module(module_index, module_spellings, file, imported_module)
586 {
587 if matches!(binding.kind, ImportKind::Glob) {
590 let Some(exports) = exports_by_file.get(&target_file) else {
591 continue;
592 };
593 for export_name in exports.exports_by_name.keys() {
594 if export_name.starts_with('_') {
595 continue;
596 }
597 reverse
598 .entry(target_file.clone())
599 .or_default()
600 .push(ImportEdge {
601 importer: file.clone(),
602 local_name: export_name.clone(),
603 target_file: target_file.clone(),
604 kind: ImportEdgeKind::Named(export_name.clone()),
605 });
606 }
607 continue;
608 }
609 let kind = match (binding.kind, binding.imported_name.as_deref()) {
610 (ImportKind::Default, _) => ImportEdgeKind::Default,
611 (ImportKind::Namespace, _) => ImportEdgeKind::Namespace,
612 (ImportKind::Named, Some(name)) => ImportEdgeKind::Named(name.to_string()),
613 (ImportKind::Named, None) => ImportEdgeKind::Named(local_name.clone()),
614 (ImportKind::CommonJsRequire, _) | (ImportKind::Glob, _) => continue,
616 };
617 reverse
618 .entry(target_file.clone())
619 .or_default()
620 .push(ImportEdge {
621 importer: file.clone(),
622 local_name: local_name.clone(),
623 target_file,
624 kind,
625 });
626 }
627 }
628 }
629 keep_going().then_some(reverse)
630}
631
632pub fn usage_seeds(
634 python: &dyn PythonUsageSource,
635 target_file: &ProjectFile,
636 target_short: &str,
637) -> BTreeSet<(ProjectFile, String)> {
638 python
639 .usage_index()
640 .seeds_for_target(target_file, target_short)
641}
642
643pub fn usage_matching_edges(
645 python: &dyn PythonUsageSource,
646 importer: &ProjectFile,
647 seeds: &BTreeSet<(ProjectFile, String)>,
648) -> Vec<ImportEdge> {
649 python
650 .usage_index()
651 .matching_edges_for_importer(importer, seeds)
652}
653
654pub fn usage_importer_files(
655 python: &dyn PythonUsageSource,
656 seeds: &BTreeSet<(ProjectFile, String)>,
657) -> HashSet<ProjectFile> {
658 python.usage_index().importer_files_for_seeds(seeds)
659}
660
661pub fn usage_resolve_module_files(
662 python: &dyn PythonUsageSource,
663 importing_file: &ProjectFile,
664 module_specifier: &str,
665) -> Vec<ProjectFile> {
666 python
667 .usage_index()
668 .resolve_module_files(importing_file, module_specifier)
669}
670
671pub fn usage_module_binding_timeline(
672 python: &dyn PythonUsageSource,
673 file: &ProjectFile,
674 build: impl FnOnce() -> ModuleBindingTimeline,
675) -> Arc<ModuleBindingTimeline> {
676 let mut build = Some(build);
677 python.module_binding_timeline(file, &mut || {
678 build.take().expect("memo builds at most once")()
679 })
680}
681
682pub fn usage_scope_facts(
683 python: &dyn PythonUsageSource,
684 file: &ProjectFile,
685 build: impl FnOnce() -> Option<PythonScopeFacts>,
686) -> Option<Arc<PythonScopeFacts>> {
687 let mut build = Some(build);
688 python.scope_facts(file, &mut || {
689 build.take().expect("memo builds at most once")()
690 })
691}
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696 use brokk_bifrost_test_macros::hardwon;
697
698 #[test]
699 #[hardwon("59dfb80c3bb8: module replacement chains failed to canonicalize or reject cycles")]
700 fn module_replacement_chains_canonicalize_and_cycles_are_rejected() {
701 let root = tempfile::tempdir().expect("temporary project root");
702 let first = ProjectFile::new(root.path(), "first.py");
703 let second = ProjectFile::new(root.path(), "second.py");
704 let canonical = ProjectFile::new(root.path(), "canonical.py");
705 let chain = HashMap::from_iter([
706 (first.clone(), second.clone()),
707 (second.clone(), canonical.clone()),
708 ]);
709
710 assert_eq!(
711 canonical_module_replacement(&first, &chain),
712 Some(canonical)
713 );
714
715 let cycle = HashMap::from_iter([(first.clone(), second.clone()), (second, first.clone())]);
716 assert_eq!(canonical_module_replacement(&first, &cycle), None);
717 }
718}