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 transitive_importers_of(&self, target: &Path) -> Vec<PathBuf> {
469 let target = normalize_path(target);
470 let mut visited = HashSet::from([target.clone()]);
471 let mut pending = vec![target];
472 let mut out = Vec::new();
473
474 while let Some(current) = pending.pop() {
475 for importer in self.importers_of(¤t) {
476 let importer = normalize_path(&importer);
477 if visited.insert(importer.clone()) {
478 pending.push(importer.clone());
479 out.push(importer);
480 }
481 }
482 }
483
484 out.sort();
485 out
486 }
487
488 pub fn imports_for_module(&self, file: &Path) -> Vec<ModuleImport> {
490 let file = normalize_path(file);
491 let Some(module) = self.modules.get(&file) else {
492 return Vec::new();
493 };
494 let mut imports: Vec<ModuleImport> = module
495 .imports
496 .iter()
497 .map(|import| {
498 let mut selective_names = import
499 .selective_names
500 .as_ref()
501 .map(|names| names.iter().cloned().collect::<Vec<_>>());
502 if let Some(names) = selective_names.as_mut() {
503 names.sort();
504 }
505 ModuleImport {
506 raw_path: import.raw_path.clone(),
507 resolved_path: import.path.as_ref().map(|path| normalize_path(path)),
508 selective_names,
509 namespace_alias: import.namespace_alias.clone(),
510 is_pub: import.is_pub,
511 }
512 })
513 .collect();
514 imports.sort_by(|left, right| {
515 left.raw_path
516 .cmp(&right.raw_path)
517 .then_with(|| left.selective_names.cmp(&right.selective_names))
518 .then_with(|| left.namespace_alias.cmp(&right.namespace_alias))
519 .then_with(|| left.resolved_path.cmp(&right.resolved_path))
520 });
521 imports
522 }
523
524 pub fn exports_for_module(&self, file: &Path) -> Vec<String> {
526 let file = normalize_path(file);
527 let Some(module) = self.modules.get(&file) else {
528 return Vec::new();
529 };
530 let mut exports: Vec<String> = module.exports.iter().cloned().collect();
531 exports.sort();
532 exports
533 }
534
535 pub fn wildcard_exports_for(&self, file: &Path) -> WildcardResolution {
540 let file = normalize_path(file);
541 let Some(module) = self.modules.get(&file) else {
542 return WildcardResolution::Unknown;
543 };
544 if module.has_unresolved_wildcard_import {
545 return WildcardResolution::Unknown;
546 }
547
548 let mut names = HashSet::new();
549 for import in module
550 .imports
551 .iter()
552 .filter(|import| import.selective_names.is_none())
553 {
554 let Some(import_path) = &import.path else {
555 return WildcardResolution::Unknown;
556 };
557 let imported = self.modules.get(import_path).or_else(|| {
558 let normalized = normalize_path(import_path);
559 self.modules.get(&normalized)
560 });
561 let Some(imported) = imported else {
562 return WildcardResolution::Unknown;
563 };
564 names.extend(imported.exports.iter().cloned());
565 }
566 WildcardResolution::Resolved(names)
567 }
568
569 #[must_use]
590 pub fn import_compile_failures(&self, file: &Path) -> Vec<ImportCompileFailure> {
591 let file = normalize_path(file);
592 let Some(module) = self.modules.get(&file) else {
593 return Vec::new();
594 };
595 let mut failures = Vec::new();
596 for import in &module.imports {
597 let Some(import_path) = &import.path else {
598 continue;
599 };
600 let Some(target) = self
601 .modules
602 .get(import_path)
603 .or_else(|| self.modules.get(&normalize_path(import_path)))
604 else {
605 continue;
606 };
607 if let Some(error) = &target.load_error {
608 failures.push(ImportCompileFailure {
609 import_raw_path: import.raw_path.clone(),
610 import_span: import.import_span,
611 module_path: normalize_path(import_path),
612 error: error.clone(),
613 });
614 }
615 }
616 failures
617 }
618
619 pub fn imported_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
620 let file = normalize_path(file);
621 let module = self.modules.get(&file)?;
622 if module.has_unresolved_wildcard_import
623 || module.has_unresolved_selective_import
624 || module.has_unresolved_namespace_import
625 {
626 return None;
627 }
628
629 let mut names = HashSet::new();
630 for import in &module.imports {
631 if let Some(alias) = &import.namespace_alias {
633 names.insert(alias.clone());
634 continue;
635 }
636 let import_path = import.path.as_ref()?;
637 let imported = self
638 .modules
639 .get(import_path)
640 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
641 if imported.load_error.is_some() {
647 return None;
648 }
649 match &import.selective_names {
650 None => {
651 names.extend(imported.exports.iter().cloned());
652 }
653 Some(selective) => {
654 for name in selective {
663 if imported.declarations.contains_key(name)
664 || imported.exports.contains(name)
665 {
666 names.insert(name.clone());
667 }
668 }
669 }
670 }
671 }
672 Some(names)
673 }
674
675 pub fn imported_names_by_kind_for_file(
680 &self,
681 file: &Path,
682 kind: DefKind,
683 ) -> Option<HashSet<String>> {
684 let file = normalize_path(file);
685 let module = self.modules.get(&file)?;
686 if module.has_unresolved_wildcard_import
687 || module.has_unresolved_selective_import
688 || module.has_unresolved_namespace_import
689 {
690 return None;
691 }
692
693 let mut names = HashSet::new();
694 for import in &module.imports {
695 if import.namespace_alias.is_some() {
697 continue;
698 }
699 let import_path = import.path.as_ref()?;
700 let imported_names: Vec<String> = match &import.selective_names {
701 Some(selective) => selective.iter().cloned().collect(),
702 None => self
703 .modules
704 .get(import_path)
705 .or_else(|| self.modules.get(&normalize_path(import_path)))?
706 .exports
707 .iter()
708 .cloned()
709 .collect(),
710 };
711 for name in imported_names {
712 if self.exported_kind(import_path, &name) == Some(kind) {
713 names.insert(name);
714 }
715 }
716 }
717 Some(names)
718 }
719
720 pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
724 let file = normalize_path(file);
725 let module = self.modules.get(&file)?;
726 if module.has_unresolved_wildcard_import
727 || module.has_unresolved_selective_import
728 || module.has_unresolved_namespace_import
729 {
730 return None;
731 }
732
733 let mut decls = Vec::new();
734 let mut seen = HashSet::new();
735 for import in &module.imports {
736 if import.namespace_alias.is_some() {
738 continue;
739 }
740 let import_path = import.path.as_ref()?;
741 let imported = self
742 .modules
743 .get(import_path)
744 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
745 if imported.load_error.is_some() {
751 return None;
752 }
753 let mut names_to_collect: Vec<String> = match &import.selective_names {
754 None => imported.exports.iter().cloned().collect(),
755 Some(selective) => selective.iter().cloned().collect(),
756 };
757 names_to_collect.sort();
758 for name in &names_to_collect {
759 let mut visited = HashSet::new();
760 if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
761 let origin = self
762 .export_definition_of(import_path, name)
763 .map_or_else(|| import_path.clone(), |definition| definition.file);
764 self.extend_type_dependency(&origin, &decl, &mut decls, &mut seen);
765 }
766 }
767 for ty_decl in &imported.type_declarations {
777 if type_decl_name(ty_decl).is_some() {
778 self.extend_type_dependency(import_path, ty_decl, &mut decls, &mut seen);
779 }
780 }
781
782 for name in &names_to_collect {
783 let mut visited = HashSet::new();
784 let Some(callable) =
785 self.find_exported_callable_decl(import_path, name, &mut visited)
786 else {
787 continue;
788 };
789 let origin = self
790 .export_definition_of(import_path, name)
791 .map_or_else(|| import_path.clone(), |definition| definition.file);
792 self.extend_callable_type_dependencies(&origin, &callable, &mut decls, &mut seen);
793 }
794 }
795 Some(decls)
796 }
797
798 pub fn imported_callable_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
802 let file = normalize_path(file);
803 let module = self.modules.get(&file)?;
804 if module.has_unresolved_wildcard_import
805 || module.has_unresolved_selective_import
806 || module.has_unresolved_namespace_import
807 {
808 return None;
809 }
810
811 let mut decls = Vec::new();
812 for import in &module.imports {
813 if import.namespace_alias.is_some() {
815 continue;
816 }
817 let import_path = import.path.as_ref()?;
818 let imported = self
819 .modules
820 .get(import_path)
821 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
822 if imported.load_error.is_some() {
828 return None;
829 }
830 let selective_import = import.selective_names.is_some();
831 let names_to_collect: Vec<String> = match &import.selective_names {
832 None => imported.exports.iter().cloned().collect(),
833 Some(selective) => selective.iter().cloned().collect(),
834 };
835 for name in &names_to_collect {
836 if selective_import || imported.own_exports.contains(name) {
837 if let Some(decl) = imported
838 .callable_declarations
839 .iter()
840 .find(|decl| callable_decl_name(decl) == Some(name.as_str()))
841 {
842 decls.push(decl.clone());
843 continue;
844 }
845 }
846 let mut visited = HashSet::new();
847 if let Some(decl) =
848 self.find_exported_callable_decl(import_path, name, &mut visited)
849 {
850 decls.push(decl);
851 }
852 }
853 }
854 Some(decls)
855 }
856
857 fn find_exported_type_decl(
860 &self,
861 path: &Path,
862 name: &str,
863 visited: &mut HashSet<PathBuf>,
864 ) -> Option<SNode> {
865 let canonical = normalize_path(path);
866 if !visited.insert(canonical.clone()) {
867 return None;
868 }
869 let module = self
870 .modules
871 .get(&canonical)
872 .or_else(|| self.modules.get(path))?;
873 for decl in &module.type_declarations {
874 if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
875 return Some(decl.clone());
876 }
877 }
878 if let Some(sources) = module.selective_re_exports.get(name) {
879 for source in sources {
880 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
881 return Some(decl);
882 }
883 }
884 }
885 for source in &module.wildcard_re_export_paths {
886 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
887 return Some(decl);
888 }
889 }
890 None
891 }
892
893 fn find_exported_callable_decl(
894 &self,
895 path: &Path,
896 name: &str,
897 visited: &mut HashSet<PathBuf>,
898 ) -> Option<SNode> {
899 let canonical = normalize_path(path);
900 if !visited.insert(canonical.clone()) {
901 return None;
902 }
903 let module = self
904 .modules
905 .get(&canonical)
906 .or_else(|| self.modules.get(path))?;
907 for decl in &module.callable_declarations {
908 if callable_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
909 return Some(decl.clone());
910 }
911 }
912 if let Some(sources) = module.selective_re_exports.get(name) {
913 for source in sources {
914 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
915 return Some(decl);
916 }
917 }
918 }
919 for source in &module.wildcard_re_export_paths {
920 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
921 return Some(decl);
922 }
923 }
924 None
925 }
926
927 pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
933 let mut visited = HashSet::new();
934 self.definition_of_inner(file, name, &mut visited)
935 }
936
937 pub fn export_definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
944 let mut visited = HashSet::new();
945 self.export_definition_of_inner(file, name, &mut visited)
946 }
947
948 pub fn declared_names_for_file(&self, file: &Path) -> Option<Vec<&str>> {
952 let module = self.modules.get(&normalize_path(file))?;
953 let mut names: Vec<&str> = module.declarations.keys().map(String::as_str).collect();
954 names.sort_unstable();
955 Some(names)
956 }
957
958 fn definition_of_inner(
959 &self,
960 file: &Path,
961 name: &str,
962 visited: &mut HashSet<PathBuf>,
963 ) -> Option<DefSite> {
964 let file = normalize_path(file);
965 if !visited.insert(file.clone()) {
966 return None;
967 }
968 let current = self.modules.get(&file)?;
969
970 if let Some(local) = current.declarations.get(name) {
971 return Some(local.clone());
972 }
973
974 if let Some(sources) = current.selective_re_exports.get(name) {
979 for source in sources {
980 if let Some(def) = self.definition_of_inner(source, name, visited) {
981 return Some(def);
982 }
983 }
984 }
985
986 for source in ¤t.wildcard_re_export_paths {
988 if let Some(def) = self.definition_of_inner(source, name, visited) {
989 return Some(def);
990 }
991 }
992
993 for import in ¤t.imports {
995 let Some(selective_names) = &import.selective_names else {
996 continue;
997 };
998 if !selective_names.contains(name) {
999 continue;
1000 }
1001 if let Some(path) = &import.path {
1002 if let Some(def) = self.definition_of_inner(path, name, visited) {
1003 return Some(def);
1004 }
1005 }
1006 }
1007
1008 for import in ¤t.imports {
1010 if import.selective_names.is_some() || import.namespace_alias.is_some() {
1011 continue;
1012 }
1013 if let Some(path) = &import.path {
1014 if let Some(def) = self.definition_of_inner(path, name, visited) {
1015 return Some(def);
1016 }
1017 }
1018 }
1019
1020 None
1021 }
1022
1023 fn export_definition_of_inner(
1024 &self,
1025 file: &Path,
1026 name: &str,
1027 visited: &mut HashSet<PathBuf>,
1028 ) -> Option<DefSite> {
1029 let file = normalize_path(file);
1030 if !visited.insert(file.clone()) {
1031 return None;
1032 }
1033 let current = self.modules.get(&file)?;
1034
1035 if current.own_exports.contains(name) {
1036 if let Some(local) = current.declarations.get(name) {
1037 return Some(local.clone());
1038 }
1039 }
1040 if let Some(sources) = current.selective_re_exports.get(name) {
1041 for source in sources {
1042 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1043 return Some(definition);
1044 }
1045 }
1046 }
1047 for source in ¤t.wildcard_re_export_paths {
1048 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1049 return Some(definition);
1050 }
1051 }
1052 None
1053 }
1054
1055 pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
1059 let file = normalize_path(file);
1060 let Some(module) = self.modules.get(&file) else {
1061 return Vec::new();
1062 };
1063
1064 let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
1068
1069 for (name, srcs) in &module.selective_re_exports {
1070 sources
1071 .entry(name.clone())
1072 .or_default()
1073 .extend(srcs.iter().cloned());
1074 }
1075 for src in &module.wildcard_re_export_paths {
1076 let canonical = normalize_path(src);
1077 let Some(src_module) = self
1078 .modules
1079 .get(&canonical)
1080 .or_else(|| self.modules.get(src))
1081 else {
1082 continue;
1083 };
1084 for name in &src_module.exports {
1085 sources
1086 .entry(name.clone())
1087 .or_default()
1088 .push(canonical.clone());
1089 }
1090 }
1091
1092 for name in &module.own_exports {
1096 if let Some(entry) = sources.get_mut(name) {
1097 entry.push(file.clone());
1098 }
1099 }
1100
1101 let mut conflicts = Vec::new();
1102 for (name, mut srcs) in sources {
1103 srcs.sort();
1104 srcs.dedup();
1105 if srcs.len() > 1 {
1106 conflicts.push(ReExportConflict {
1107 name,
1108 sources: srcs,
1109 });
1110 }
1111 }
1112 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
1113 conflicts
1114 }
1115
1116 pub fn selective_import_issues(&self, file: &Path) -> Vec<SelectiveImportIssue> {
1122 let file = normalize_path(file);
1123 let Some(module) = self.modules.get(&file) else {
1124 return Vec::new();
1125 };
1126
1127 let mut out = Vec::new();
1128 for import in &module.imports {
1129 let Some(selective) = &import.selective_names else {
1130 continue;
1131 };
1132 let Some(import_path) = &import.path else {
1133 continue;
1134 };
1135 let Some(target) = self
1136 .modules
1137 .get(import_path)
1138 .or_else(|| self.modules.get(&normalize_path(import_path)))
1139 else {
1140 continue;
1141 };
1142 if target.load_error.is_some() {
1143 continue;
1144 }
1145 for name in selective {
1146 let kind = if target.exports.contains(name) {
1147 continue;
1148 } else if target.declarations.contains_key(name) {
1149 SelectiveImportIssueKind::Private
1150 } else {
1151 SelectiveImportIssueKind::Missing
1152 };
1153 out.push(SelectiveImportIssue {
1154 name: name.clone(),
1155 module: import.raw_path.clone(),
1156 span: import.import_span,
1157 kind,
1158 });
1159 }
1160 }
1161 out.sort_by(|a, b| (&a.name, &a.module, a.kind).cmp(&(&b.name, &b.module, b.kind)));
1162 out.dedup();
1163 out
1164 }
1165
1166 pub fn exported_kind(&self, file: &Path, name: &str) -> Option<DefKind> {
1170 self.exported_kind_inner(file, name, &mut HashSet::new())
1171 }
1172
1173 fn exported_kind_inner(
1174 &self,
1175 file: &Path,
1176 name: &str,
1177 visited: &mut HashSet<PathBuf>,
1178 ) -> Option<DefKind> {
1179 let file = normalize_path(file);
1180 if !visited.insert(file.clone()) {
1181 return None;
1182 }
1183 let result = self.modules.get(&file).and_then(|module| {
1184 if module.own_exports.contains(name) {
1185 return module
1186 .declarations
1187 .get(name)
1188 .map(|definition| definition.kind)
1189 .or_else(|| {
1190 stdlib_module_from_path(&file).and_then(|stdlib_module| {
1191 stdlib::builtin_reexports(stdlib_module)
1192 .contains(&name)
1193 .then_some(DefKind::Function)
1194 })
1195 });
1196 }
1197 if let Some(sources) = module.selective_re_exports.get(name) {
1198 for source in sources {
1199 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1200 return Some(kind);
1201 }
1202 }
1203 }
1204 for source in &module.wildcard_re_export_paths {
1205 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1206 return Some(kind);
1207 }
1208 }
1209 None
1210 });
1211 visited.remove(&file);
1212 result
1213 }
1214}
1215
1216#[derive(Debug, Clone, PartialEq, Eq)]
1219pub struct ReExportConflict {
1220 pub name: String,
1221 pub sources: Vec<PathBuf>,
1222}
1223
1224#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1226pub enum SelectiveImportIssueKind {
1227 Missing,
1229 Private,
1231}
1232
1233#[derive(Debug, Clone, PartialEq, Eq)]
1235pub struct SelectiveImportIssue {
1236 pub name: String,
1238 pub module: String,
1240 pub span: Span,
1242 pub kind: SelectiveImportIssueKind,
1244}
1245
1246impl SelectiveImportIssue {
1247 #[must_use]
1249 pub fn message(&self) -> String {
1250 match self.kind {
1251 SelectiveImportIssueKind::Missing => format!(
1252 "imported symbol `{}` does not exist in `{}`",
1253 self.name, self.module
1254 ),
1255 SelectiveImportIssueKind::Private => format!(
1256 "imported symbol `{}` is not exported by `{}` — it is defined there but not `pub`",
1257 self.name, self.module
1258 ),
1259 }
1260 }
1261
1262 #[must_use]
1264 pub fn help(&self) -> String {
1265 match self.kind {
1266 SelectiveImportIssueKind::Missing => format!(
1267 "update the import to a symbol exported by `{}`",
1268 self.module
1269 ),
1270 SelectiveImportIssueKind::Private => {
1271 format!(
1272 "mark `{}` as `pub` in `{}` to export it",
1273 self.name, self.module
1274 )
1275 }
1276 }
1277 }
1278}
1279
1280fn load_module(
1281 path: &Path,
1282 package_snapshots: &[PackageSnapshot],
1283 source_overrides: Option<&HashMap<PathBuf, String>>,
1284) -> (ModuleInfo, Option<ParsedModuleSource>) {
1285 let source = source_overrides
1286 .and_then(|overrides| overrides.get(&normalize_path(path)).cloned())
1287 .or_else(|| read_module_source(path));
1288 let Some(source) = source else {
1289 return (ModuleInfo::default(), None);
1290 };
1291 let mut lexer = harn_lexer::Lexer::new(&source);
1292 let tokens = match lexer.tokenize() {
1293 Ok(tokens) => tokens,
1294 Err(error) => {
1295 let module = ModuleInfo {
1296 load_error: Some(ModuleLoadError {
1297 message: error.to_string(),
1298 span: error.span(),
1299 }),
1300 ..ModuleInfo::default()
1301 };
1302 return (module, None);
1303 }
1304 };
1305 let mut parser = Parser::new(tokens);
1306 let program = match parser.parse() {
1307 Ok(program) => program,
1308 Err(error) => {
1309 let module = ModuleInfo {
1310 load_error: Some(ModuleLoadError {
1311 message: error.to_string(),
1312 span: error.span(),
1313 }),
1314 ..ModuleInfo::default()
1315 };
1316 return (module, None);
1317 }
1318 };
1319
1320 let mut module = ModuleInfo::default();
1321 for node in &program {
1322 collect_module_info(path, node, &mut module, package_snapshots);
1323 collect_type_declarations(node, &mut module.type_declarations);
1324 collect_callable_declarations(node, &mut module.callable_declarations);
1325 }
1326 if let Some(stdlib_module) = stdlib_module_from_path(path) {
1327 module.own_exports.extend(
1328 stdlib::builtin_reexports(stdlib_module)
1329 .iter()
1330 .map(|name| (*name).to_string()),
1331 );
1332 }
1333 module.exports.extend(module.own_exports.iter().cloned());
1337 module
1338 .exports
1339 .extend(module.selective_re_exports.keys().cloned());
1340 let parsed = ParsedModuleSource { source, program };
1341 (module, Some(parsed))
1342}
1343
1344fn stdlib_module_from_path(path: &Path) -> Option<&str> {
1347 let s = path.to_str()?;
1348 s.strip_prefix("<std>/")
1349}
1350
1351fn normalize_path(path: &Path) -> PathBuf {
1352 canonical_path(path)
1353}
1354
1355pub fn canonical_path(path: &Path) -> PathBuf {
1368 use std::sync::OnceLock;
1369 if stdlib_module_from_path(path).is_some() {
1370 return path.to_path_buf();
1371 }
1372 static MEMO: OnceLock<std::sync::Mutex<HashMap<PathBuf, PathBuf>>> = OnceLock::new();
1373 let memo = MEMO.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
1374 if let Some(hit) = memo
1375 .lock()
1376 .expect("canonical path memo lock poisoned")
1377 .get(path)
1378 .cloned()
1379 {
1380 return hit;
1381 }
1382 match path.canonicalize() {
1383 Ok(canonical) => {
1384 memo.lock()
1385 .expect("canonical path memo lock poisoned")
1386 .insert(path.to_path_buf(), canonical.clone());
1387 canonical
1388 }
1389 Err(_) => path.to_path_buf(),
1390 }
1391}
1392
1393#[cfg(test)]
1394#[path = "tests.rs"]
1395mod tests;