1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use crate::{package_imports::acquire_package_snapshots, package_snapshot::PackageSnapshot};
6use harn_lexer::Span;
7use harn_parser::SNode;
8
9pub const SOURCE_FINGERPRINT: &str = env!("HARN_SOURCE_FINGERPRINT");
11
12pub mod asset_paths;
13mod construction;
14mod declarations;
15pub mod fingerprint;
16pub mod host_capabilities;
17pub mod host_capability_config;
18mod import_recording;
19mod load_memo;
20pub mod manifest_walk;
21mod namespace_imports;
22mod namespace_signatures;
23pub mod package_execution;
24mod package_imports;
25pub mod package_snapshot;
26pub mod personas;
27pub mod project_config;
28mod source_reads;
29pub use source_reads::{
30 build_with_source, build_with_sources, module_source_disk_reads, read_module_source,
31};
32mod references;
33mod standalone;
34mod stdlib;
35pub use stdlib::{stdlib_catalog, try_stdlib_catalog};
36mod symbol_reachability;
37mod type_dependencies;
38mod typecheck;
39mod visibility;
40mod wave_load;
41use wave_load::load_wave;
42pub use wave_load::MODULE_GRAPH_JOBS_ENV;
43
44pub use construction::{
45 build, build_closed_program, build_for_reference_index, build_with_module_projection,
46 build_with_parsed_sources,
47};
48
49use declarations::{callable_decl_name, decl_site, type_decl_name};
50pub use declarations::{public_declarations, sibling_declarations, DefKind, PublicDeclaration};
51pub use namespace_imports::NamespaceImportInfo;
52pub use namespace_signatures::NamespaceMemberSignature;
53pub use package_imports::{
54 resolve_import_path, resolve_import_path_with_guard, resolve_import_path_with_snapshot,
55 unresolved_package_alias, PackageImport,
56};
57pub use references::{index_references, RefSite, ReferenceEdge, ReferenceIndex};
58pub use standalone::build_with_standalone_source;
59use standalone::PackageContext;
60pub use symbol_reachability::{
61 closed_program_reachability, ExportDemand, ModuleSymbolDemand, SymbolReachability,
62};
63pub use visibility::{sibling_directory_access, sibling_module_access};
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct DefSite {
68 pub name: String,
69 pub file: PathBuf,
70 pub kind: DefKind,
71 pub span: Span,
72}
73
74#[derive(Debug, Clone)]
76pub enum WildcardResolution {
77 Resolved(HashSet<String>),
79 Unknown,
81}
82
83#[derive(Debug, Default)]
85pub struct ModuleGraph {
86 modules: HashMap<PathBuf, Arc<ModuleInfo>>,
87 _package_snapshots: Vec<PackageSnapshot>,
89}
90
91#[derive(Debug, Clone)]
92pub struct ParsedModuleSource {
93 pub source: String,
94 pub program: Vec<SNode>,
95}
96
97#[derive(Debug, Default)]
98pub struct ModuleGraphBuild {
99 pub graph: ModuleGraph,
100 pub parsed_sources: HashMap<PathBuf, ParsedModuleSource>,
101}
102
103#[derive(Debug, Default, Clone)]
104struct ModuleInfo {
105 declarations: HashMap<String, DefSite>,
108 exports: HashSet<String>,
113 own_exports: HashSet<String>,
116 sibling_exports: HashSet<String>,
119 selective_re_exports: HashMap<String, Vec<PathBuf>>,
126 wildcard_re_export_paths: Vec<PathBuf>,
130 namespace_re_exports: HashMap<String, PathBuf>,
137 selective_import_names: HashSet<String>,
139 imports: Vec<ImportRef>,
141 has_unresolved_wildcard_import: bool,
143 has_unresolved_selective_import: bool,
147 has_unresolved_namespace_import: bool,
149 type_declarations: Vec<SNode>,
152 callable_declarations: Vec<SNode>,
155 load_error: Option<ModuleLoadError>,
161}
162
163#[derive(Debug, Clone)]
169pub struct ModuleLoadError {
170 pub message: String,
172 pub span: Span,
174}
175
176#[derive(Debug, Clone)]
179pub struct ImportCompileFailure {
180 pub import_raw_path: String,
182 pub import_span: Span,
184 pub module_path: PathBuf,
186 pub error: ModuleLoadError,
188}
189
190#[derive(Debug, Clone)]
191struct ImportRef {
192 raw_path: String,
193 path: Option<PathBuf>,
194 selective_names: Option<HashSet<String>>,
195 namespace_alias: Option<String>,
198 is_pub: bool,
199 import_span: Span,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct ModuleImport {
205 pub raw_path: String,
207 pub resolved_path: Option<PathBuf>,
209 pub selective_names: Option<Vec<String>>,
211 pub namespace_alias: Option<String>,
213 pub is_pub: bool,
215}
216
217type ModuleObserver<'a> = dyn Fn(&Path, &str, Option<&[SNode]>) + Sync + 'a;
218
219#[derive(Clone, Copy)]
220enum ParsedSourceRetention<'a> {
221 None,
222 Seeds(&'a HashSet<PathBuf>),
223 All,
224}
225
226impl ParsedSourceRetention<'_> {
227 fn retains(self, path: &Path) -> bool {
228 match self {
229 Self::None => false,
230 Self::Seeds(targets) => targets.contains(path),
231 Self::All => true,
232 }
233 }
234}
235
236fn build_inner(
237 files: &[PathBuf],
238 parsed_source_retention: ParsedSourceRetention<'_>,
239 source_overrides: Option<&HashMap<PathBuf, String>>,
240 package_context: PackageContext,
241 observer: Option<&ModuleObserver<'_>>,
242) -> ModuleGraphBuild {
243 let package_snapshots = match package_context {
244 PackageContext::Project => acquire_package_snapshots(files),
245 PackageContext::Standalone => Vec::new(),
246 };
247 let mut modules: HashMap<PathBuf, Arc<ModuleInfo>> = HashMap::new();
248 let mut parsed_sources: HashMap<PathBuf, ParsedModuleSource> = HashMap::new();
249 let mut seen: HashSet<PathBuf> = HashSet::new();
250 let mut wave: Vec<PathBuf> = Vec::new();
251 for file in files {
252 let canonical = normalize_path(file);
253 if seen.insert(canonical.clone()) {
254 wave.push(canonical);
255 }
256 }
257 while !wave.is_empty() {
265 let loaded = load_wave(
266 &wave,
267 &package_snapshots,
268 parsed_source_retention,
269 source_overrides,
270 observer,
271 );
272 let mut next_wave: Vec<PathBuf> = Vec::new();
273 for (path, (module, parsed)) in wave.drain(..).zip(loaded) {
274 if parsed_source_retention.retains(&path) {
275 if let Some(parsed) = parsed {
276 parsed_sources.insert(path.clone(), parsed);
277 }
278 }
279 for import in &module.imports {
296 if let Some(import_path) = &import.path {
297 let canonical = normalize_path(import_path);
298 if seen.insert(canonical.clone()) {
299 next_wave.push(canonical);
300 }
301 }
302 }
303 modules.insert(path, module);
304 }
305 wave = next_wave;
306 }
307 resolve_re_exports(&mut modules);
308 ModuleGraphBuild {
309 graph: ModuleGraph {
310 modules,
311 _package_snapshots: package_snapshots,
312 },
313 parsed_sources,
314 }
315}
316
317fn resolve_re_exports(modules: &mut HashMap<PathBuf, Arc<ModuleInfo>>) {
322 let keys: Vec<PathBuf> = modules.keys().cloned().collect();
323 loop {
324 let mut changed = false;
325 for path in &keys {
326 let wildcard_paths = modules
329 .get(path)
330 .map(|m| m.wildcard_re_export_paths.clone())
331 .unwrap_or_default();
332 if wildcard_paths.is_empty() {
333 continue;
334 }
335 let mut additions: Vec<String> = Vec::new();
336 for src in &wildcard_paths {
337 let src_canonical = normalize_path(src);
338 if let Some(src_module) = modules.get(src).or_else(|| modules.get(&src_canonical)) {
339 additions.extend(src_module.exports.iter().cloned());
340 }
341 }
342 let Some(module) = modules.get_mut(path) else {
345 continue;
346 };
347 additions.retain(|name| !module.exports.contains(name));
348 if !additions.is_empty() {
349 Arc::make_mut(module).exports.extend(additions);
350 changed = true;
351 }
352 }
353 if !changed {
354 break;
355 }
356 }
357}
358
359impl ModuleGraph {
360 pub fn module_paths(&self) -> Vec<PathBuf> {
365 let mut paths: Vec<PathBuf> = self.modules.keys().cloned().collect();
366 paths.sort();
367 paths
368 }
369
370 pub fn contains_module(&self, path: &Path) -> bool {
373 self.modules.contains_key(path) || self.modules.contains_key(&normalize_path(path))
374 }
375
376 pub fn all_selective_import_names(&self) -> HashSet<&str> {
378 let mut names = HashSet::new();
379 for module in self.modules.values() {
380 for name in &module.selective_import_names {
381 names.insert(name.as_str());
382 }
383 }
384 names
385 }
386
387 pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
390 let target = normalize_path(target);
391 let mut out: Vec<PathBuf> = self
392 .modules
393 .iter()
394 .filter(|(_, info)| {
395 info.imports.iter().any(|import| {
396 import
397 .path
398 .as_ref()
399 .is_some_and(|p| normalize_path(p) == target)
400 })
401 })
402 .map(|(path, _)| path.clone())
403 .collect();
404 out.sort();
405 out
406 }
407
408 pub fn transitive_importers_of(&self, target: &Path) -> Vec<PathBuf> {
415 let target = normalize_path(target);
416 let mut visited = HashSet::from([target.clone()]);
417 let mut pending = vec![target];
418 let mut out = Vec::new();
419
420 while let Some(current) = pending.pop() {
421 for importer in self.importers_of(¤t) {
422 let importer = normalize_path(&importer);
423 if visited.insert(importer.clone()) {
424 pending.push(importer.clone());
425 out.push(importer);
426 }
427 }
428 }
429
430 out.sort();
431 out
432 }
433
434 pub fn imports_for_module(&self, file: &Path) -> Vec<ModuleImport> {
436 let file = normalize_path(file);
437 let Some(module) = self.modules.get(&file) else {
438 return Vec::new();
439 };
440 let mut imports: Vec<ModuleImport> = module
441 .imports
442 .iter()
443 .map(|import| {
444 let mut selective_names = import
445 .selective_names
446 .as_ref()
447 .map(|names| names.iter().cloned().collect::<Vec<_>>());
448 if let Some(names) = selective_names.as_mut() {
449 names.sort();
450 }
451 ModuleImport {
452 raw_path: import.raw_path.clone(),
453 resolved_path: import.path.as_ref().map(|path| normalize_path(path)),
454 selective_names,
455 namespace_alias: import.namespace_alias.clone(),
456 is_pub: import.is_pub,
457 }
458 })
459 .collect();
460 imports.sort_by(|left, right| {
461 left.raw_path
462 .cmp(&right.raw_path)
463 .then_with(|| left.selective_names.cmp(&right.selective_names))
464 .then_with(|| left.namespace_alias.cmp(&right.namespace_alias))
465 .then_with(|| left.resolved_path.cmp(&right.resolved_path))
466 });
467 imports
468 }
469
470 pub fn wildcard_exports_for(&self, file: &Path) -> WildcardResolution {
475 let file = normalize_path(file);
476 let Some(module) = self.modules.get(&file) else {
477 return WildcardResolution::Unknown;
478 };
479 if module.has_unresolved_wildcard_import {
480 return WildcardResolution::Unknown;
481 }
482
483 let mut names = HashSet::new();
484 for import in module
485 .imports
486 .iter()
487 .filter(|import| import.selective_names.is_none())
488 {
489 let Some(import_path) = &import.path else {
490 return WildcardResolution::Unknown;
491 };
492 let imported = self.modules.get(import_path).or_else(|| {
493 let normalized = normalize_path(import_path);
494 self.modules.get(&normalized)
495 });
496 let Some(_imported) = imported else {
497 return WildcardResolution::Unknown;
498 };
499 names.extend(self.exports_for_import(&file, import_path));
500 }
501 WildcardResolution::Resolved(names)
502 }
503
504 #[must_use]
525 pub fn import_compile_failures(&self, file: &Path) -> Vec<ImportCompileFailure> {
526 let file = normalize_path(file);
527 let Some(module) = self.modules.get(&file) else {
528 return Vec::new();
529 };
530 let mut failures = Vec::new();
531 for import in &module.imports {
532 let Some(import_path) = &import.path else {
533 continue;
534 };
535 let Some(target) = self
536 .modules
537 .get(import_path)
538 .or_else(|| self.modules.get(&normalize_path(import_path)))
539 else {
540 continue;
541 };
542 if let Some(error) = &target.load_error {
543 failures.push(ImportCompileFailure {
544 import_raw_path: import.raw_path.clone(),
545 import_span: import.import_span,
546 module_path: normalize_path(import_path),
547 error: error.clone(),
548 });
549 }
550 }
551 failures
552 }
553
554 pub fn imported_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
555 let file = normalize_path(file);
556 let module = self.modules.get(&file)?;
557 if module.has_unresolved_wildcard_import
558 || module.has_unresolved_selective_import
559 || module.has_unresolved_namespace_import
560 {
561 return None;
562 }
563
564 let mut names = HashSet::new();
565 for import in &module.imports {
566 if let Some(alias) = &import.namespace_alias {
568 names.insert(alias.clone());
569 continue;
570 }
571 let import_path = import.path.as_ref()?;
572 let imported = self
573 .modules
574 .get(import_path)
575 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
576 if imported.load_error.is_some() {
582 return None;
583 }
584 match &import.selective_names {
585 None => {
586 names.extend(self.exports_for_import(&file, import_path));
587 }
588 Some(selective) => {
589 for name in selective {
598 if imported.declarations.contains_key(name)
599 || imported.exports.contains(name)
600 {
601 names.insert(name.clone());
602 }
603 }
604 }
605 }
606 }
607 Some(names)
608 }
609
610 pub fn imported_names_by_kind_for_file(
615 &self,
616 file: &Path,
617 kind: DefKind,
618 ) -> Option<HashSet<String>> {
619 let file = normalize_path(file);
620 let module = self.modules.get(&file)?;
621 if module.has_unresolved_wildcard_import
622 || module.has_unresolved_selective_import
623 || module.has_unresolved_namespace_import
624 {
625 return None;
626 }
627
628 let mut names = HashSet::new();
629 for import in &module.imports {
630 if import.namespace_alias.is_some() {
632 continue;
633 }
634 let import_path = import.path.as_ref()?;
635 let imported_names: Vec<String> = match &import.selective_names {
636 Some(selective) => selective.iter().cloned().collect(),
637 None => self.exports_for_import(&file, import_path),
638 };
639 for name in imported_names {
640 if self.exported_kind_for_import(&file, import_path, &name) == Some(kind) {
641 names.insert(name);
642 }
643 }
644 }
645 Some(names)
646 }
647
648 pub fn imported_callable_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
653 let mut names = HashSet::new();
654 for kind in [
655 DefKind::Function,
656 DefKind::Pipeline,
657 DefKind::Tool,
658 DefKind::Struct,
659 ] {
660 names.extend(self.imported_names_by_kind_for_file(file, kind)?);
661 }
662 Some(names)
663 }
664
665 pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
669 let file = normalize_path(file);
670 let module = self.modules.get(&file)?;
671 if module.has_unresolved_wildcard_import
672 || module.has_unresolved_selective_import
673 || module.has_unresolved_namespace_import
674 {
675 return None;
676 }
677
678 let mut decls = Vec::new();
679 let mut seen = HashSet::new();
680 for import in &module.imports {
681 if import.namespace_alias.is_some() {
683 continue;
684 }
685 let import_path = import.path.as_ref()?;
686 let imported = self
687 .modules
688 .get(import_path)
689 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
690 if imported.load_error.is_some() {
696 return None;
697 }
698 let mut names_to_collect: Vec<String> = match &import.selective_names {
699 None => imported.exports.iter().cloned().collect(),
700 Some(selective) => selective.iter().cloned().collect(),
701 };
702 names_to_collect.sort();
703 for name in &names_to_collect {
704 let mut visited = HashSet::new();
705 if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
706 let origin = self
707 .export_definition_of(import_path, name)
708 .map_or_else(|| import_path.clone(), |definition| definition.file);
709 self.extend_type_dependency(&origin, &decl, &mut decls, &mut seen);
710 }
711 }
712 for ty_decl in &imported.type_declarations {
722 if type_decl_name(ty_decl).is_some() {
723 self.extend_type_dependency(import_path, ty_decl, &mut decls, &mut seen);
724 }
725 }
726
727 for name in &names_to_collect {
728 let mut visited = HashSet::new();
729 let Some(callable) =
730 self.find_exported_callable_decl(import_path, name, &mut visited)
731 else {
732 continue;
733 };
734 let origin = self
735 .export_definition_of(import_path, name)
736 .map_or_else(|| import_path.clone(), |definition| definition.file);
737 self.extend_callable_type_dependencies(&origin, &callable, &mut decls, &mut seen);
738 }
739 }
740 Some(decls)
741 }
742
743 pub fn imported_callable_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
745 let file = normalize_path(file);
746 let module = self.modules.get(&file)?;
747 if module.has_unresolved_wildcard_import
748 || module.has_unresolved_selective_import
749 || module.has_unresolved_namespace_import
750 {
751 return None;
752 }
753
754 let mut decls = Vec::new();
755 for import in &module.imports {
756 if import.namespace_alias.is_some() {
758 continue;
759 }
760 let import_path = import.path.as_ref()?;
761 let imported = self
762 .modules
763 .get(import_path)
764 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
765 if imported.load_error.is_some() {
771 return None;
772 }
773 let selective_import = import.selective_names.is_some();
774 let names_to_collect: Vec<String> = match &import.selective_names {
775 None => self.exports_for_import(&file, import_path),
776 Some(selective) => selective.iter().cloned().collect(),
777 };
778 for name in &names_to_collect {
779 if selective_import
780 || imported.own_exports.contains(name)
781 || (sibling_module_access(&file, import_path)
782 && imported.sibling_exports.contains(name))
783 {
784 if let Some(decl) = imported
785 .callable_declarations
786 .iter()
787 .find(|decl| callable_decl_name(decl) == Some(name.as_str()))
788 {
789 decls.push(decl.clone());
790 continue;
791 }
792 }
793 let mut visited = HashSet::new();
794 if let Some(decl) =
795 self.find_exported_callable_decl(import_path, name, &mut visited)
796 {
797 decls.push(decl);
798 }
799 }
800 }
801 Some(decls)
802 }
803
804 fn find_exported_type_decl(
807 &self,
808 path: &Path,
809 name: &str,
810 visited: &mut HashSet<PathBuf>,
811 ) -> Option<SNode> {
812 let canonical = normalize_path(path);
813 if !visited.insert(canonical.clone()) {
814 return None;
815 }
816 let module = self
817 .modules
818 .get(&canonical)
819 .or_else(|| self.modules.get(path))?;
820 for decl in &module.type_declarations {
821 if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
822 return Some(decl.clone());
823 }
824 }
825 if let Some(sources) = module.selective_re_exports.get(name) {
826 for source in sources {
827 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
828 return Some(decl);
829 }
830 }
831 }
832 for source in &module.wildcard_re_export_paths {
833 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
834 return Some(decl);
835 }
836 }
837 None
838 }
839
840 fn find_exported_callable_decl(
841 &self,
842 path: &Path,
843 name: &str,
844 visited: &mut HashSet<PathBuf>,
845 ) -> Option<SNode> {
846 let canonical = normalize_path(path);
847 if !visited.insert(canonical.clone()) {
848 return None;
849 }
850 let module = self
851 .modules
852 .get(&canonical)
853 .or_else(|| self.modules.get(path))?;
854 for decl in &module.callable_declarations {
855 if callable_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
856 return Some(decl.clone());
857 }
858 }
859 if let Some(sources) = module.selective_re_exports.get(name) {
860 for source in sources {
861 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
862 return Some(decl);
863 }
864 }
865 }
866 for source in &module.wildcard_re_export_paths {
867 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
868 return Some(decl);
869 }
870 }
871 None
872 }
873
874 pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
880 let mut visited = HashSet::new();
881 self.definition_of_inner(file, name, &mut visited)
882 }
883
884 pub fn export_definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
891 let mut visited = HashSet::new();
892 self.export_definition_of_inner(file, name, &mut visited)
893 }
894
895 pub fn declared_names_for_file(&self, file: &Path) -> Option<Vec<&str>> {
899 let module = self.modules.get(&normalize_path(file))?;
900 let mut names: Vec<&str> = module.declarations.keys().map(String::as_str).collect();
901 names.sort_unstable();
902 Some(names)
903 }
904
905 fn definition_of_inner(
906 &self,
907 file: &Path,
908 name: &str,
909 visited: &mut HashSet<PathBuf>,
910 ) -> Option<DefSite> {
911 let file = normalize_path(file);
912 if !visited.insert(file.clone()) {
913 return None;
914 }
915 let current = self.modules.get(&file)?;
916
917 if let Some(local) = current.declarations.get(name) {
918 return Some(local.clone());
919 }
920
921 if let Some(sources) = current.selective_re_exports.get(name) {
926 for source in sources {
927 if let Some(def) = self.definition_of_inner(source, name, visited) {
928 return Some(def);
929 }
930 }
931 }
932
933 for source in ¤t.wildcard_re_export_paths {
935 if let Some(def) = self.definition_of_inner(source, name, visited) {
936 return Some(def);
937 }
938 }
939
940 for import in ¤t.imports {
942 let Some(selective_names) = &import.selective_names else {
943 continue;
944 };
945 if !selective_names.contains(name) {
946 continue;
947 }
948 if let Some(path) = &import.path {
949 if let Some(def) = self.definition_of_inner(path, name, visited) {
950 return Some(def);
951 }
952 }
953 }
954
955 for import in ¤t.imports {
957 if import.selective_names.is_some() || import.namespace_alias.is_some() {
958 continue;
959 }
960 if let Some(path) = &import.path {
961 if let Some(def) = self.definition_of_inner(path, name, visited) {
962 return Some(def);
963 }
964 }
965 }
966
967 None
968 }
969
970 fn export_definition_of_inner(
971 &self,
972 file: &Path,
973 name: &str,
974 visited: &mut HashSet<PathBuf>,
975 ) -> Option<DefSite> {
976 let file = normalize_path(file);
977 if !visited.insert(file.clone()) {
978 return None;
979 }
980 let current = self.modules.get(&file)?;
981
982 if current.own_exports.contains(name) {
983 if let Some(local) = current.declarations.get(name) {
984 return Some(local.clone());
985 }
986 }
987 if let Some(sources) = current.selective_re_exports.get(name) {
988 for source in sources {
989 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
990 return Some(definition);
991 }
992 }
993 }
994 for source in ¤t.wildcard_re_export_paths {
995 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
996 return Some(definition);
997 }
998 }
999 None
1000 }
1001
1002 pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
1006 let file = normalize_path(file);
1007 let Some(module) = self.modules.get(&file) else {
1008 return Vec::new();
1009 };
1010
1011 let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
1015
1016 for (name, srcs) in &module.selective_re_exports {
1017 sources
1018 .entry(name.clone())
1019 .or_default()
1020 .extend(srcs.iter().cloned());
1021 }
1022 for src in &module.wildcard_re_export_paths {
1023 let canonical = normalize_path(src);
1024 let Some(src_module) = self
1025 .modules
1026 .get(&canonical)
1027 .or_else(|| self.modules.get(src))
1028 else {
1029 continue;
1030 };
1031 for name in &src_module.exports {
1032 sources
1033 .entry(name.clone())
1034 .or_default()
1035 .push(canonical.clone());
1036 }
1037 }
1038
1039 for name in &module.own_exports {
1043 if let Some(entry) = sources.get_mut(name) {
1044 entry.push(file.clone());
1045 }
1046 }
1047
1048 let mut conflicts = Vec::new();
1049 for (name, mut srcs) in sources {
1050 srcs.sort();
1051 srcs.dedup();
1052 if srcs.len() > 1 {
1053 conflicts.push(ReExportConflict {
1054 name,
1055 sources: srcs,
1056 });
1057 }
1058 }
1059 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
1060 conflicts
1061 }
1062
1063 pub fn selective_import_issues(&self, file: &Path) -> Vec<SelectiveImportIssue> {
1069 let file = normalize_path(file);
1070 let Some(module) = self.modules.get(&file) else {
1071 return Vec::new();
1072 };
1073
1074 let mut out = Vec::new();
1075 for import in &module.imports {
1076 let Some(selective) = &import.selective_names else {
1077 continue;
1078 };
1079 let Some(import_path) = &import.path else {
1080 continue;
1081 };
1082 let Some(target) = self
1083 .modules
1084 .get(import_path)
1085 .or_else(|| self.modules.get(&normalize_path(import_path)))
1086 else {
1087 continue;
1088 };
1089 if target.load_error.is_some() {
1090 continue;
1091 }
1092 let visible: HashSet<_> = self
1093 .exports_for_import(&file, import_path)
1094 .into_iter()
1095 .collect();
1096 for name in selective {
1097 let can_bind = visible.contains(name);
1098 let can_publish = !import.is_pub || target.exports.contains(name);
1099 let kind = if can_bind && can_publish {
1100 continue;
1101 } else if target.declarations.contains_key(name) {
1102 SelectiveImportIssueKind::Private
1103 } else {
1104 SelectiveImportIssueKind::Missing
1105 };
1106 out.push(SelectiveImportIssue {
1107 name: name.clone(),
1108 module: import.raw_path.clone(),
1109 span: import.import_span,
1110 kind,
1111 });
1112 }
1113 }
1114 out.sort_by(|a, b| (&a.name, &a.module, a.kind).cmp(&(&b.name, &b.module, b.kind)));
1115 out.dedup();
1116 out
1117 }
1118
1119 pub fn exported_kind(&self, file: &Path, name: &str) -> Option<DefKind> {
1123 self.exported_kind_inner(file, name, &mut HashSet::new())
1124 }
1125
1126 fn exported_kind_inner(
1127 &self,
1128 file: &Path,
1129 name: &str,
1130 visited: &mut HashSet<PathBuf>,
1131 ) -> Option<DefKind> {
1132 let file = normalize_path(file);
1133 if !visited.insert(file.clone()) {
1134 return None;
1135 }
1136 let result = self.modules.get(&file).and_then(|module| {
1137 if module.own_exports.contains(name) {
1138 return module
1139 .declarations
1140 .get(name)
1141 .map(|definition| definition.kind)
1142 .or_else(|| {
1143 stdlib_module_name(&file).and_then(|stdlib_module| {
1144 stdlib::builtin_reexports(stdlib_module)
1145 .contains(&name)
1146 .then_some(DefKind::Function)
1147 })
1148 });
1149 }
1150 if let Some(sources) = module.selective_re_exports.get(name) {
1151 for source in sources {
1152 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1153 return Some(kind);
1154 }
1155 }
1156 }
1157 for source in &module.wildcard_re_export_paths {
1158 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1159 return Some(kind);
1160 }
1161 }
1162 None
1163 });
1164 visited.remove(&file);
1165 result
1166 }
1167}
1168
1169#[derive(Debug, Clone, PartialEq, Eq)]
1172pub struct ReExportConflict {
1173 pub name: String,
1174 pub sources: Vec<PathBuf>,
1175}
1176
1177#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1179pub enum SelectiveImportIssueKind {
1180 Missing,
1182 Private,
1184}
1185
1186#[derive(Debug, Clone, PartialEq, Eq)]
1188pub struct SelectiveImportIssue {
1189 pub name: String,
1191 pub module: String,
1193 pub span: Span,
1195 pub kind: SelectiveImportIssueKind,
1197}
1198
1199impl SelectiveImportIssue {
1200 #[must_use]
1202 pub fn message(&self) -> String {
1203 match self.kind {
1204 SelectiveImportIssueKind::Missing => format!(
1205 "imported symbol `{}` does not exist in `{}`",
1206 self.name, self.module
1207 ),
1208 SelectiveImportIssueKind::Private => format!(
1209 "imported symbol `{}` is not exported by `{}` — it is defined there but not `pub`",
1210 self.name, self.module
1211 ),
1212 }
1213 }
1214
1215 #[must_use]
1217 pub fn help(&self) -> String {
1218 match self.kind {
1219 SelectiveImportIssueKind::Missing => format!(
1220 "update the import to a symbol exported by `{}`",
1221 self.module
1222 ),
1223 SelectiveImportIssueKind::Private => {
1224 format!(
1225 "mark `{}` as `pub` in `{}` to export it",
1226 self.name, self.module
1227 )
1228 }
1229 }
1230 }
1231}
1232
1233pub fn stdlib_module_name(path: &Path) -> Option<&str> {
1236 let s = path.to_str()?;
1237 s.strip_prefix("<std>/")
1238}
1239
1240fn normalize_path(path: &Path) -> PathBuf {
1241 canonical_path(path)
1242}
1243
1244pub fn canonical_path(path: &Path) -> PathBuf {
1257 use std::sync::OnceLock;
1258 if let Some(virtual_path) = stdlib::canonical_virtual_path(path) {
1259 return virtual_path;
1260 }
1261 static MEMO: OnceLock<std::sync::RwLock<HashMap<PathBuf, PathBuf>>> = OnceLock::new();
1264 let memo = MEMO.get_or_init(|| std::sync::RwLock::new(HashMap::new()));
1265 if let Some(hit) = memo
1266 .read()
1267 .expect("canonical path memo lock poisoned")
1268 .get(path)
1269 .cloned()
1270 {
1271 return hit;
1272 }
1273 match path.canonicalize() {
1274 Ok(canonical) => {
1275 memo.write()
1276 .expect("canonical path memo lock poisoned")
1277 .insert(path.to_path_buf(), canonical.clone());
1278 canonical
1279 }
1280 Err(_) => manifest_walk::normalize_lexically(path),
1283 }
1284}
1285
1286#[cfg(test)]
1287#[path = "tests.rs"]
1288mod tests;