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_callable_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
725 let mut names = HashSet::new();
726 for kind in [
727 DefKind::Function,
728 DefKind::Pipeline,
729 DefKind::Tool,
730 DefKind::Struct,
731 ] {
732 names.extend(self.imported_names_by_kind_for_file(file, kind)?);
733 }
734 Some(names)
735 }
736
737 pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
741 let file = normalize_path(file);
742 let module = self.modules.get(&file)?;
743 if module.has_unresolved_wildcard_import
744 || module.has_unresolved_selective_import
745 || module.has_unresolved_namespace_import
746 {
747 return None;
748 }
749
750 let mut decls = Vec::new();
751 let mut seen = HashSet::new();
752 for import in &module.imports {
753 if import.namespace_alias.is_some() {
755 continue;
756 }
757 let import_path = import.path.as_ref()?;
758 let imported = self
759 .modules
760 .get(import_path)
761 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
762 if imported.load_error.is_some() {
768 return None;
769 }
770 let mut names_to_collect: Vec<String> = match &import.selective_names {
771 None => imported.exports.iter().cloned().collect(),
772 Some(selective) => selective.iter().cloned().collect(),
773 };
774 names_to_collect.sort();
775 for name in &names_to_collect {
776 let mut visited = HashSet::new();
777 if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
778 let origin = self
779 .export_definition_of(import_path, name)
780 .map_or_else(|| import_path.clone(), |definition| definition.file);
781 self.extend_type_dependency(&origin, &decl, &mut decls, &mut seen);
782 }
783 }
784 for ty_decl in &imported.type_declarations {
794 if type_decl_name(ty_decl).is_some() {
795 self.extend_type_dependency(import_path, ty_decl, &mut decls, &mut seen);
796 }
797 }
798
799 for name in &names_to_collect {
800 let mut visited = HashSet::new();
801 let Some(callable) =
802 self.find_exported_callable_decl(import_path, name, &mut visited)
803 else {
804 continue;
805 };
806 let origin = self
807 .export_definition_of(import_path, name)
808 .map_or_else(|| import_path.clone(), |definition| definition.file);
809 self.extend_callable_type_dependencies(&origin, &callable, &mut decls, &mut seen);
810 }
811 }
812 Some(decls)
813 }
814
815 pub fn imported_callable_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
819 let file = normalize_path(file);
820 let module = self.modules.get(&file)?;
821 if module.has_unresolved_wildcard_import
822 || module.has_unresolved_selective_import
823 || module.has_unresolved_namespace_import
824 {
825 return None;
826 }
827
828 let mut decls = Vec::new();
829 for import in &module.imports {
830 if import.namespace_alias.is_some() {
832 continue;
833 }
834 let import_path = import.path.as_ref()?;
835 let imported = self
836 .modules
837 .get(import_path)
838 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
839 if imported.load_error.is_some() {
845 return None;
846 }
847 let selective_import = import.selective_names.is_some();
848 let names_to_collect: Vec<String> = match &import.selective_names {
849 None => imported.exports.iter().cloned().collect(),
850 Some(selective) => selective.iter().cloned().collect(),
851 };
852 for name in &names_to_collect {
853 if selective_import || imported.own_exports.contains(name) {
854 if let Some(decl) = imported
855 .callable_declarations
856 .iter()
857 .find(|decl| callable_decl_name(decl) == Some(name.as_str()))
858 {
859 decls.push(decl.clone());
860 continue;
861 }
862 }
863 let mut visited = HashSet::new();
864 if let Some(decl) =
865 self.find_exported_callable_decl(import_path, name, &mut visited)
866 {
867 decls.push(decl);
868 }
869 }
870 }
871 Some(decls)
872 }
873
874 fn find_exported_type_decl(
877 &self,
878 path: &Path,
879 name: &str,
880 visited: &mut HashSet<PathBuf>,
881 ) -> Option<SNode> {
882 let canonical = normalize_path(path);
883 if !visited.insert(canonical.clone()) {
884 return None;
885 }
886 let module = self
887 .modules
888 .get(&canonical)
889 .or_else(|| self.modules.get(path))?;
890 for decl in &module.type_declarations {
891 if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
892 return Some(decl.clone());
893 }
894 }
895 if let Some(sources) = module.selective_re_exports.get(name) {
896 for source in sources {
897 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
898 return Some(decl);
899 }
900 }
901 }
902 for source in &module.wildcard_re_export_paths {
903 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
904 return Some(decl);
905 }
906 }
907 None
908 }
909
910 fn find_exported_callable_decl(
911 &self,
912 path: &Path,
913 name: &str,
914 visited: &mut HashSet<PathBuf>,
915 ) -> Option<SNode> {
916 let canonical = normalize_path(path);
917 if !visited.insert(canonical.clone()) {
918 return None;
919 }
920 let module = self
921 .modules
922 .get(&canonical)
923 .or_else(|| self.modules.get(path))?;
924 for decl in &module.callable_declarations {
925 if callable_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
926 return Some(decl.clone());
927 }
928 }
929 if let Some(sources) = module.selective_re_exports.get(name) {
930 for source in sources {
931 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
932 return Some(decl);
933 }
934 }
935 }
936 for source in &module.wildcard_re_export_paths {
937 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
938 return Some(decl);
939 }
940 }
941 None
942 }
943
944 pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
950 let mut visited = HashSet::new();
951 self.definition_of_inner(file, name, &mut visited)
952 }
953
954 pub fn export_definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
961 let mut visited = HashSet::new();
962 self.export_definition_of_inner(file, name, &mut visited)
963 }
964
965 pub fn declared_names_for_file(&self, file: &Path) -> Option<Vec<&str>> {
969 let module = self.modules.get(&normalize_path(file))?;
970 let mut names: Vec<&str> = module.declarations.keys().map(String::as_str).collect();
971 names.sort_unstable();
972 Some(names)
973 }
974
975 fn definition_of_inner(
976 &self,
977 file: &Path,
978 name: &str,
979 visited: &mut HashSet<PathBuf>,
980 ) -> Option<DefSite> {
981 let file = normalize_path(file);
982 if !visited.insert(file.clone()) {
983 return None;
984 }
985 let current = self.modules.get(&file)?;
986
987 if let Some(local) = current.declarations.get(name) {
988 return Some(local.clone());
989 }
990
991 if let Some(sources) = current.selective_re_exports.get(name) {
996 for source in sources {
997 if let Some(def) = self.definition_of_inner(source, name, visited) {
998 return Some(def);
999 }
1000 }
1001 }
1002
1003 for source in ¤t.wildcard_re_export_paths {
1005 if let Some(def) = self.definition_of_inner(source, name, visited) {
1006 return Some(def);
1007 }
1008 }
1009
1010 for import in ¤t.imports {
1012 let Some(selective_names) = &import.selective_names else {
1013 continue;
1014 };
1015 if !selective_names.contains(name) {
1016 continue;
1017 }
1018 if let Some(path) = &import.path {
1019 if let Some(def) = self.definition_of_inner(path, name, visited) {
1020 return Some(def);
1021 }
1022 }
1023 }
1024
1025 for import in ¤t.imports {
1027 if import.selective_names.is_some() || import.namespace_alias.is_some() {
1028 continue;
1029 }
1030 if let Some(path) = &import.path {
1031 if let Some(def) = self.definition_of_inner(path, name, visited) {
1032 return Some(def);
1033 }
1034 }
1035 }
1036
1037 None
1038 }
1039
1040 fn export_definition_of_inner(
1041 &self,
1042 file: &Path,
1043 name: &str,
1044 visited: &mut HashSet<PathBuf>,
1045 ) -> Option<DefSite> {
1046 let file = normalize_path(file);
1047 if !visited.insert(file.clone()) {
1048 return None;
1049 }
1050 let current = self.modules.get(&file)?;
1051
1052 if current.own_exports.contains(name) {
1053 if let Some(local) = current.declarations.get(name) {
1054 return Some(local.clone());
1055 }
1056 }
1057 if let Some(sources) = current.selective_re_exports.get(name) {
1058 for source in sources {
1059 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1060 return Some(definition);
1061 }
1062 }
1063 }
1064 for source in ¤t.wildcard_re_export_paths {
1065 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1066 return Some(definition);
1067 }
1068 }
1069 None
1070 }
1071
1072 pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
1076 let file = normalize_path(file);
1077 let Some(module) = self.modules.get(&file) else {
1078 return Vec::new();
1079 };
1080
1081 let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
1085
1086 for (name, srcs) in &module.selective_re_exports {
1087 sources
1088 .entry(name.clone())
1089 .or_default()
1090 .extend(srcs.iter().cloned());
1091 }
1092 for src in &module.wildcard_re_export_paths {
1093 let canonical = normalize_path(src);
1094 let Some(src_module) = self
1095 .modules
1096 .get(&canonical)
1097 .or_else(|| self.modules.get(src))
1098 else {
1099 continue;
1100 };
1101 for name in &src_module.exports {
1102 sources
1103 .entry(name.clone())
1104 .or_default()
1105 .push(canonical.clone());
1106 }
1107 }
1108
1109 for name in &module.own_exports {
1113 if let Some(entry) = sources.get_mut(name) {
1114 entry.push(file.clone());
1115 }
1116 }
1117
1118 let mut conflicts = Vec::new();
1119 for (name, mut srcs) in sources {
1120 srcs.sort();
1121 srcs.dedup();
1122 if srcs.len() > 1 {
1123 conflicts.push(ReExportConflict {
1124 name,
1125 sources: srcs,
1126 });
1127 }
1128 }
1129 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
1130 conflicts
1131 }
1132
1133 pub fn selective_import_issues(&self, file: &Path) -> Vec<SelectiveImportIssue> {
1139 let file = normalize_path(file);
1140 let Some(module) = self.modules.get(&file) else {
1141 return Vec::new();
1142 };
1143
1144 let mut out = Vec::new();
1145 for import in &module.imports {
1146 let Some(selective) = &import.selective_names else {
1147 continue;
1148 };
1149 let Some(import_path) = &import.path else {
1150 continue;
1151 };
1152 let Some(target) = self
1153 .modules
1154 .get(import_path)
1155 .or_else(|| self.modules.get(&normalize_path(import_path)))
1156 else {
1157 continue;
1158 };
1159 if target.load_error.is_some() {
1160 continue;
1161 }
1162 for name in selective {
1163 let kind = if target.exports.contains(name) {
1164 continue;
1165 } else if target.declarations.contains_key(name) {
1166 SelectiveImportIssueKind::Private
1167 } else {
1168 SelectiveImportIssueKind::Missing
1169 };
1170 out.push(SelectiveImportIssue {
1171 name: name.clone(),
1172 module: import.raw_path.clone(),
1173 span: import.import_span,
1174 kind,
1175 });
1176 }
1177 }
1178 out.sort_by(|a, b| (&a.name, &a.module, a.kind).cmp(&(&b.name, &b.module, b.kind)));
1179 out.dedup();
1180 out
1181 }
1182
1183 pub fn exported_kind(&self, file: &Path, name: &str) -> Option<DefKind> {
1187 self.exported_kind_inner(file, name, &mut HashSet::new())
1188 }
1189
1190 fn exported_kind_inner(
1191 &self,
1192 file: &Path,
1193 name: &str,
1194 visited: &mut HashSet<PathBuf>,
1195 ) -> Option<DefKind> {
1196 let file = normalize_path(file);
1197 if !visited.insert(file.clone()) {
1198 return None;
1199 }
1200 let result = self.modules.get(&file).and_then(|module| {
1201 if module.own_exports.contains(name) {
1202 return module
1203 .declarations
1204 .get(name)
1205 .map(|definition| definition.kind)
1206 .or_else(|| {
1207 stdlib_module_from_path(&file).and_then(|stdlib_module| {
1208 stdlib::builtin_reexports(stdlib_module)
1209 .contains(&name)
1210 .then_some(DefKind::Function)
1211 })
1212 });
1213 }
1214 if let Some(sources) = module.selective_re_exports.get(name) {
1215 for source in sources {
1216 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1217 return Some(kind);
1218 }
1219 }
1220 }
1221 for source in &module.wildcard_re_export_paths {
1222 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1223 return Some(kind);
1224 }
1225 }
1226 None
1227 });
1228 visited.remove(&file);
1229 result
1230 }
1231}
1232
1233#[derive(Debug, Clone, PartialEq, Eq)]
1236pub struct ReExportConflict {
1237 pub name: String,
1238 pub sources: Vec<PathBuf>,
1239}
1240
1241#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1243pub enum SelectiveImportIssueKind {
1244 Missing,
1246 Private,
1248}
1249
1250#[derive(Debug, Clone, PartialEq, Eq)]
1252pub struct SelectiveImportIssue {
1253 pub name: String,
1255 pub module: String,
1257 pub span: Span,
1259 pub kind: SelectiveImportIssueKind,
1261}
1262
1263impl SelectiveImportIssue {
1264 #[must_use]
1266 pub fn message(&self) -> String {
1267 match self.kind {
1268 SelectiveImportIssueKind::Missing => format!(
1269 "imported symbol `{}` does not exist in `{}`",
1270 self.name, self.module
1271 ),
1272 SelectiveImportIssueKind::Private => format!(
1273 "imported symbol `{}` is not exported by `{}` — it is defined there but not `pub`",
1274 self.name, self.module
1275 ),
1276 }
1277 }
1278
1279 #[must_use]
1281 pub fn help(&self) -> String {
1282 match self.kind {
1283 SelectiveImportIssueKind::Missing => format!(
1284 "update the import to a symbol exported by `{}`",
1285 self.module
1286 ),
1287 SelectiveImportIssueKind::Private => {
1288 format!(
1289 "mark `{}` as `pub` in `{}` to export it",
1290 self.name, self.module
1291 )
1292 }
1293 }
1294 }
1295}
1296
1297fn load_module(
1298 path: &Path,
1299 package_snapshots: &[PackageSnapshot],
1300 source_overrides: Option<&HashMap<PathBuf, String>>,
1301) -> (ModuleInfo, Option<ParsedModuleSource>) {
1302 let source = source_overrides
1303 .and_then(|overrides| overrides.get(&normalize_path(path)).cloned())
1304 .or_else(|| read_module_source(path));
1305 let Some(source) = source else {
1306 return (ModuleInfo::default(), None);
1307 };
1308 let mut lexer = harn_lexer::Lexer::new(&source);
1309 let tokens = match lexer.tokenize() {
1310 Ok(tokens) => tokens,
1311 Err(error) => {
1312 let module = ModuleInfo {
1313 load_error: Some(ModuleLoadError {
1314 message: error.to_string(),
1315 span: error.span(),
1316 }),
1317 ..ModuleInfo::default()
1318 };
1319 return (module, None);
1320 }
1321 };
1322 let mut parser = Parser::new(tokens);
1323 let program = match parser.parse() {
1324 Ok(program) => program,
1325 Err(error) => {
1326 let module = ModuleInfo {
1327 load_error: Some(ModuleLoadError {
1328 message: error.to_string(),
1329 span: error.span(),
1330 }),
1331 ..ModuleInfo::default()
1332 };
1333 return (module, None);
1334 }
1335 };
1336
1337 let mut module = ModuleInfo::default();
1338 for node in &program {
1339 collect_module_info(path, node, &mut module, package_snapshots);
1340 collect_type_declarations(node, &mut module.type_declarations);
1341 collect_callable_declarations(node, &mut module.callable_declarations);
1342 }
1343 if let Some(stdlib_module) = stdlib_module_from_path(path) {
1344 module.own_exports.extend(
1345 stdlib::builtin_reexports(stdlib_module)
1346 .iter()
1347 .map(|name| (*name).to_string()),
1348 );
1349 }
1350 module.exports.extend(module.own_exports.iter().cloned());
1354 module
1355 .exports
1356 .extend(module.selective_re_exports.keys().cloned());
1357 let parsed = ParsedModuleSource { source, program };
1358 (module, Some(parsed))
1359}
1360
1361fn stdlib_module_from_path(path: &Path) -> Option<&str> {
1364 let s = path.to_str()?;
1365 s.strip_prefix("<std>/")
1366}
1367
1368fn normalize_path(path: &Path) -> PathBuf {
1369 canonical_path(path)
1370}
1371
1372pub fn canonical_path(path: &Path) -> PathBuf {
1385 use std::sync::OnceLock;
1386 if stdlib_module_from_path(path).is_some() {
1387 return path.to_path_buf();
1388 }
1389 static MEMO: OnceLock<std::sync::Mutex<HashMap<PathBuf, PathBuf>>> = OnceLock::new();
1390 let memo = MEMO.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
1391 if let Some(hit) = memo
1392 .lock()
1393 .expect("canonical path memo lock poisoned")
1394 .get(path)
1395 .cloned()
1396 {
1397 return hit;
1398 }
1399 match path.canonicalize() {
1400 Ok(canonical) => {
1401 memo.lock()
1402 .expect("canonical path memo lock poisoned")
1403 .insert(path.to_path_buf(), canonical.clone());
1404 canonical
1405 }
1406 Err(_) => path.to_path_buf(),
1407 }
1408}
1409
1410#[cfg(test)]
1411#[path = "tests.rs"]
1412mod tests;