1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4use crate::package_imports::acquire_package_snapshots;
5use crate::package_snapshot::PackageSnapshot;
6use harn_lexer::Span;
7use harn_parser::{Parser, SNode};
8
9pub mod asset_paths;
10mod declarations;
11pub mod fingerprint;
12mod import_recording;
13pub mod manifest_walk;
14mod namespace_imports;
15mod namespace_signatures;
16pub mod package_execution;
17mod package_imports;
18pub mod package_snapshot;
19pub mod personas;
20pub mod project_config;
21mod stdlib;
22mod symbol_reachability;
23mod type_dependencies;
24
25use declarations::{
26 callable_decl_name, collect_callable_declarations, collect_module_info,
27 collect_type_declarations, decl_site, type_decl_name,
28};
29pub use declarations::{public_declarations, DefKind, PublicDeclaration};
30pub use namespace_imports::NamespaceImportInfo;
31pub use namespace_signatures::NamespaceMemberSignature;
32pub use package_imports::{
33 resolve_import_path, resolve_import_path_with_guard, resolve_import_path_with_snapshot,
34};
35pub use symbol_reachability::{
36 closed_program_reachability, ExportDemand, ModuleSymbolDemand, SymbolReachability,
37};
38
39#[derive(Debug, Clone)]
41pub struct DefSite {
42 pub name: String,
43 pub file: PathBuf,
44 pub kind: DefKind,
45 pub span: Span,
46}
47
48#[derive(Debug, Clone)]
50pub enum WildcardResolution {
51 Resolved(HashSet<String>),
53 Unknown,
55}
56
57#[derive(Debug, Default)]
59pub struct ModuleGraph {
60 modules: HashMap<PathBuf, ModuleInfo>,
61 _package_snapshots: Vec<PackageSnapshot>,
63}
64
65#[derive(Debug, Clone)]
66pub struct ParsedModuleSource {
67 pub source: String,
68 pub program: Vec<SNode>,
69}
70
71#[derive(Debug, Default)]
72pub struct ModuleGraphBuild {
73 pub graph: ModuleGraph,
74 pub parsed_sources: HashMap<PathBuf, ParsedModuleSource>,
75}
76
77#[derive(Debug, Default)]
78struct ModuleInfo {
79 declarations: HashMap<String, DefSite>,
82 exports: HashSet<String>,
87 own_exports: HashSet<String>,
90 selective_re_exports: HashMap<String, Vec<PathBuf>>,
97 wildcard_re_export_paths: Vec<PathBuf>,
101 namespace_re_exports: HashMap<String, PathBuf>,
108 selective_import_names: HashSet<String>,
110 imports: Vec<ImportRef>,
112 has_unresolved_wildcard_import: bool,
114 has_unresolved_selective_import: bool,
118 has_unresolved_namespace_import: bool,
120 type_declarations: Vec<SNode>,
123 callable_declarations: Vec<SNode>,
126 load_error: Option<ModuleLoadError>,
132}
133
134#[derive(Debug, Clone)]
140pub struct ModuleLoadError {
141 pub message: String,
143 pub span: Span,
145}
146
147#[derive(Debug, Clone)]
150pub struct ImportCompileFailure {
151 pub import_raw_path: String,
153 pub import_span: Span,
155 pub module_path: PathBuf,
157 pub error: ModuleLoadError,
159}
160
161#[derive(Debug, Clone)]
162struct ImportRef {
163 raw_path: String,
164 path: Option<PathBuf>,
165 selective_names: Option<HashSet<String>>,
166 namespace_alias: Option<String>,
169 is_pub: bool,
170 import_span: Span,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct ModuleImport {
176 pub raw_path: String,
178 pub resolved_path: Option<PathBuf>,
180 pub selective_names: Option<Vec<String>>,
182 pub namespace_alias: Option<String>,
184 pub is_pub: bool,
186}
187
188pub fn read_module_source(path: &Path) -> Option<String> {
194 if let Some(stdlib_module) = stdlib_module_from_path(path) {
195 return stdlib::get_stdlib_source(stdlib_module).map(ToString::to_string);
196 }
197 std::fs::read_to_string(path).ok()
198}
199
200pub fn build(files: &[PathBuf]) -> ModuleGraph {
206 build_inner(files, None, false, None).graph
207}
208
209pub fn build_with_source(file: &Path, source: &str) -> ModuleGraph {
215 let file = normalize_path(file);
216 let source_overrides = HashMap::from([(file.clone(), source.to_string())]);
217 build_inner(&[file], None, false, Some(&source_overrides)).graph
218}
219
220pub fn build_with_parsed_sources(files: &[PathBuf]) -> ModuleGraphBuild {
226 let parsed_source_targets = files.iter().map(|file| normalize_path(file)).collect();
227 build_inner(files, Some(&parsed_source_targets), false, None)
228}
229
230pub fn build_closed_program(files: &[PathBuf]) -> ModuleGraphBuild {
236 build_inner(files, None, true, None)
237}
238
239fn build_inner(
240 files: &[PathBuf],
241 parsed_source_targets: Option<&HashSet<PathBuf>>,
242 retain_all_parsed_sources: bool,
243 source_overrides: Option<&HashMap<PathBuf, String>>,
244) -> ModuleGraphBuild {
245 let package_snapshots = acquire_package_snapshots(files);
246 let mut modules: HashMap<PathBuf, ModuleInfo> = HashMap::new();
247 let mut parsed_sources: HashMap<PathBuf, ParsedModuleSource> = HashMap::new();
248 let mut seen: HashSet<PathBuf> = HashSet::new();
249 let mut wave: Vec<PathBuf> = Vec::new();
250 for file in files {
251 let canonical = normalize_path(file);
252 if seen.insert(canonical.clone()) {
253 wave.push(canonical);
254 }
255 }
256 while !wave.is_empty() {
264 let loaded = load_wave(&wave, &package_snapshots, source_overrides);
265 let mut next_wave: Vec<PathBuf> = Vec::new();
266 for (path, (module, parsed)) in wave.drain(..).zip(loaded) {
267 let retain_parsed_source = retain_all_parsed_sources
268 || parsed_source_targets.is_some_and(|targets| targets.contains(&path));
269 if retain_parsed_source {
270 if let Some(parsed) = parsed {
271 parsed_sources.insert(path.clone(), parsed);
272 }
273 }
274 for import in &module.imports {
291 if let Some(import_path) = &import.path {
292 let canonical = normalize_path(import_path);
293 if seen.insert(canonical.clone()) {
294 next_wave.push(canonical);
295 }
296 }
297 }
298 modules.insert(path, module);
299 }
300 wave = next_wave;
301 }
302 resolve_re_exports(&mut modules);
303 ModuleGraphBuild {
304 graph: ModuleGraph {
305 modules,
306 _package_snapshots: package_snapshots,
307 },
308 parsed_sources,
309 }
310}
311
312pub const MODULE_GRAPH_JOBS_ENV: &str = "HARN_MODULE_GRAPH_JOBS";
315
316fn load_wave(
319 paths: &[PathBuf],
320 package_snapshots: &[PackageSnapshot],
321 source_overrides: Option<&HashMap<PathBuf, String>>,
322) -> Vec<(ModuleInfo, Option<ParsedModuleSource>)> {
323 const MIN_PARALLEL_WAVE: usize = 8;
324 let configured = std::env::var(MODULE_GRAPH_JOBS_ENV)
325 .ok()
326 .and_then(|value| value.parse::<usize>().ok())
327 .filter(|&jobs| jobs > 0);
328 let workers = configured
329 .unwrap_or_else(|| {
330 std::thread::available_parallelism()
331 .map(std::num::NonZeroUsize::get)
332 .unwrap_or(1)
333 })
334 .min(paths.len());
335 if workers <= 1 || paths.len() < MIN_PARALLEL_WAVE {
336 return paths
337 .iter()
338 .map(|path| load_module(path, package_snapshots, source_overrides))
339 .collect();
340 }
341 let next = std::sync::atomic::AtomicUsize::new(0);
342 let mut produced: Vec<(usize, (ModuleInfo, Option<ParsedModuleSource>))> =
343 std::thread::scope(|scope| {
344 let handles: Vec<_> = (0..workers)
345 .map(|_| {
346 scope.spawn(|| {
347 let mut local = Vec::new();
348 loop {
349 let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
350 let Some(path) = paths.get(index) else {
351 break;
352 };
353 local.push((
354 index,
355 load_module(path, package_snapshots, source_overrides),
356 ));
357 }
358 local
359 })
360 })
361 .collect();
362 handles
363 .into_iter()
364 .flat_map(|handle| match handle.join() {
365 Ok(local) => local,
366 Err(panic) => std::panic::resume_unwind(panic),
367 })
368 .collect()
369 });
370 produced.sort_unstable_by_key(|(index, _)| *index);
371 produced.into_iter().map(|(_, loaded)| loaded).collect()
372}
373
374fn resolve_re_exports(modules: &mut HashMap<PathBuf, ModuleInfo>) {
379 let keys: Vec<PathBuf> = modules.keys().cloned().collect();
380 loop {
381 let mut changed = false;
382 for path in &keys {
383 let wildcard_paths = modules
386 .get(path)
387 .map(|m| m.wildcard_re_export_paths.clone())
388 .unwrap_or_default();
389 if wildcard_paths.is_empty() {
390 continue;
391 }
392 let mut additions: Vec<String> = Vec::new();
393 for src in &wildcard_paths {
394 let src_canonical = normalize_path(src);
395 if let Some(src_module) = modules.get(src).or_else(|| modules.get(&src_canonical)) {
396 additions.extend(src_module.exports.iter().cloned());
397 }
398 }
399 if let Some(module) = modules.get_mut(path) {
400 for name in additions {
401 if module.exports.insert(name) {
402 changed = true;
403 }
404 }
405 }
406 }
407 if !changed {
408 break;
409 }
410 }
411}
412
413impl ModuleGraph {
414 pub fn module_paths(&self) -> Vec<PathBuf> {
419 let mut paths: Vec<PathBuf> = self.modules.keys().cloned().collect();
420 paths.sort();
421 paths
422 }
423
424 pub fn contains_module(&self, path: &Path) -> bool {
427 self.modules.contains_key(path) || self.modules.contains_key(&normalize_path(path))
428 }
429
430 pub fn all_selective_import_names(&self) -> HashSet<&str> {
432 let mut names = HashSet::new();
433 for module in self.modules.values() {
434 for name in &module.selective_import_names {
435 names.insert(name.as_str());
436 }
437 }
438 names
439 }
440
441 pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
444 let target = normalize_path(target);
445 let mut out: Vec<PathBuf> = self
446 .modules
447 .iter()
448 .filter(|(_, info)| {
449 info.imports.iter().any(|import| {
450 import
451 .path
452 .as_ref()
453 .is_some_and(|p| normalize_path(p) == target)
454 })
455 })
456 .map(|(path, _)| path.clone())
457 .collect();
458 out.sort();
459 out
460 }
461
462 pub fn imports_for_module(&self, file: &Path) -> Vec<ModuleImport> {
464 let file = normalize_path(file);
465 let Some(module) = self.modules.get(&file) else {
466 return Vec::new();
467 };
468 let mut imports: Vec<ModuleImport> = module
469 .imports
470 .iter()
471 .map(|import| {
472 let mut selective_names = import
473 .selective_names
474 .as_ref()
475 .map(|names| names.iter().cloned().collect::<Vec<_>>());
476 if let Some(names) = selective_names.as_mut() {
477 names.sort();
478 }
479 ModuleImport {
480 raw_path: import.raw_path.clone(),
481 resolved_path: import.path.as_ref().map(|path| normalize_path(path)),
482 selective_names,
483 namespace_alias: import.namespace_alias.clone(),
484 is_pub: import.is_pub,
485 }
486 })
487 .collect();
488 imports.sort_by(|left, right| {
489 left.raw_path
490 .cmp(&right.raw_path)
491 .then_with(|| left.selective_names.cmp(&right.selective_names))
492 .then_with(|| left.namespace_alias.cmp(&right.namespace_alias))
493 .then_with(|| left.resolved_path.cmp(&right.resolved_path))
494 });
495 imports
496 }
497
498 pub fn exports_for_module(&self, file: &Path) -> Vec<String> {
500 let file = normalize_path(file);
501 let Some(module) = self.modules.get(&file) else {
502 return Vec::new();
503 };
504 let mut exports: Vec<String> = module.exports.iter().cloned().collect();
505 exports.sort();
506 exports
507 }
508
509 pub fn wildcard_exports_for(&self, file: &Path) -> WildcardResolution {
514 let file = normalize_path(file);
515 let Some(module) = self.modules.get(&file) else {
516 return WildcardResolution::Unknown;
517 };
518 if module.has_unresolved_wildcard_import {
519 return WildcardResolution::Unknown;
520 }
521
522 let mut names = HashSet::new();
523 for import in module
524 .imports
525 .iter()
526 .filter(|import| import.selective_names.is_none())
527 {
528 let Some(import_path) = &import.path else {
529 return WildcardResolution::Unknown;
530 };
531 let imported = self.modules.get(import_path).or_else(|| {
532 let normalized = normalize_path(import_path);
533 self.modules.get(&normalized)
534 });
535 let Some(imported) = imported else {
536 return WildcardResolution::Unknown;
537 };
538 names.extend(imported.exports.iter().cloned());
539 }
540 WildcardResolution::Resolved(names)
541 }
542
543 #[must_use]
564 pub fn import_compile_failures(&self, file: &Path) -> Vec<ImportCompileFailure> {
565 let file = normalize_path(file);
566 let Some(module) = self.modules.get(&file) else {
567 return Vec::new();
568 };
569 let mut failures = Vec::new();
570 for import in &module.imports {
571 let Some(import_path) = &import.path else {
572 continue;
573 };
574 let Some(target) = self
575 .modules
576 .get(import_path)
577 .or_else(|| self.modules.get(&normalize_path(import_path)))
578 else {
579 continue;
580 };
581 if let Some(error) = &target.load_error {
582 failures.push(ImportCompileFailure {
583 import_raw_path: import.raw_path.clone(),
584 import_span: import.import_span,
585 module_path: normalize_path(import_path),
586 error: error.clone(),
587 });
588 }
589 }
590 failures
591 }
592
593 pub fn imported_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
594 let file = normalize_path(file);
595 let module = self.modules.get(&file)?;
596 if module.has_unresolved_wildcard_import
597 || module.has_unresolved_selective_import
598 || module.has_unresolved_namespace_import
599 {
600 return None;
601 }
602
603 let mut names = HashSet::new();
604 for import in &module.imports {
605 if let Some(alias) = &import.namespace_alias {
607 names.insert(alias.clone());
608 continue;
609 }
610 let import_path = import.path.as_ref()?;
611 let imported = self
612 .modules
613 .get(import_path)
614 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
615 if imported.load_error.is_some() {
621 return None;
622 }
623 match &import.selective_names {
624 None => {
625 names.extend(imported.exports.iter().cloned());
626 }
627 Some(selective) => {
628 for name in selective {
637 if imported.declarations.contains_key(name)
638 || imported.exports.contains(name)
639 {
640 names.insert(name.clone());
641 }
642 }
643 }
644 }
645 }
646 Some(names)
647 }
648
649 pub fn imported_names_by_kind_for_file(
654 &self,
655 file: &Path,
656 kind: DefKind,
657 ) -> Option<HashSet<String>> {
658 let file = normalize_path(file);
659 let module = self.modules.get(&file)?;
660 if module.has_unresolved_wildcard_import
661 || module.has_unresolved_selective_import
662 || module.has_unresolved_namespace_import
663 {
664 return None;
665 }
666
667 let mut names = HashSet::new();
668 for import in &module.imports {
669 if import.namespace_alias.is_some() {
671 continue;
672 }
673 let import_path = import.path.as_ref()?;
674 let imported_names: Vec<String> = match &import.selective_names {
675 Some(selective) => selective.iter().cloned().collect(),
676 None => self
677 .modules
678 .get(import_path)
679 .or_else(|| self.modules.get(&normalize_path(import_path)))?
680 .exports
681 .iter()
682 .cloned()
683 .collect(),
684 };
685 for name in imported_names {
686 if self.exported_kind(import_path, &name) == Some(kind) {
687 names.insert(name);
688 }
689 }
690 }
691 Some(names)
692 }
693
694 pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
698 let file = normalize_path(file);
699 let module = self.modules.get(&file)?;
700 if module.has_unresolved_wildcard_import
701 || module.has_unresolved_selective_import
702 || module.has_unresolved_namespace_import
703 {
704 return None;
705 }
706
707 let mut decls = Vec::new();
708 let mut seen = HashSet::new();
709 for import in &module.imports {
710 if import.namespace_alias.is_some() {
712 continue;
713 }
714 let import_path = import.path.as_ref()?;
715 let imported = self
716 .modules
717 .get(import_path)
718 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
719 if imported.load_error.is_some() {
725 return None;
726 }
727 let mut names_to_collect: Vec<String> = match &import.selective_names {
728 None => imported.exports.iter().cloned().collect(),
729 Some(selective) => selective.iter().cloned().collect(),
730 };
731 names_to_collect.sort();
732 for name in &names_to_collect {
733 let mut visited = HashSet::new();
734 if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
735 let origin = self
736 .export_definition_of(import_path, name)
737 .map_or_else(|| import_path.clone(), |definition| definition.file);
738 self.extend_type_dependency(&origin, &decl, &mut decls, &mut seen);
739 }
740 }
741 for ty_decl in &imported.type_declarations {
751 if type_decl_name(ty_decl).is_some() {
752 self.extend_type_dependency(import_path, ty_decl, &mut decls, &mut seen);
753 }
754 }
755
756 for name in &names_to_collect {
757 let mut visited = HashSet::new();
758 let Some(callable) =
759 self.find_exported_callable_decl(import_path, name, &mut visited)
760 else {
761 continue;
762 };
763 let origin = self
764 .export_definition_of(import_path, name)
765 .map_or_else(|| import_path.clone(), |definition| definition.file);
766 self.extend_callable_type_dependencies(&origin, &callable, &mut decls, &mut seen);
767 }
768 }
769 Some(decls)
770 }
771
772 pub fn imported_callable_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
776 let file = normalize_path(file);
777 let module = self.modules.get(&file)?;
778 if module.has_unresolved_wildcard_import
779 || module.has_unresolved_selective_import
780 || module.has_unresolved_namespace_import
781 {
782 return None;
783 }
784
785 let mut decls = Vec::new();
786 for import in &module.imports {
787 if import.namespace_alias.is_some() {
789 continue;
790 }
791 let import_path = import.path.as_ref()?;
792 let imported = self
793 .modules
794 .get(import_path)
795 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
796 if imported.load_error.is_some() {
802 return None;
803 }
804 let selective_import = import.selective_names.is_some();
805 let names_to_collect: Vec<String> = match &import.selective_names {
806 None => imported.exports.iter().cloned().collect(),
807 Some(selective) => selective.iter().cloned().collect(),
808 };
809 for name in &names_to_collect {
810 if selective_import || imported.own_exports.contains(name) {
811 if let Some(decl) = imported
812 .callable_declarations
813 .iter()
814 .find(|decl| callable_decl_name(decl) == Some(name.as_str()))
815 {
816 decls.push(decl.clone());
817 continue;
818 }
819 }
820 let mut visited = HashSet::new();
821 if let Some(decl) =
822 self.find_exported_callable_decl(import_path, name, &mut visited)
823 {
824 decls.push(decl);
825 }
826 }
827 }
828 Some(decls)
829 }
830
831 fn find_exported_type_decl(
834 &self,
835 path: &Path,
836 name: &str,
837 visited: &mut HashSet<PathBuf>,
838 ) -> Option<SNode> {
839 let canonical = normalize_path(path);
840 if !visited.insert(canonical.clone()) {
841 return None;
842 }
843 let module = self
844 .modules
845 .get(&canonical)
846 .or_else(|| self.modules.get(path))?;
847 for decl in &module.type_declarations {
848 if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
849 return Some(decl.clone());
850 }
851 }
852 if let Some(sources) = module.selective_re_exports.get(name) {
853 for source in sources {
854 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
855 return Some(decl);
856 }
857 }
858 }
859 for source in &module.wildcard_re_export_paths {
860 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
861 return Some(decl);
862 }
863 }
864 None
865 }
866
867 fn find_exported_callable_decl(
868 &self,
869 path: &Path,
870 name: &str,
871 visited: &mut HashSet<PathBuf>,
872 ) -> Option<SNode> {
873 let canonical = normalize_path(path);
874 if !visited.insert(canonical.clone()) {
875 return None;
876 }
877 let module = self
878 .modules
879 .get(&canonical)
880 .or_else(|| self.modules.get(path))?;
881 for decl in &module.callable_declarations {
882 if callable_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
883 return Some(decl.clone());
884 }
885 }
886 if let Some(sources) = module.selective_re_exports.get(name) {
887 for source in sources {
888 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
889 return Some(decl);
890 }
891 }
892 }
893 for source in &module.wildcard_re_export_paths {
894 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
895 return Some(decl);
896 }
897 }
898 None
899 }
900
901 pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
907 let mut visited = HashSet::new();
908 self.definition_of_inner(file, name, &mut visited)
909 }
910
911 pub fn export_definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
918 let mut visited = HashSet::new();
919 self.export_definition_of_inner(file, name, &mut visited)
920 }
921
922 pub fn declared_names_for_file(&self, file: &Path) -> Option<Vec<&str>> {
926 let module = self.modules.get(&normalize_path(file))?;
927 let mut names: Vec<&str> = module.declarations.keys().map(String::as_str).collect();
928 names.sort_unstable();
929 Some(names)
930 }
931
932 fn definition_of_inner(
933 &self,
934 file: &Path,
935 name: &str,
936 visited: &mut HashSet<PathBuf>,
937 ) -> Option<DefSite> {
938 let file = normalize_path(file);
939 if !visited.insert(file.clone()) {
940 return None;
941 }
942 let current = self.modules.get(&file)?;
943
944 if let Some(local) = current.declarations.get(name) {
945 return Some(local.clone());
946 }
947
948 if let Some(sources) = current.selective_re_exports.get(name) {
953 for source in sources {
954 if let Some(def) = self.definition_of_inner(source, name, visited) {
955 return Some(def);
956 }
957 }
958 }
959
960 for source in ¤t.wildcard_re_export_paths {
962 if let Some(def) = self.definition_of_inner(source, name, visited) {
963 return Some(def);
964 }
965 }
966
967 for import in ¤t.imports {
969 let Some(selective_names) = &import.selective_names else {
970 continue;
971 };
972 if !selective_names.contains(name) {
973 continue;
974 }
975 if let Some(path) = &import.path {
976 if let Some(def) = self.definition_of_inner(path, name, visited) {
977 return Some(def);
978 }
979 }
980 }
981
982 for import in ¤t.imports {
984 if import.selective_names.is_some() || import.namespace_alias.is_some() {
985 continue;
986 }
987 if let Some(path) = &import.path {
988 if let Some(def) = self.definition_of_inner(path, name, visited) {
989 return Some(def);
990 }
991 }
992 }
993
994 None
995 }
996
997 fn export_definition_of_inner(
998 &self,
999 file: &Path,
1000 name: &str,
1001 visited: &mut HashSet<PathBuf>,
1002 ) -> Option<DefSite> {
1003 let file = normalize_path(file);
1004 if !visited.insert(file.clone()) {
1005 return None;
1006 }
1007 let current = self.modules.get(&file)?;
1008
1009 if current.own_exports.contains(name) {
1010 if let Some(local) = current.declarations.get(name) {
1011 return Some(local.clone());
1012 }
1013 }
1014 if let Some(sources) = current.selective_re_exports.get(name) {
1015 for source in sources {
1016 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1017 return Some(definition);
1018 }
1019 }
1020 }
1021 for source in ¤t.wildcard_re_export_paths {
1022 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1023 return Some(definition);
1024 }
1025 }
1026 None
1027 }
1028
1029 pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
1033 let file = normalize_path(file);
1034 let Some(module) = self.modules.get(&file) else {
1035 return Vec::new();
1036 };
1037
1038 let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
1042
1043 for (name, srcs) in &module.selective_re_exports {
1044 sources
1045 .entry(name.clone())
1046 .or_default()
1047 .extend(srcs.iter().cloned());
1048 }
1049 for src in &module.wildcard_re_export_paths {
1050 let canonical = normalize_path(src);
1051 let Some(src_module) = self
1052 .modules
1053 .get(&canonical)
1054 .or_else(|| self.modules.get(src))
1055 else {
1056 continue;
1057 };
1058 for name in &src_module.exports {
1059 sources
1060 .entry(name.clone())
1061 .or_default()
1062 .push(canonical.clone());
1063 }
1064 }
1065
1066 for name in &module.own_exports {
1070 if let Some(entry) = sources.get_mut(name) {
1071 entry.push(file.clone());
1072 }
1073 }
1074
1075 let mut conflicts = Vec::new();
1076 for (name, mut srcs) in sources {
1077 srcs.sort();
1078 srcs.dedup();
1079 if srcs.len() > 1 {
1080 conflicts.push(ReExportConflict {
1081 name,
1082 sources: srcs,
1083 });
1084 }
1085 }
1086 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
1087 conflicts
1088 }
1089
1090 pub fn selective_import_issues(&self, file: &Path) -> Vec<SelectiveImportIssue> {
1096 let file = normalize_path(file);
1097 let Some(module) = self.modules.get(&file) else {
1098 return Vec::new();
1099 };
1100
1101 let mut out = Vec::new();
1102 for import in &module.imports {
1103 let Some(selective) = &import.selective_names else {
1104 continue;
1105 };
1106 let Some(import_path) = &import.path else {
1107 continue;
1108 };
1109 let Some(target) = self
1110 .modules
1111 .get(import_path)
1112 .or_else(|| self.modules.get(&normalize_path(import_path)))
1113 else {
1114 continue;
1115 };
1116 if target.load_error.is_some() {
1117 continue;
1118 }
1119 for name in selective {
1120 let kind = if target.exports.contains(name) {
1121 continue;
1122 } else if target.declarations.contains_key(name) {
1123 SelectiveImportIssueKind::Private
1124 } else {
1125 SelectiveImportIssueKind::Missing
1126 };
1127 out.push(SelectiveImportIssue {
1128 name: name.clone(),
1129 module: import.raw_path.clone(),
1130 span: import.import_span,
1131 kind,
1132 });
1133 }
1134 }
1135 out.sort_by(|a, b| (&a.name, &a.module, a.kind).cmp(&(&b.name, &b.module, b.kind)));
1136 out.dedup();
1137 out
1138 }
1139
1140 pub fn exported_kind(&self, file: &Path, name: &str) -> Option<DefKind> {
1144 self.exported_kind_inner(file, name, &mut HashSet::new())
1145 }
1146
1147 fn exported_kind_inner(
1148 &self,
1149 file: &Path,
1150 name: &str,
1151 visited: &mut HashSet<PathBuf>,
1152 ) -> Option<DefKind> {
1153 let file = normalize_path(file);
1154 if !visited.insert(file.clone()) {
1155 return None;
1156 }
1157 let result = self.modules.get(&file).and_then(|module| {
1158 if module.own_exports.contains(name) {
1159 return module
1160 .declarations
1161 .get(name)
1162 .map(|definition| definition.kind)
1163 .or_else(|| {
1164 stdlib_module_from_path(&file).and_then(|stdlib_module| {
1165 stdlib::builtin_reexports(stdlib_module)
1166 .contains(&name)
1167 .then_some(DefKind::Function)
1168 })
1169 });
1170 }
1171 if let Some(sources) = module.selective_re_exports.get(name) {
1172 for source in sources {
1173 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1174 return Some(kind);
1175 }
1176 }
1177 }
1178 for source in &module.wildcard_re_export_paths {
1179 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1180 return Some(kind);
1181 }
1182 }
1183 None
1184 });
1185 visited.remove(&file);
1186 result
1187 }
1188}
1189
1190#[derive(Debug, Clone, PartialEq, Eq)]
1193pub struct ReExportConflict {
1194 pub name: String,
1195 pub sources: Vec<PathBuf>,
1196}
1197
1198#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1200pub enum SelectiveImportIssueKind {
1201 Missing,
1203 Private,
1205}
1206
1207#[derive(Debug, Clone, PartialEq, Eq)]
1209pub struct SelectiveImportIssue {
1210 pub name: String,
1212 pub module: String,
1214 pub span: Span,
1216 pub kind: SelectiveImportIssueKind,
1218}
1219
1220impl SelectiveImportIssue {
1221 #[must_use]
1223 pub fn message(&self) -> String {
1224 match self.kind {
1225 SelectiveImportIssueKind::Missing => format!(
1226 "imported symbol `{}` does not exist in `{}`",
1227 self.name, self.module
1228 ),
1229 SelectiveImportIssueKind::Private => format!(
1230 "imported symbol `{}` is not exported by `{}` — it is defined there but not `pub`",
1231 self.name, self.module
1232 ),
1233 }
1234 }
1235
1236 #[must_use]
1238 pub fn help(&self) -> String {
1239 match self.kind {
1240 SelectiveImportIssueKind::Missing => format!(
1241 "update the import to a symbol exported by `{}`",
1242 self.module
1243 ),
1244 SelectiveImportIssueKind::Private => {
1245 format!(
1246 "mark `{}` as `pub` in `{}` to export it",
1247 self.name, self.module
1248 )
1249 }
1250 }
1251 }
1252}
1253
1254fn load_module(
1255 path: &Path,
1256 package_snapshots: &[PackageSnapshot],
1257 source_overrides: Option<&HashMap<PathBuf, String>>,
1258) -> (ModuleInfo, Option<ParsedModuleSource>) {
1259 let source = source_overrides
1260 .and_then(|overrides| overrides.get(&normalize_path(path)).cloned())
1261 .or_else(|| read_module_source(path));
1262 let Some(source) = source else {
1263 return (ModuleInfo::default(), None);
1264 };
1265 let mut lexer = harn_lexer::Lexer::new(&source);
1266 let tokens = match lexer.tokenize() {
1267 Ok(tokens) => tokens,
1268 Err(error) => {
1269 let module = ModuleInfo {
1270 load_error: Some(ModuleLoadError {
1271 message: error.to_string(),
1272 span: error.span(),
1273 }),
1274 ..ModuleInfo::default()
1275 };
1276 return (module, None);
1277 }
1278 };
1279 let mut parser = Parser::new(tokens);
1280 let program = match parser.parse() {
1281 Ok(program) => program,
1282 Err(error) => {
1283 let module = ModuleInfo {
1284 load_error: Some(ModuleLoadError {
1285 message: error.to_string(),
1286 span: error.span(),
1287 }),
1288 ..ModuleInfo::default()
1289 };
1290 return (module, None);
1291 }
1292 };
1293
1294 let mut module = ModuleInfo::default();
1295 for node in &program {
1296 collect_module_info(path, node, &mut module, package_snapshots);
1297 collect_type_declarations(node, &mut module.type_declarations);
1298 collect_callable_declarations(node, &mut module.callable_declarations);
1299 }
1300 if let Some(stdlib_module) = stdlib_module_from_path(path) {
1301 module.own_exports.extend(
1302 stdlib::builtin_reexports(stdlib_module)
1303 .iter()
1304 .map(|name| (*name).to_string()),
1305 );
1306 }
1307 module.exports.extend(module.own_exports.iter().cloned());
1311 module
1312 .exports
1313 .extend(module.selective_re_exports.keys().cloned());
1314 let parsed = ParsedModuleSource { source, program };
1315 (module, Some(parsed))
1316}
1317
1318fn stdlib_module_from_path(path: &Path) -> Option<&str> {
1321 let s = path.to_str()?;
1322 s.strip_prefix("<std>/")
1323}
1324
1325fn normalize_path(path: &Path) -> PathBuf {
1326 canonical_path(path)
1327}
1328
1329pub fn canonical_path(path: &Path) -> PathBuf {
1342 use std::sync::OnceLock;
1343 if stdlib_module_from_path(path).is_some() {
1344 return path.to_path_buf();
1345 }
1346 static MEMO: OnceLock<std::sync::Mutex<HashMap<PathBuf, PathBuf>>> = OnceLock::new();
1347 let memo = MEMO.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
1348 if let Some(hit) = memo
1349 .lock()
1350 .expect("canonical path memo lock poisoned")
1351 .get(path)
1352 .cloned()
1353 {
1354 return hit;
1355 }
1356 match path.canonicalize() {
1357 Ok(canonical) => {
1358 memo.lock()
1359 .expect("canonical path memo lock poisoned")
1360 .insert(path.to_path_buf(), canonical.clone());
1361 canonical
1362 }
1363 Err(_) => path.to_path_buf(),
1364 }
1365}
1366
1367#[cfg(test)]
1368#[path = "tests.rs"]
1369mod tests;