1use brokk_bifrost_core::analyzer::capabilities::ImportAnalysisProvider;
13use brokk_bifrost_core::analyzer::model::ImportInfo;
14use brokk_bifrost_core::analyzer::prepared_syntax::{IndexedFileFacts, PreparedSyntaxTree};
15use brokk_bifrost_core::analyzer::project::Project;
16use brokk_bifrost_core::analyzer::query_token::QueryToken;
17use brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path;
18use brokk_bifrost_core::analyzer::usages::model::{
19 ExportEntry, ExportIndex, ImportBinder, ImportBinding, ImportKind, ReexportStar,
20};
21use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, Language, ProjectFile};
22use brokk_bifrost_core::hash::{HashMap, HashSet};
23use std::path::{Path, PathBuf};
24use std::sync::Arc;
25use tree_sitter::Node;
26
27use crate::bindings::{python_direct_scope_bindings_bounded, python_executable_scope_children};
28use crate::declarations::{
29 module_code_unit, parse_python_tree, python_base_origin_node, python_declared_import_root,
30 python_import_root, python_project_module_components, python_repeated_root_extension_dir,
31};
32use crate::imports::{
33 PythonImportDetails, python_import_details, python_namespace_binding_module,
34 python_namespace_binding_name, resolve_exported_fqn, resolve_import_bindings,
35 resolve_python_relative_module,
36};
37
38pub trait PythonSource:
49 CodeUnitIndex + ImportAnalysisProvider + crate::source_facts::PythonSourceFactProvider
50{
51 fn record_memo_inputs(&self) -> [u8; 32];
55
56 fn path_module_fqn(&self, module_fq: &str) -> Option<Vec<CodeUnit>>;
57
58 fn path_module_fqns_batch(&self, module_fqs: &[String]) -> Vec<Option<Vec<CodeUnit>>>;
61
62 fn definition_fqn(&self, fqn: &str) -> Vec<CodeUnit>;
63
64 fn module_spellings(&self) -> Arc<PythonModuleSpellings>;
71
72 fn import_binder_of(&self, file: &ProjectFile) -> Arc<ImportBinder>;
77
78 fn export_index_of(&self, file: &ProjectFile) -> Arc<ExportIndex>;
79
80 fn prepared_syntax(
85 &self,
86 token: QueryToken<'_>,
87 file: &ProjectFile,
88 ) -> Option<Arc<PreparedSyntaxTree>>;
89
90 fn visit_file_facts(
93 &self,
94 files: &[ProjectFile],
95 keep_going: &dyn Fn() -> bool,
96 visit: &mut dyn FnMut(&ProjectFile, Option<&dyn IndexedFileFacts>),
97 );
98}
99
100pub fn retain_modules_for_importer<T>(
119 project: &dyn Project,
120 importer: &ProjectFile,
121 candidates: &mut Vec<T>,
122 source_of: impl Fn(&T) -> &ProjectFile,
123) {
124 retain_nearest_owning_root(importer.rel_path(), candidates, |candidate| {
125 let source = source_of(candidate);
126 debug_assert_eq!(
127 importer.root(),
128 source.root(),
129 "one workspace's Python files share one project root"
130 );
131 python_import_root(project, source)
132 });
133}
134
135pub fn retain_nearest_owning_root<T>(
145 importer: &Path,
146 candidates: &mut Vec<T>,
147 root_of: impl Fn(&T) -> PathBuf,
148) {
149 if candidates.len() < 2 {
150 return;
151 }
152 let owning_depth = |candidate: &T| {
153 let root = root_of(candidate);
154 importer
155 .starts_with(&root)
156 .then(|| root.components().count())
157 };
158 let depths = candidates.iter().map(owning_depth).collect::<Vec<_>>();
159 let Some(nearest) = depths.iter().copied().flatten().max() else {
160 return;
161 };
162 *candidates = std::mem::take(candidates)
163 .into_iter()
164 .zip(depths)
165 .filter(|(_, depth)| *depth == Some(nearest))
166 .map(|(candidate, _)| candidate)
167 .collect();
168}
169
170#[derive(Clone, Debug)]
172struct ProjectRootModuleName {
173 file: ProjectFile,
174 components: Vec<String>,
175}
176
177#[derive(Clone, Debug)]
186pub struct PythonSourceRootModule {
187 pub file: ProjectFile,
188 pub source_root: PathBuf,
192}
193
194#[derive(Default)]
216pub struct PythonModuleSpellings {
217 project: Option<Arc<dyn Project>>,
218 by_last_component: HashMap<String, Vec<ProjectRootModuleName>>,
219 package_dirs: HashSet<PathBuf>,
222 path_extensions: HashMap<PathBuf, HashSet<PathBuf>>,
234}
235
236impl std::fmt::Debug for PythonModuleSpellings {
237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 f.debug_struct("PythonModuleSpellings")
239 .field("by_last_component", &self.by_last_component)
240 .field("package_dirs", &self.package_dirs)
241 .field("path_extensions", &self.path_extensions)
242 .finish_non_exhaustive()
243 }
244}
245
246impl PythonModuleSpellings {
247 pub fn authority_rows(&self) -> Vec<String> {
250 let mut rows = Vec::new();
251 for (last, candidates) in &self.by_last_component {
252 for candidate in candidates {
253 let root = self
254 .project()
255 .and_then(|project| python_declared_import_root(project, &candidate.file))
256 .map(|path| {
257 brokk_bifrost_core::path_utils::workspace_relative_slash_path(&path)
258 });
259 rows.push(format!(
260 "module:{last:?}:{:?}:{:?}:{root:?}",
261 brokk_bifrost_core::path_utils::rel_path_string(&candidate.file),
262 candidate.components
263 ));
264 }
265 }
266 rows.extend(self.package_dirs.iter().map(|directory| {
267 format!(
268 "package:{:?}",
269 brokk_bifrost_core::path_utils::workspace_relative_slash_path(directory)
270 )
271 }));
272 for (directory, extensions) in &self.path_extensions {
273 for extension in extensions {
274 rows.push(format!(
275 "extension:{:?}:{:?}",
276 brokk_bifrost_core::path_utils::workspace_relative_slash_path(directory),
277 brokk_bifrost_core::path_utils::workspace_relative_slash_path(extension)
278 ));
279 }
280 }
281 rows.sort();
282 rows
283 }
284
285 pub(crate) fn project(&self) -> Option<&dyn Project> {
286 self.project.as_deref()
287 }
288
289 pub fn build(files: impl IntoIterator<Item = ProjectFile>, project: Arc<dyn Project>) -> Self {
295 let mut by_last_component: HashMap<String, Vec<ProjectRootModuleName>> = HashMap::default();
296 let mut package_dirs: HashSet<PathBuf> = HashSet::default();
297 let mut package_markers: Vec<ProjectFile> = Vec::new();
298 for file in files {
299 if file.rel_path().file_name().and_then(|name| name.to_str()) == Some("__init__.py")
300 && let Some(directory) = file.rel_path().parent()
301 {
302 package_dirs.insert(directory.to_path_buf());
303 package_markers.push(file.clone());
304 }
305 let components = python_project_module_components(&file);
306 let Some(last) = components.last() else {
307 continue;
308 };
309 by_last_component
310 .entry(last.clone())
311 .or_default()
312 .push(ProjectRootModuleName { file, components });
313 }
314 let mut path_extensions: HashMap<PathBuf, HashSet<PathBuf>> = HashMap::default();
315 for marker in package_markers {
316 let Some(directory) = marker.rel_path().parent() else {
317 continue;
318 };
319 if !python_package_holds_a_repeat_of_itself(directory, &package_dirs) {
320 continue;
321 }
322 let Ok(source) = project.read_source(&marker) else {
323 continue;
324 };
325 let extended = parse_python_tree(&source)
326 .map(|tree| {
327 crate::package_paths::appended_path_dirs(tree.root_node(), &source)
328 .into_iter()
329 .filter_map(|relative| {
330 brokk_bifrost_core::path_utils::normalize_relative_path_from(
331 directory, &relative,
332 )
333 })
334 .collect::<Vec<_>>()
335 })
336 .unwrap_or_default();
337 if !extended.is_empty() {
338 path_extensions
339 .entry(directory.to_path_buf())
340 .or_default()
341 .extend(extended);
342 }
343 }
344 Self {
345 project: Some(project),
346 by_last_component,
347 package_dirs,
348 path_extensions,
349 }
350 }
351
352 pub fn suffix_candidates(&self, module_fq: &str) -> Vec<PythonSourceRootModule> {
364 if module_fq.starts_with('.') {
368 return Vec::new();
369 }
370 let components = parse_symbol_path(Language::Python, module_fq);
371 let Some(last) = components.last().filter(|_| components.len() > 1) else {
372 return Vec::new();
373 };
374 let mut modules = Vec::new();
375 for candidate in self
376 .by_last_component
377 .get(last.as_str())
378 .into_iter()
379 .flatten()
380 {
381 let Some(dropped) = candidate
384 .components
385 .len()
386 .checked_sub(components.len())
387 .filter(|dropped| *dropped > 0)
388 else {
389 continue;
390 };
391 if candidate.components[dropped..] != components[..] {
392 continue;
393 }
394 let source_root: PathBuf = candidate
397 .file
398 .rel_path()
399 .components()
400 .take(dropped)
401 .collect();
402 debug_assert!(
403 !source_root.as_os_str().is_empty(),
404 "dropping a component of {:?} leaves a directory",
405 candidate.file.rel_path()
406 );
407 modules.push(PythonSourceRootModule {
408 file: candidate.file.clone(),
409 source_root,
410 });
411 }
412 modules
413 }
414
415 pub fn establishes_source_root(&self, candidate: &PythonSourceRootModule) -> bool {
461 if let Some(declared) = python_declared_import_root(
462 self.project()
463 .expect("source-root candidates have a selected project"),
464 &candidate.file,
465 ) {
466 if declared == candidate.source_root {
467 return true;
468 }
469 let Some(extension) = python_repeated_root_extension_dir(
470 &candidate.file,
471 &candidate.source_root,
472 &declared,
473 ) else {
474 return false;
475 };
476 return self.package_chain_reaches_module(candidate)
477 && self
478 .path_extensions
479 .get(&candidate.source_root)
480 .is_some_and(|extended| extended.contains(&extension));
481 }
482 self.package_chain_reaches_module(candidate)
483 }
484
485 fn package_chain_reaches_module(&self, candidate: &PythonSourceRootModule) -> bool {
494 debug_assert!(
495 candidate
496 .file
497 .rel_path()
498 .starts_with(&candidate.source_root),
499 "a candidate's implied root is a prefix of its own path"
500 );
501 let mut directory = candidate.file.rel_path().parent();
502 while let Some(current) = directory {
503 if !self.package_dirs.contains(current) {
504 return false;
505 }
506 if current == candidate.source_root {
507 return true;
508 }
509 directory = current.parent();
510 }
511 false
512 }
513}
514
515fn python_package_holds_a_repeat_of_itself(
523 directory: &Path,
524 package_dirs: &HashSet<PathBuf>,
525) -> bool {
526 directory
527 .file_name()
528 .is_some_and(|name| package_dirs.contains(&directory.join(name)))
529}
530
531pub fn resolve_module_code_unit(
539 python: &dyn PythonSource,
540 importer: Option<&ProjectFile>,
541 module_fq: &str,
542) -> Option<CodeUnit> {
543 let indexed = match python.path_module_fqn(module_fq) {
544 Some(units) => indexed_module_for_importer(python.project(), importer, units),
547 None => indexed_module_for_importer(
548 python.project(),
549 importer,
550 python.definition_fqn(module_fq),
551 ),
552 };
553 match indexed {
554 IndexedModuleLookup::Unique(module) => Some(module),
555 IndexedModuleLookup::Ambiguous => None,
559 IndexedModuleLookup::Missing => {
560 let importer = importer?;
561 module_under_source_root(&python.module_spellings(), importer, module_fq)
562 }
563 }
564}
565
566enum IndexedModuleLookup {
575 Unique(CodeUnit),
576 Ambiguous,
577 Missing,
578}
579
580fn indexed_module_for_importer(
581 project: &dyn Project,
582 importer: Option<&ProjectFile>,
583 units: Vec<CodeUnit>,
584) -> IndexedModuleLookup {
585 let mut modules = units
586 .into_iter()
587 .filter(CodeUnit::is_module)
588 .collect::<Vec<_>>();
589 if let Some(importer) = importer {
590 retain_modules_for_importer(project, importer, &mut modules, CodeUnit::source);
591 }
592 match modules.len() {
593 0 => IndexedModuleLookup::Missing,
594 1 => IndexedModuleLookup::Unique(modules.remove(0)),
595 _ => IndexedModuleLookup::Ambiguous,
596 }
597}
598
599pub fn python_module_file_under_source_root(
628 spellings: &PythonModuleSpellings,
629 importer: &ProjectFile,
630 module_fq: &str,
631) -> Option<ProjectFile> {
632 let mut candidates = spellings.suffix_candidates(module_fq);
633 retain_nearest_owning_root(importer.rel_path(), &mut candidates, |candidate| {
634 candidate.source_root.clone()
635 });
636 candidates.retain(|candidate| spellings.establishes_source_root(candidate));
637 let [candidate] = candidates.as_slice() else {
638 return None;
639 };
640 debug_assert_eq!(
641 importer.root(),
642 candidate.file.root(),
643 "one workspace's Python files share one project root"
644 );
645 Some(candidate.file.clone())
646}
647
648fn module_under_source_root(
653 spellings: &PythonModuleSpellings,
654 importer: &ProjectFile,
655 module_fq: &str,
656) -> Option<CodeUnit> {
657 let file = python_module_file_under_source_root(spellings, importer, module_fq)?;
658 module_code_unit(spellings.project()?, &file)
659}
660
661pub fn resolve_module_code_units_batch(
667 python: &dyn PythonSource,
668 importer: Option<&ProjectFile>,
669 module_fqs: &[String],
670) -> Vec<Option<CodeUnit>> {
671 let path_results = python.path_module_fqns_batch(module_fqs);
672 let mut lookups: Vec<IndexedModuleLookup> = Vec::with_capacity(module_fqs.len());
673 let mut needs_definition_fallback = Vec::new();
674 for (i, units) in path_results.into_iter().enumerate() {
675 match units {
676 Some(units) => lookups.push(indexed_module_for_importer(
677 python.project(),
678 importer,
679 units,
680 )),
681 None => {
682 needs_definition_fallback.push(i);
683 lookups.push(IndexedModuleLookup::Missing);
685 }
686 }
687 }
688 for i in needs_definition_fallback {
689 lookups[i] = indexed_module_for_importer(
690 python.project(),
691 importer,
692 python.definition_fqn(&module_fqs[i]),
693 );
694 }
695 let mut results: Vec<Option<CodeUnit>> = vec![None; module_fqs.len()];
696 let mut needs_source_root = Vec::new();
697 for (i, lookup) in lookups.into_iter().enumerate() {
698 match lookup {
699 IndexedModuleLookup::Unique(module) => results[i] = Some(module),
700 IndexedModuleLookup::Ambiguous => {}
701 IndexedModuleLookup::Missing => needs_source_root.push(i),
702 }
703 }
704 if let Some(importer) = importer
705 && !needs_source_root.is_empty()
706 {
707 let spellings = python.module_spellings();
710 for i in needs_source_root {
711 results[i] = module_under_source_root(&spellings, importer, &module_fqs[i]);
712 }
713 }
714 results
715}
716
717pub fn compute_export_index_of(
718 python: &dyn PythonSource,
719 token: QueryToken<'_>,
720 file: &ProjectFile,
721) -> ExportIndex {
722 let mut index = ExportIndex::empty();
723 let mut events = Vec::new();
724 let declarations = python.top_level_declarations(file);
725 collect_local_export_events(
726 declarations.iter(),
727 |code_unit| {
728 python
729 .ranges(code_unit)
730 .iter()
731 .map(|range| range.start_byte)
732 .max()
733 .unwrap_or(usize::MAX)
734 },
735 &mut events,
736 );
737
738 let imports = python.import_info_of(token, file);
739 collect_reexport_events_from_imports(python, file, &imports, &mut events, &mut index);
740
741 finish_export_index(events, index)
742}
743
744pub fn export_index_from_file_facts(
745 python: &dyn PythonSource,
746 file: &ProjectFile,
747 facts: &dyn IndexedFileFacts,
748 module_name: &str,
749) -> ExportIndex {
750 let mut index = ExportIndex::empty();
751 let mut events = Vec::new();
752 collect_local_export_events(
753 facts.top_level_declarations().iter(),
754 |code_unit| {
755 facts
756 .declaration_ranges(code_unit)
757 .into_iter()
758 .flatten()
759 .map(|range| range.start_byte)
760 .max()
761 .unwrap_or(usize::MAX)
762 },
763 &mut events,
764 );
765
766 if !facts
767 .top_level_declarations()
768 .iter()
769 .any(CodeUnit::is_module)
770 && let Some(identifier) = module_name.rsplit('.').next()
771 && !identifier.is_empty()
772 && !identifier.starts_with('_')
773 {
774 events.push((
775 0,
776 identifier.to_string(),
777 ExportEntry::Local {
778 local_name: identifier.to_string(),
779 },
780 ));
781 }
782
783 collect_reexport_events_from_imports(python, file, facts.imports(), &mut events, &mut index);
784
785 finish_export_index(events, index)
786}
787
788fn collect_local_export_events<'a>(
789 declarations: impl IntoIterator<Item = &'a CodeUnit>,
790 mut start_byte: impl FnMut(&CodeUnit) -> usize,
791 events: &mut Vec<(usize, String, ExportEntry)>,
792) -> HashSet<String> {
793 let mut local_names = HashSet::default();
794 for code_unit in declarations {
795 let identifier = code_unit.identifier().trim();
796 if identifier.is_empty() {
797 continue;
798 }
799 local_names.insert(identifier.to_string());
800 events.push((
801 start_byte(code_unit),
802 identifier.to_string(),
803 ExportEntry::Local {
804 local_name: identifier.to_string(),
805 },
806 ));
807 }
808 local_names
809}
810
811fn finish_export_index(
812 mut events: Vec<(usize, String, ExportEntry)>,
813 mut index: ExportIndex,
814) -> ExportIndex {
815 events.sort_by_key(|(start_byte, _, _)| *start_byte);
816 for (_, exported_name, entry) in events {
817 index.exports_by_name.insert(exported_name, entry);
818 }
819 index
820}
821
822fn collect_reexport_events_from_imports(
823 python: &dyn PythonSource,
824 file: &ProjectFile,
825 imports: &[ImportInfo],
826 events: &mut Vec<(usize, String, ExportEntry)>,
827 index: &mut ExportIndex,
828) {
829 for import in imports {
830 if import
831 .path
832 .as_ref()
833 .is_none_or(|path| !path.lexical_scopes.is_empty())
834 {
835 continue;
836 }
837 record_single_reexport_event(python, file, import, events, index);
838 }
839}
840
841fn record_single_reexport_event(
842 python: &dyn PythonSource,
843 file: &ProjectFile,
844 import: &ImportInfo,
845 events: &mut Vec<(usize, String, ExportEntry)>,
846 index: &mut ExportIndex,
847) {
848 let Some(PythonImportDetails::FromImport {
849 module,
850 name,
851 alias,
852 wildcard,
853 }) = python_import_details(import)
854 else {
855 return;
856 };
857 let start_byte = import
858 .path
859 .as_ref()
860 .map(|path| path.declaration_start_byte)
861 .unwrap_or(usize::MAX);
862 let resolved_module = if module.starts_with('.') {
863 resolve_python_relative_module(python.project(), file, &module)
864 } else {
865 Some(module.clone())
866 };
867 let Some(resolved_module) = resolved_module else {
868 return;
869 };
870
871 if wildcard {
872 index.reexport_stars.push(ReexportStar {
873 module_specifier: resolved_module,
874 });
875 return;
876 }
877 let exported_name = alias.unwrap_or(name.clone());
878 let module_candidate = format!("{resolved_module}.{name}");
884 if resolve_module_code_unit(python, Some(file), &module_candidate).is_some() {
885 events.push((
886 start_byte,
887 exported_name,
888 ExportEntry::ReexportedModule {
889 module_specifier: module_candidate,
890 },
891 ));
892 return;
893 }
894 events.push((
895 start_byte,
896 exported_name,
897 ExportEntry::ReexportedNamed {
898 module_specifier: resolved_module,
899 imported_name: name,
900 },
901 ));
902}
903
904pub fn import_binder_from_imports(
905 python: &dyn PythonSource,
906 file: &ProjectFile,
907 imports: &[ImportInfo],
908) -> ImportBinder {
909 let mut binder = ImportBinder::empty();
910
911 for (local_name, binding) in import_bindings_from_imports(python, file, imports) {
912 binder.bindings.insert(local_name, binding);
913 }
914
915 binder
916}
917
918pub fn import_bindings_from_imports(
923 python: &dyn PythonSource,
924 file: &ProjectFile,
925 imports: &[ImportInfo],
926) -> Vec<(String, ImportBinding)> {
927 let mut bindings = Vec::new();
928
929 for import in imports {
930 let Some(details) = python_import_details(import) else {
931 continue;
932 };
933 match details {
934 PythonImportDetails::Import { module, alias } => {
935 let local_name = python_namespace_binding_name(import, alias.as_deref(), &module);
936 let module_specifier =
937 python_namespace_binding_module(import, alias.as_deref(), &module);
938 bindings.push((
939 local_name,
940 ImportBinding {
941 module_specifier,
942 namespace_imported_module: Some(module),
943 kind: ImportKind::Namespace,
944 imported_name: None,
945 },
946 ));
947 }
948 PythonImportDetails::FromImport {
949 module,
950 name,
951 wildcard,
952 ..
953 } => {
954 let resolved_module = if module.starts_with('.') {
955 resolve_python_relative_module(python.project(), file, &module)
956 } else {
957 Some(module.clone())
958 };
959 let Some(resolved_module) = resolved_module else {
960 continue;
961 };
962 if wildcard {
963 bindings.extend(
968 public_declarations_in_module(python, file, &resolved_module)
969 .into_iter()
970 .map(|declaration| {
971 let name = declaration.identifier().to_string();
972 (
973 name.clone(),
974 ImportBinding {
975 module_specifier: resolved_module.clone(),
976 namespace_imported_module: None,
977 kind: ImportKind::Named,
978 imported_name: Some(name),
979 },
980 )
981 }),
982 );
983 continue;
984 }
985 let local_name = import
990 .local_name()
991 .map(str::to_string)
992 .unwrap_or_else(|| name.clone());
993 let module_candidate = format!("{resolved_module}.{name}");
994 let exported = resolve_exported_fqn(python, Some(file), &module_candidate);
995 let exported_module =
996 if !exported.is_empty() && exported.iter().all(CodeUnit::is_module) {
997 let mut paths = exported.iter().map(CodeUnit::fq_segment_texts);
998 let path = paths
999 .next()
1000 .expect("non-empty exported modules have a first path");
1001 paths
1002 .all(|candidate| candidate == path)
1003 .then(|| path.join("."))
1004 } else {
1005 None
1006 };
1007 let implicit_submodule = if exported.is_empty()
1008 && resolve_module_code_unit(python, Some(file), &module_candidate).is_some()
1009 {
1010 Some(module_candidate)
1011 } else {
1012 None
1013 };
1014 if let Some(module_specifier) = exported_module.or(implicit_submodule) {
1015 bindings.push((
1016 local_name,
1017 ImportBinding {
1018 module_specifier,
1019 namespace_imported_module: None,
1020 kind: ImportKind::Namespace,
1021 imported_name: None,
1022 },
1023 ));
1024 continue;
1025 }
1026 bindings.push((
1027 local_name,
1028 ImportBinding {
1029 module_specifier: resolved_module,
1030 namespace_imported_module: None,
1031 kind: ImportKind::Named,
1032 imported_name: Some(name),
1033 },
1034 ));
1035 }
1036 }
1037 }
1038
1039 bindings
1040}
1041
1042pub fn public_declarations_in_module(
1043 python: &dyn PythonSource,
1044 importer: &ProjectFile,
1045 module_fq: &str,
1046) -> Vec<CodeUnit> {
1047 let Some(module_code_unit) = resolve_module_code_unit(python, Some(importer), module_fq) else {
1048 return Vec::new();
1049 };
1050 python
1051 .direct_children(&module_code_unit)
1052 .into_iter()
1053 .filter(|code_unit| !code_unit.identifier().starts_with('_'))
1054 .collect()
1055}
1056
1057pub fn resolve_base_class(
1058 python: &dyn PythonSource,
1059 token: QueryToken<'_>,
1060 code_unit: &CodeUnit,
1061 raw: &str,
1062) -> Option<CodeUnit> {
1063 let trimmed = raw.trim();
1064 if trimmed.is_empty() {
1065 return None;
1066 }
1067
1068 let binder = python.import_binder_of(code_unit.source());
1069 if let Some((head, tail)) = trimmed.split_once('.') {
1070 if let Some(binding) = binder.bindings.get(head)
1071 && binding.kind == ImportKind::Namespace
1072 {
1073 let fq_name = format!("{}.{}", binding.module_specifier, tail);
1074 return python.definitions(&fq_name).next();
1075 }
1076 return python.definitions(trimmed).next();
1077 }
1078
1079 if let Some(binding) = binder.bindings.get(trimmed) {
1080 match binding.kind {
1081 ImportKind::Namespace => {
1082 return resolve_module_code_unit(
1083 python,
1084 Some(code_unit.source()),
1085 &binding.module_specifier,
1086 );
1087 }
1088 ImportKind::Named => {
1089 let imported_name = binding.imported_name.as_ref()?;
1090 let fqn = format!("{}.{}", binding.module_specifier, imported_name);
1091 return resolve_exported_fqn(python, Some(code_unit.source()), &fqn)
1092 .into_iter()
1093 .next()
1094 .or_else(|| python.definitions(&fqn).next());
1095 }
1096 _ => {}
1097 }
1098 }
1099
1100 if python
1101 .import_info_of(token, code_unit.source())
1102 .iter()
1103 .any(|import| import.is_wildcard)
1104 && let Some(imported) =
1105 resolve_import_bindings(python, token, code_unit.source()).get(trimmed)
1106 {
1107 return Some(imported.clone());
1108 }
1109
1110 if let Some(local) = resolve_preceding_local_class_base(python, token, code_unit, trimmed) {
1111 return Some(local);
1112 }
1113
1114 let local_fq_name = format!("{}.{}", code_unit.package_name(), trimmed);
1115 python
1116 .definitions(&local_fq_name)
1117 .next()
1118 .or_else(|| python.definitions(trimmed).next())
1119}
1120
1121fn class_definition_in_statement(node: Node<'_>) -> Option<Node<'_>> {
1122 if node.kind() == "class_definition" {
1123 return Some(node);
1124 }
1125 (node.kind() == "decorated_definition")
1126 .then(|| {
1127 let mut cursor = node.walk();
1128 node.named_children(&mut cursor)
1129 .find(|child| child.kind() == "class_definition")
1130 })
1131 .flatten()
1132}
1133
1134fn declaration_statement(node: Node<'_>) -> Option<Node<'_>> {
1135 match node.kind() {
1136 "decorated_definition" if class_definition_in_statement(node).is_some() => Some(node),
1137 "class_definition" => Some(
1138 node.parent()
1139 .filter(|parent| parent.kind() == "decorated_definition")
1140 .unwrap_or(node),
1141 ),
1142 _ => None,
1143 }
1144}
1145
1146fn scope_statement_binds_name(node: Node<'_>, source: &str, name: &str) -> bool {
1147 let mut stack = vec![node];
1148 while let Some(node) = stack.pop() {
1149 if node.kind() == "wildcard_import" {
1150 return true;
1151 }
1152 if matches!(node.kind(), "global_statement" | "nonlocal_statement") {
1153 let mut cursor = node.walk();
1154 if node.named_children(&mut cursor).any(|child| {
1155 child.kind() == "identifier"
1156 && child.utf8_text(source.as_bytes()).ok() == Some(name)
1157 }) {
1158 return true;
1159 }
1160 }
1161 let Some(bindings) = python_direct_scope_bindings_bounded(node, source, || true) else {
1162 return true;
1163 };
1164 if bindings
1165 .iter()
1166 .any(|binding| binding.declaration.utf8_text(source.as_bytes()).ok() == Some(name))
1167 {
1168 return true;
1169 }
1170 stack.extend(python_executable_scope_children(node));
1171 }
1172 false
1173}
1174
1175fn resolve_preceding_local_class_base(
1176 python: &dyn PythonSource,
1177 token: QueryToken<'_>,
1178 code_unit: &CodeUnit,
1179 raw: &str,
1180) -> Option<CodeUnit> {
1181 let parent = python.parent_of(code_unit)?;
1182 if !parent.is_function() && !parent.is_class() {
1183 return None;
1184 }
1185 let prepared = python.prepared_syntax(token, code_unit.source())?;
1186 let source = prepared.source();
1187 let current_node = prepared.declaration_node(code_unit)?;
1188 let current_statement = declaration_statement(current_node)?;
1189 let current_class = class_definition_in_statement(current_statement)?;
1190 let base_list = current_class.child_by_field_name("superclasses")?;
1191 let mut base_cursor = base_list.walk();
1192 if !base_list.named_children(&mut base_cursor).any(|base| {
1193 let origin = python_base_origin_node(base);
1194 origin.kind() == "identifier" && origin.utf8_text(source.as_bytes()).ok() == Some(raw)
1195 }) {
1196 return None;
1197 }
1198 let block = current_statement.parent()?;
1199 let mut block_cursor = block.walk();
1200 let siblings = block.named_children(&mut block_cursor).collect::<Vec<_>>();
1201 let current_index = siblings
1202 .iter()
1203 .position(|sibling| sibling.id() == current_statement.id())?;
1204
1205 let mut preceding: Option<(usize, CodeUnit, Node<'_>)> = None;
1206 for candidate in prepared.direct_children(&parent) {
1207 if !candidate.is_class()
1208 || candidate.source() != code_unit.source()
1209 || candidate.identifier() != raw
1210 {
1211 continue;
1212 }
1213 let Some(candidate_node) = prepared.declaration_node(candidate) else {
1214 continue;
1215 };
1216 let Some(candidate_statement) = declaration_statement(candidate_node) else {
1217 continue;
1218 };
1219 if candidate_statement.parent().map(|node| node.id()) != Some(block.id())
1220 || candidate_statement.start_byte() >= current_statement.start_byte()
1221 {
1222 continue;
1223 }
1224 if preceding
1225 .as_ref()
1226 .is_none_or(|(start, _, _)| candidate_statement.start_byte() > *start)
1227 {
1228 preceding = Some((
1229 candidate_statement.start_byte(),
1230 candidate.clone(),
1231 candidate_statement,
1232 ));
1233 }
1234 }
1235 let (_, candidate, candidate_statement) = preceding?;
1236 let candidate_index = siblings
1237 .iter()
1238 .position(|sibling| sibling.id() == candidate_statement.id())?;
1239 if candidate_index >= current_index
1240 || siblings[candidate_index + 1..current_index]
1241 .iter()
1242 .any(|statement| scope_statement_binds_name(*statement, source, raw))
1243 {
1244 return None;
1245 }
1246 Some(candidate)
1247}
1248
1249pub fn render_skeleton_recursive(
1250 index: &dyn CodeUnitIndex,
1251 code_unit: &CodeUnit,
1252 indent: &str,
1253 header_only: bool,
1254 out: &mut String,
1255) {
1256 enum Work {
1257 Declaration(CodeUnit, String),
1258 Elision(String),
1259 }
1260 let mut stack = vec![Work::Declaration(code_unit.clone(), indent.to_string())];
1261 while let Some(work) = stack.pop() {
1262 let Work::Declaration(unit, indent) = work else {
1263 let Work::Elision(indent) = work else {
1264 unreachable!()
1265 };
1266 out.push_str(&indent);
1267 out.push_str("[...]\n");
1268 continue;
1269 };
1270 if !unit.is_module()
1271 && !unit.is_file_scope()
1272 && let Some(signature) = index.signatures(&unit).first()
1273 {
1274 let signature = if unit.is_field() {
1277 signature.strip_suffix(" ...").unwrap_or(signature)
1278 } else {
1279 signature
1280 };
1281 for line in signature.lines() {
1282 out.push_str(&indent);
1283 out.push_str(line);
1284 out.push('\n');
1285 }
1286 }
1287 let children = index.direct_children(&unit);
1288 let child_indent = format!("{indent} ");
1289 if header_only && children.iter().any(|child| !child.is_field()) {
1290 stack.push(Work::Elision(child_indent.clone()));
1291 }
1292 for child in children
1293 .into_iter()
1294 .rev()
1295 .filter(|child| !header_only || child.is_field())
1296 {
1297 stack.push(Work::Declaration(child, child_indent.clone()));
1298 }
1299 }
1300}