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