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;
12pub mod host_capabilities;
13pub mod host_capability_config;
14mod import_recording;
15pub mod manifest_walk;
16mod namespace_imports;
17mod namespace_signatures;
18pub mod package_execution;
19mod package_imports;
20pub mod package_snapshot;
21pub mod personas;
22pub mod project_config;
23mod stdlib;
24mod symbol_reachability;
25mod type_dependencies;
26
27use declarations::{
28 callable_decl_name, collect_callable_declarations, collect_module_info,
29 collect_type_declarations, decl_site, type_decl_name,
30};
31pub use declarations::{public_declarations, DefKind, PublicDeclaration};
32pub use namespace_imports::NamespaceImportInfo;
33pub use namespace_signatures::NamespaceMemberSignature;
34pub use package_imports::{
35 resolve_import_path, resolve_import_path_with_guard, resolve_import_path_with_snapshot,
36};
37pub use symbol_reachability::{
38 closed_program_reachability, ExportDemand, ModuleSymbolDemand, SymbolReachability,
39};
40
41#[derive(Debug, Clone)]
43pub struct DefSite {
44 pub name: String,
45 pub file: PathBuf,
46 pub kind: DefKind,
47 pub span: Span,
48}
49
50#[derive(Debug, Clone)]
52pub enum WildcardResolution {
53 Resolved(HashSet<String>),
55 Unknown,
57}
58
59#[derive(Debug, Default)]
61pub struct ModuleGraph {
62 modules: HashMap<PathBuf, ModuleInfo>,
63 _package_snapshots: Vec<PackageSnapshot>,
65}
66
67#[derive(Debug, Clone)]
68pub struct ParsedModuleSource {
69 pub source: String,
70 pub program: Vec<SNode>,
71}
72
73#[derive(Debug, Default)]
74pub struct ModuleGraphBuild {
75 pub graph: ModuleGraph,
76 pub parsed_sources: HashMap<PathBuf, ParsedModuleSource>,
77}
78
79#[derive(Debug, Default)]
80struct ModuleInfo {
81 declarations: HashMap<String, DefSite>,
84 exports: HashSet<String>,
89 own_exports: HashSet<String>,
92 selective_re_exports: HashMap<String, Vec<PathBuf>>,
99 wildcard_re_export_paths: Vec<PathBuf>,
103 namespace_re_exports: HashMap<String, PathBuf>,
110 selective_import_names: HashSet<String>,
112 imports: Vec<ImportRef>,
114 has_unresolved_wildcard_import: bool,
116 has_unresolved_selective_import: bool,
120 has_unresolved_namespace_import: bool,
122 type_declarations: Vec<SNode>,
125 callable_declarations: Vec<SNode>,
128 load_error: Option<ModuleLoadError>,
134}
135
136#[derive(Debug, Clone)]
142pub struct ModuleLoadError {
143 pub message: String,
145 pub span: Span,
147}
148
149#[derive(Debug, Clone)]
152pub struct ImportCompileFailure {
153 pub import_raw_path: String,
155 pub import_span: Span,
157 pub module_path: PathBuf,
159 pub error: ModuleLoadError,
161}
162
163#[derive(Debug, Clone)]
164struct ImportRef {
165 raw_path: String,
166 path: Option<PathBuf>,
167 selective_names: Option<HashSet<String>>,
168 namespace_alias: Option<String>,
171 is_pub: bool,
172 import_span: Span,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
177pub struct ModuleImport {
178 pub raw_path: String,
180 pub resolved_path: Option<PathBuf>,
182 pub selective_names: Option<Vec<String>>,
184 pub namespace_alias: Option<String>,
186 pub is_pub: bool,
188}
189
190pub fn read_module_source(path: &Path) -> Option<String> {
196 if let Some(stdlib_module) = stdlib_module_name(path) {
197 return stdlib::get_stdlib_source(stdlib_module).map(ToString::to_string);
198 }
199 std::fs::read_to_string(path).ok()
200}
201
202pub fn build(files: &[PathBuf]) -> ModuleGraph {
208 build_inner(files, ParsedSourceRetention::None, None).graph
209}
210
211pub fn build_with_source(file: &Path, source: &str) -> ModuleGraph {
217 let file = normalize_path(file);
218 let source_overrides = HashMap::from([(file.clone(), source.to_string())]);
219 build_inner(
220 &[file],
221 ParsedSourceRetention::None,
222 Some(&source_overrides),
223 )
224 .graph
225}
226
227pub fn build_with_parsed_sources(files: &[PathBuf]) -> ModuleGraphBuild {
233 let parsed_source_targets = files.iter().map(|file| normalize_path(file)).collect();
234 build_inner(
235 files,
236 ParsedSourceRetention::Seeds(&parsed_source_targets),
237 None,
238 )
239}
240
241pub fn build_closed_program(files: &[PathBuf]) -> ModuleGraphBuild {
247 build_inner(files, ParsedSourceRetention::All, None)
248}
249
250#[derive(Clone, Copy)]
251enum ParsedSourceRetention<'a> {
252 None,
253 Seeds(&'a HashSet<PathBuf>),
254 All,
255}
256
257impl ParsedSourceRetention<'_> {
258 fn retains(self, path: &Path) -> bool {
259 match self {
260 Self::None => false,
261 Self::Seeds(targets) => targets.contains(path),
262 Self::All => true,
263 }
264 }
265}
266
267fn build_inner(
268 files: &[PathBuf],
269 parsed_source_retention: ParsedSourceRetention<'_>,
270 source_overrides: Option<&HashMap<PathBuf, String>>,
271) -> ModuleGraphBuild {
272 let package_snapshots = acquire_package_snapshots(files);
273 let mut modules: HashMap<PathBuf, ModuleInfo> = HashMap::new();
274 let mut parsed_sources: HashMap<PathBuf, ParsedModuleSource> = HashMap::new();
275 let mut seen: HashSet<PathBuf> = HashSet::new();
276 let mut wave: Vec<PathBuf> = Vec::new();
277 for file in files {
278 let canonical = normalize_path(file);
279 if seen.insert(canonical.clone()) {
280 wave.push(canonical);
281 }
282 }
283 while !wave.is_empty() {
291 let loaded = load_wave(
292 &wave,
293 &package_snapshots,
294 parsed_source_retention,
295 source_overrides,
296 );
297 let mut next_wave: Vec<PathBuf> = Vec::new();
298 for (path, (module, parsed)) in wave.drain(..).zip(loaded) {
299 if parsed_source_retention.retains(&path) {
300 if let Some(parsed) = parsed {
301 parsed_sources.insert(path.clone(), parsed);
302 }
303 }
304 for import in &module.imports {
321 if let Some(import_path) = &import.path {
322 let canonical = normalize_path(import_path);
323 if seen.insert(canonical.clone()) {
324 next_wave.push(canonical);
325 }
326 }
327 }
328 modules.insert(path, module);
329 }
330 wave = next_wave;
331 }
332 resolve_re_exports(&mut modules);
333 ModuleGraphBuild {
334 graph: ModuleGraph {
335 modules,
336 _package_snapshots: package_snapshots,
337 },
338 parsed_sources,
339 }
340}
341
342pub const MODULE_GRAPH_JOBS_ENV: &str = "HARN_MODULE_GRAPH_JOBS";
345
346fn load_wave(
349 paths: &[PathBuf],
350 package_snapshots: &[PackageSnapshot],
351 parsed_source_retention: ParsedSourceRetention<'_>,
352 source_overrides: Option<&HashMap<PathBuf, String>>,
353) -> Vec<(ModuleInfo, Option<ParsedModuleSource>)> {
354 const MIN_PARALLEL_WAVE: usize = 8;
355 let configured = std::env::var(MODULE_GRAPH_JOBS_ENV)
356 .ok()
357 .and_then(|value| value.parse::<usize>().ok())
358 .filter(|&jobs| jobs > 0);
359 let workers = configured
360 .unwrap_or_else(|| {
361 std::thread::available_parallelism()
362 .map(std::num::NonZeroUsize::get)
363 .unwrap_or(1)
364 })
365 .min(paths.len());
366 if workers <= 1 || paths.len() < MIN_PARALLEL_WAVE {
367 return paths
368 .iter()
369 .map(|path| {
370 load_module(
371 path,
372 package_snapshots,
373 source_overrides,
374 parsed_source_retention.retains(path),
375 )
376 })
377 .collect();
378 }
379 let next = std::sync::atomic::AtomicUsize::new(0);
380 let mut produced: Vec<(usize, (ModuleInfo, Option<ParsedModuleSource>))> =
381 std::thread::scope(|scope| {
382 let handles: Vec<_> = (0..workers)
383 .map(|_| {
384 scope.spawn(|| {
385 let mut local = Vec::new();
386 loop {
387 let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
388 let Some(path) = paths.get(index) else {
389 break;
390 };
391 local.push((
392 index,
393 load_module(
394 path,
395 package_snapshots,
396 source_overrides,
397 parsed_source_retention.retains(path),
398 ),
399 ));
400 }
401 local
402 })
403 })
404 .collect();
405 handles
406 .into_iter()
407 .flat_map(|handle| match handle.join() {
408 Ok(local) => local,
409 Err(panic) => std::panic::resume_unwind(panic),
410 })
411 .collect()
412 });
413 produced.sort_unstable_by_key(|(index, _)| *index);
414 produced.into_iter().map(|(_, loaded)| loaded).collect()
415}
416
417fn resolve_re_exports(modules: &mut HashMap<PathBuf, ModuleInfo>) {
422 let keys: Vec<PathBuf> = modules.keys().cloned().collect();
423 loop {
424 let mut changed = false;
425 for path in &keys {
426 let wildcard_paths = modules
429 .get(path)
430 .map(|m| m.wildcard_re_export_paths.clone())
431 .unwrap_or_default();
432 if wildcard_paths.is_empty() {
433 continue;
434 }
435 let mut additions: Vec<String> = Vec::new();
436 for src in &wildcard_paths {
437 let src_canonical = normalize_path(src);
438 if let Some(src_module) = modules.get(src).or_else(|| modules.get(&src_canonical)) {
439 additions.extend(src_module.exports.iter().cloned());
440 }
441 }
442 if let Some(module) = modules.get_mut(path) {
443 for name in additions {
444 if module.exports.insert(name) {
445 changed = true;
446 }
447 }
448 }
449 }
450 if !changed {
451 break;
452 }
453 }
454}
455
456impl ModuleGraph {
457 pub fn module_paths(&self) -> Vec<PathBuf> {
462 let mut paths: Vec<PathBuf> = self.modules.keys().cloned().collect();
463 paths.sort();
464 paths
465 }
466
467 pub fn contains_module(&self, path: &Path) -> bool {
470 self.modules.contains_key(path) || self.modules.contains_key(&normalize_path(path))
471 }
472
473 pub fn all_selective_import_names(&self) -> HashSet<&str> {
475 let mut names = HashSet::new();
476 for module in self.modules.values() {
477 for name in &module.selective_import_names {
478 names.insert(name.as_str());
479 }
480 }
481 names
482 }
483
484 pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
487 let target = normalize_path(target);
488 let mut out: Vec<PathBuf> = self
489 .modules
490 .iter()
491 .filter(|(_, info)| {
492 info.imports.iter().any(|import| {
493 import
494 .path
495 .as_ref()
496 .is_some_and(|p| normalize_path(p) == target)
497 })
498 })
499 .map(|(path, _)| path.clone())
500 .collect();
501 out.sort();
502 out
503 }
504
505 pub fn transitive_importers_of(&self, target: &Path) -> Vec<PathBuf> {
512 let target = normalize_path(target);
513 let mut visited = HashSet::from([target.clone()]);
514 let mut pending = vec![target];
515 let mut out = Vec::new();
516
517 while let Some(current) = pending.pop() {
518 for importer in self.importers_of(¤t) {
519 let importer = normalize_path(&importer);
520 if visited.insert(importer.clone()) {
521 pending.push(importer.clone());
522 out.push(importer);
523 }
524 }
525 }
526
527 out.sort();
528 out
529 }
530
531 pub fn imports_for_module(&self, file: &Path) -> Vec<ModuleImport> {
533 let file = normalize_path(file);
534 let Some(module) = self.modules.get(&file) else {
535 return Vec::new();
536 };
537 let mut imports: Vec<ModuleImport> = module
538 .imports
539 .iter()
540 .map(|import| {
541 let mut selective_names = import
542 .selective_names
543 .as_ref()
544 .map(|names| names.iter().cloned().collect::<Vec<_>>());
545 if let Some(names) = selective_names.as_mut() {
546 names.sort();
547 }
548 ModuleImport {
549 raw_path: import.raw_path.clone(),
550 resolved_path: import.path.as_ref().map(|path| normalize_path(path)),
551 selective_names,
552 namespace_alias: import.namespace_alias.clone(),
553 is_pub: import.is_pub,
554 }
555 })
556 .collect();
557 imports.sort_by(|left, right| {
558 left.raw_path
559 .cmp(&right.raw_path)
560 .then_with(|| left.selective_names.cmp(&right.selective_names))
561 .then_with(|| left.namespace_alias.cmp(&right.namespace_alias))
562 .then_with(|| left.resolved_path.cmp(&right.resolved_path))
563 });
564 imports
565 }
566
567 pub fn exports_for_module(&self, file: &Path) -> Vec<String> {
569 let file = normalize_path(file);
570 let Some(module) = self.modules.get(&file) else {
571 return Vec::new();
572 };
573 let mut exports: Vec<String> = module.exports.iter().cloned().collect();
574 exports.sort();
575 exports
576 }
577
578 pub fn wildcard_exports_for(&self, file: &Path) -> WildcardResolution {
583 let file = normalize_path(file);
584 let Some(module) = self.modules.get(&file) else {
585 return WildcardResolution::Unknown;
586 };
587 if module.has_unresolved_wildcard_import {
588 return WildcardResolution::Unknown;
589 }
590
591 let mut names = HashSet::new();
592 for import in module
593 .imports
594 .iter()
595 .filter(|import| import.selective_names.is_none())
596 {
597 let Some(import_path) = &import.path else {
598 return WildcardResolution::Unknown;
599 };
600 let imported = self.modules.get(import_path).or_else(|| {
601 let normalized = normalize_path(import_path);
602 self.modules.get(&normalized)
603 });
604 let Some(imported) = imported else {
605 return WildcardResolution::Unknown;
606 };
607 names.extend(imported.exports.iter().cloned());
608 }
609 WildcardResolution::Resolved(names)
610 }
611
612 #[must_use]
633 pub fn import_compile_failures(&self, file: &Path) -> Vec<ImportCompileFailure> {
634 let file = normalize_path(file);
635 let Some(module) = self.modules.get(&file) else {
636 return Vec::new();
637 };
638 let mut failures = Vec::new();
639 for import in &module.imports {
640 let Some(import_path) = &import.path else {
641 continue;
642 };
643 let Some(target) = self
644 .modules
645 .get(import_path)
646 .or_else(|| self.modules.get(&normalize_path(import_path)))
647 else {
648 continue;
649 };
650 if let Some(error) = &target.load_error {
651 failures.push(ImportCompileFailure {
652 import_raw_path: import.raw_path.clone(),
653 import_span: import.import_span,
654 module_path: normalize_path(import_path),
655 error: error.clone(),
656 });
657 }
658 }
659 failures
660 }
661
662 pub fn imported_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
663 let file = normalize_path(file);
664 let module = self.modules.get(&file)?;
665 if module.has_unresolved_wildcard_import
666 || module.has_unresolved_selective_import
667 || module.has_unresolved_namespace_import
668 {
669 return None;
670 }
671
672 let mut names = HashSet::new();
673 for import in &module.imports {
674 if let Some(alias) = &import.namespace_alias {
676 names.insert(alias.clone());
677 continue;
678 }
679 let import_path = import.path.as_ref()?;
680 let imported = self
681 .modules
682 .get(import_path)
683 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
684 if imported.load_error.is_some() {
690 return None;
691 }
692 match &import.selective_names {
693 None => {
694 names.extend(imported.exports.iter().cloned());
695 }
696 Some(selective) => {
697 for name in selective {
706 if imported.declarations.contains_key(name)
707 || imported.exports.contains(name)
708 {
709 names.insert(name.clone());
710 }
711 }
712 }
713 }
714 }
715 Some(names)
716 }
717
718 pub fn imported_names_by_kind_for_file(
723 &self,
724 file: &Path,
725 kind: DefKind,
726 ) -> Option<HashSet<String>> {
727 let file = normalize_path(file);
728 let module = self.modules.get(&file)?;
729 if module.has_unresolved_wildcard_import
730 || module.has_unresolved_selective_import
731 || module.has_unresolved_namespace_import
732 {
733 return None;
734 }
735
736 let mut names = HashSet::new();
737 for import in &module.imports {
738 if import.namespace_alias.is_some() {
740 continue;
741 }
742 let import_path = import.path.as_ref()?;
743 let imported_names: Vec<String> = match &import.selective_names {
744 Some(selective) => selective.iter().cloned().collect(),
745 None => self
746 .modules
747 .get(import_path)
748 .or_else(|| self.modules.get(&normalize_path(import_path)))?
749 .exports
750 .iter()
751 .cloned()
752 .collect(),
753 };
754 for name in imported_names {
755 if self.exported_kind(import_path, &name) == Some(kind) {
756 names.insert(name);
757 }
758 }
759 }
760 Some(names)
761 }
762
763 pub fn imported_callable_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
768 let mut names = HashSet::new();
769 for kind in [
770 DefKind::Function,
771 DefKind::Pipeline,
772 DefKind::Tool,
773 DefKind::Struct,
774 ] {
775 names.extend(self.imported_names_by_kind_for_file(file, kind)?);
776 }
777 Some(names)
778 }
779
780 pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
784 let file = normalize_path(file);
785 let module = self.modules.get(&file)?;
786 if module.has_unresolved_wildcard_import
787 || module.has_unresolved_selective_import
788 || module.has_unresolved_namespace_import
789 {
790 return None;
791 }
792
793 let mut decls = Vec::new();
794 let mut seen = HashSet::new();
795 for import in &module.imports {
796 if import.namespace_alias.is_some() {
798 continue;
799 }
800 let import_path = import.path.as_ref()?;
801 let imported = self
802 .modules
803 .get(import_path)
804 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
805 if imported.load_error.is_some() {
811 return None;
812 }
813 let mut names_to_collect: Vec<String> = match &import.selective_names {
814 None => imported.exports.iter().cloned().collect(),
815 Some(selective) => selective.iter().cloned().collect(),
816 };
817 names_to_collect.sort();
818 for name in &names_to_collect {
819 let mut visited = HashSet::new();
820 if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
821 let origin = self
822 .export_definition_of(import_path, name)
823 .map_or_else(|| import_path.clone(), |definition| definition.file);
824 self.extend_type_dependency(&origin, &decl, &mut decls, &mut seen);
825 }
826 }
827 for ty_decl in &imported.type_declarations {
837 if type_decl_name(ty_decl).is_some() {
838 self.extend_type_dependency(import_path, ty_decl, &mut decls, &mut seen);
839 }
840 }
841
842 for name in &names_to_collect {
843 let mut visited = HashSet::new();
844 let Some(callable) =
845 self.find_exported_callable_decl(import_path, name, &mut visited)
846 else {
847 continue;
848 };
849 let origin = self
850 .export_definition_of(import_path, name)
851 .map_or_else(|| import_path.clone(), |definition| definition.file);
852 self.extend_callable_type_dependencies(&origin, &callable, &mut decls, &mut seen);
853 }
854 }
855 Some(decls)
856 }
857
858 pub fn imported_callable_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
862 let file = normalize_path(file);
863 let module = self.modules.get(&file)?;
864 if module.has_unresolved_wildcard_import
865 || module.has_unresolved_selective_import
866 || module.has_unresolved_namespace_import
867 {
868 return None;
869 }
870
871 let mut decls = Vec::new();
872 for import in &module.imports {
873 if import.namespace_alias.is_some() {
875 continue;
876 }
877 let import_path = import.path.as_ref()?;
878 let imported = self
879 .modules
880 .get(import_path)
881 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
882 if imported.load_error.is_some() {
888 return None;
889 }
890 let selective_import = import.selective_names.is_some();
891 let names_to_collect: Vec<String> = match &import.selective_names {
892 None => imported.exports.iter().cloned().collect(),
893 Some(selective) => selective.iter().cloned().collect(),
894 };
895 for name in &names_to_collect {
896 if selective_import || imported.own_exports.contains(name) {
897 if let Some(decl) = imported
898 .callable_declarations
899 .iter()
900 .find(|decl| callable_decl_name(decl) == Some(name.as_str()))
901 {
902 decls.push(decl.clone());
903 continue;
904 }
905 }
906 let mut visited = HashSet::new();
907 if let Some(decl) =
908 self.find_exported_callable_decl(import_path, name, &mut visited)
909 {
910 decls.push(decl);
911 }
912 }
913 }
914 Some(decls)
915 }
916
917 fn find_exported_type_decl(
920 &self,
921 path: &Path,
922 name: &str,
923 visited: &mut HashSet<PathBuf>,
924 ) -> Option<SNode> {
925 let canonical = normalize_path(path);
926 if !visited.insert(canonical.clone()) {
927 return None;
928 }
929 let module = self
930 .modules
931 .get(&canonical)
932 .or_else(|| self.modules.get(path))?;
933 for decl in &module.type_declarations {
934 if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
935 return Some(decl.clone());
936 }
937 }
938 if let Some(sources) = module.selective_re_exports.get(name) {
939 for source in sources {
940 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
941 return Some(decl);
942 }
943 }
944 }
945 for source in &module.wildcard_re_export_paths {
946 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
947 return Some(decl);
948 }
949 }
950 None
951 }
952
953 fn find_exported_callable_decl(
954 &self,
955 path: &Path,
956 name: &str,
957 visited: &mut HashSet<PathBuf>,
958 ) -> Option<SNode> {
959 let canonical = normalize_path(path);
960 if !visited.insert(canonical.clone()) {
961 return None;
962 }
963 let module = self
964 .modules
965 .get(&canonical)
966 .or_else(|| self.modules.get(path))?;
967 for decl in &module.callable_declarations {
968 if callable_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
969 return Some(decl.clone());
970 }
971 }
972 if let Some(sources) = module.selective_re_exports.get(name) {
973 for source in sources {
974 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
975 return Some(decl);
976 }
977 }
978 }
979 for source in &module.wildcard_re_export_paths {
980 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
981 return Some(decl);
982 }
983 }
984 None
985 }
986
987 pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
993 let mut visited = HashSet::new();
994 self.definition_of_inner(file, name, &mut visited)
995 }
996
997 pub fn export_definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
1004 let mut visited = HashSet::new();
1005 self.export_definition_of_inner(file, name, &mut visited)
1006 }
1007
1008 pub fn declared_names_for_file(&self, file: &Path) -> Option<Vec<&str>> {
1012 let module = self.modules.get(&normalize_path(file))?;
1013 let mut names: Vec<&str> = module.declarations.keys().map(String::as_str).collect();
1014 names.sort_unstable();
1015 Some(names)
1016 }
1017
1018 fn definition_of_inner(
1019 &self,
1020 file: &Path,
1021 name: &str,
1022 visited: &mut HashSet<PathBuf>,
1023 ) -> Option<DefSite> {
1024 let file = normalize_path(file);
1025 if !visited.insert(file.clone()) {
1026 return None;
1027 }
1028 let current = self.modules.get(&file)?;
1029
1030 if let Some(local) = current.declarations.get(name) {
1031 return Some(local.clone());
1032 }
1033
1034 if let Some(sources) = current.selective_re_exports.get(name) {
1039 for source in sources {
1040 if let Some(def) = self.definition_of_inner(source, name, visited) {
1041 return Some(def);
1042 }
1043 }
1044 }
1045
1046 for source in ¤t.wildcard_re_export_paths {
1048 if let Some(def) = self.definition_of_inner(source, name, visited) {
1049 return Some(def);
1050 }
1051 }
1052
1053 for import in ¤t.imports {
1055 let Some(selective_names) = &import.selective_names else {
1056 continue;
1057 };
1058 if !selective_names.contains(name) {
1059 continue;
1060 }
1061 if let Some(path) = &import.path {
1062 if let Some(def) = self.definition_of_inner(path, name, visited) {
1063 return Some(def);
1064 }
1065 }
1066 }
1067
1068 for import in ¤t.imports {
1070 if import.selective_names.is_some() || import.namespace_alias.is_some() {
1071 continue;
1072 }
1073 if let Some(path) = &import.path {
1074 if let Some(def) = self.definition_of_inner(path, name, visited) {
1075 return Some(def);
1076 }
1077 }
1078 }
1079
1080 None
1081 }
1082
1083 fn export_definition_of_inner(
1084 &self,
1085 file: &Path,
1086 name: &str,
1087 visited: &mut HashSet<PathBuf>,
1088 ) -> Option<DefSite> {
1089 let file = normalize_path(file);
1090 if !visited.insert(file.clone()) {
1091 return None;
1092 }
1093 let current = self.modules.get(&file)?;
1094
1095 if current.own_exports.contains(name) {
1096 if let Some(local) = current.declarations.get(name) {
1097 return Some(local.clone());
1098 }
1099 }
1100 if let Some(sources) = current.selective_re_exports.get(name) {
1101 for source in sources {
1102 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1103 return Some(definition);
1104 }
1105 }
1106 }
1107 for source in ¤t.wildcard_re_export_paths {
1108 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1109 return Some(definition);
1110 }
1111 }
1112 None
1113 }
1114
1115 pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
1119 let file = normalize_path(file);
1120 let Some(module) = self.modules.get(&file) else {
1121 return Vec::new();
1122 };
1123
1124 let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
1128
1129 for (name, srcs) in &module.selective_re_exports {
1130 sources
1131 .entry(name.clone())
1132 .or_default()
1133 .extend(srcs.iter().cloned());
1134 }
1135 for src in &module.wildcard_re_export_paths {
1136 let canonical = normalize_path(src);
1137 let Some(src_module) = self
1138 .modules
1139 .get(&canonical)
1140 .or_else(|| self.modules.get(src))
1141 else {
1142 continue;
1143 };
1144 for name in &src_module.exports {
1145 sources
1146 .entry(name.clone())
1147 .or_default()
1148 .push(canonical.clone());
1149 }
1150 }
1151
1152 for name in &module.own_exports {
1156 if let Some(entry) = sources.get_mut(name) {
1157 entry.push(file.clone());
1158 }
1159 }
1160
1161 let mut conflicts = Vec::new();
1162 for (name, mut srcs) in sources {
1163 srcs.sort();
1164 srcs.dedup();
1165 if srcs.len() > 1 {
1166 conflicts.push(ReExportConflict {
1167 name,
1168 sources: srcs,
1169 });
1170 }
1171 }
1172 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
1173 conflicts
1174 }
1175
1176 pub fn selective_import_issues(&self, file: &Path) -> Vec<SelectiveImportIssue> {
1182 let file = normalize_path(file);
1183 let Some(module) = self.modules.get(&file) else {
1184 return Vec::new();
1185 };
1186
1187 let mut out = Vec::new();
1188 for import in &module.imports {
1189 let Some(selective) = &import.selective_names else {
1190 continue;
1191 };
1192 let Some(import_path) = &import.path else {
1193 continue;
1194 };
1195 let Some(target) = self
1196 .modules
1197 .get(import_path)
1198 .or_else(|| self.modules.get(&normalize_path(import_path)))
1199 else {
1200 continue;
1201 };
1202 if target.load_error.is_some() {
1203 continue;
1204 }
1205 for name in selective {
1206 let kind = if target.exports.contains(name) {
1207 continue;
1208 } else if target.declarations.contains_key(name) {
1209 SelectiveImportIssueKind::Private
1210 } else {
1211 SelectiveImportIssueKind::Missing
1212 };
1213 out.push(SelectiveImportIssue {
1214 name: name.clone(),
1215 module: import.raw_path.clone(),
1216 span: import.import_span,
1217 kind,
1218 });
1219 }
1220 }
1221 out.sort_by(|a, b| (&a.name, &a.module, a.kind).cmp(&(&b.name, &b.module, b.kind)));
1222 out.dedup();
1223 out
1224 }
1225
1226 pub fn exported_kind(&self, file: &Path, name: &str) -> Option<DefKind> {
1230 self.exported_kind_inner(file, name, &mut HashSet::new())
1231 }
1232
1233 fn exported_kind_inner(
1234 &self,
1235 file: &Path,
1236 name: &str,
1237 visited: &mut HashSet<PathBuf>,
1238 ) -> Option<DefKind> {
1239 let file = normalize_path(file);
1240 if !visited.insert(file.clone()) {
1241 return None;
1242 }
1243 let result = self.modules.get(&file).and_then(|module| {
1244 if module.own_exports.contains(name) {
1245 return module
1246 .declarations
1247 .get(name)
1248 .map(|definition| definition.kind)
1249 .or_else(|| {
1250 stdlib_module_name(&file).and_then(|stdlib_module| {
1251 stdlib::builtin_reexports(stdlib_module)
1252 .contains(&name)
1253 .then_some(DefKind::Function)
1254 })
1255 });
1256 }
1257 if let Some(sources) = module.selective_re_exports.get(name) {
1258 for source in sources {
1259 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1260 return Some(kind);
1261 }
1262 }
1263 }
1264 for source in &module.wildcard_re_export_paths {
1265 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1266 return Some(kind);
1267 }
1268 }
1269 None
1270 });
1271 visited.remove(&file);
1272 result
1273 }
1274}
1275
1276#[derive(Debug, Clone, PartialEq, Eq)]
1279pub struct ReExportConflict {
1280 pub name: String,
1281 pub sources: Vec<PathBuf>,
1282}
1283
1284#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1286pub enum SelectiveImportIssueKind {
1287 Missing,
1289 Private,
1291}
1292
1293#[derive(Debug, Clone, PartialEq, Eq)]
1295pub struct SelectiveImportIssue {
1296 pub name: String,
1298 pub module: String,
1300 pub span: Span,
1302 pub kind: SelectiveImportIssueKind,
1304}
1305
1306impl SelectiveImportIssue {
1307 #[must_use]
1309 pub fn message(&self) -> String {
1310 match self.kind {
1311 SelectiveImportIssueKind::Missing => format!(
1312 "imported symbol `{}` does not exist in `{}`",
1313 self.name, self.module
1314 ),
1315 SelectiveImportIssueKind::Private => format!(
1316 "imported symbol `{}` is not exported by `{}` — it is defined there but not `pub`",
1317 self.name, self.module
1318 ),
1319 }
1320 }
1321
1322 #[must_use]
1324 pub fn help(&self) -> String {
1325 match self.kind {
1326 SelectiveImportIssueKind::Missing => format!(
1327 "update the import to a symbol exported by `{}`",
1328 self.module
1329 ),
1330 SelectiveImportIssueKind::Private => {
1331 format!(
1332 "mark `{}` as `pub` in `{}` to export it",
1333 self.name, self.module
1334 )
1335 }
1336 }
1337 }
1338}
1339
1340fn load_module(
1341 path: &Path,
1342 package_snapshots: &[PackageSnapshot],
1343 source_overrides: Option<&HashMap<PathBuf, String>>,
1344 retain_parsed_source: bool,
1345) -> (ModuleInfo, Option<ParsedModuleSource>) {
1346 let source = source_overrides
1347 .and_then(|overrides| overrides.get(&normalize_path(path)).cloned())
1348 .or_else(|| read_module_source(path));
1349 let Some(source) = source else {
1350 return (ModuleInfo::default(), None);
1351 };
1352 let mut lexer = harn_lexer::Lexer::new(&source);
1353 let tokens = match lexer.tokenize() {
1354 Ok(tokens) => tokens,
1355 Err(error) => {
1356 let module = ModuleInfo {
1357 load_error: Some(ModuleLoadError {
1358 message: error.to_string(),
1359 span: error.span(),
1360 }),
1361 ..ModuleInfo::default()
1362 };
1363 return (module, None);
1364 }
1365 };
1366 let mut parser = Parser::new(tokens);
1367 let program = match parser.parse() {
1368 Ok(program) => program,
1369 Err(error) => {
1370 let module = ModuleInfo {
1371 load_error: Some(ModuleLoadError {
1372 message: error.to_string(),
1373 span: error.span(),
1374 }),
1375 ..ModuleInfo::default()
1376 };
1377 return (module, None);
1378 }
1379 };
1380
1381 let mut module = ModuleInfo::default();
1382 for node in &program {
1383 collect_module_info(path, node, &mut module, package_snapshots);
1384 collect_type_declarations(node, &mut module.type_declarations);
1385 collect_callable_declarations(node, &mut module.callable_declarations);
1386 }
1387 if let Some(stdlib_module) = stdlib_module_name(path) {
1388 module.own_exports.extend(
1389 stdlib::builtin_reexports(stdlib_module)
1390 .iter()
1391 .map(|name| (*name).to_string()),
1392 );
1393 }
1394 module.exports.extend(module.own_exports.iter().cloned());
1398 module
1399 .exports
1400 .extend(module.selective_re_exports.keys().cloned());
1401 let parsed = retain_parsed_source.then_some(ParsedModuleSource { source, program });
1402 (module, parsed)
1403}
1404
1405pub fn stdlib_module_name(path: &Path) -> Option<&str> {
1408 let s = path.to_str()?;
1409 s.strip_prefix("<std>/")
1410}
1411
1412fn normalize_path(path: &Path) -> PathBuf {
1413 canonical_path(path)
1414}
1415
1416pub fn canonical_path(path: &Path) -> PathBuf {
1429 use std::sync::OnceLock;
1430 if stdlib_module_name(path).is_some() {
1431 return path.to_path_buf();
1432 }
1433 static MEMO: OnceLock<std::sync::Mutex<HashMap<PathBuf, PathBuf>>> = OnceLock::new();
1434 let memo = MEMO.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
1435 if let Some(hit) = memo
1436 .lock()
1437 .expect("canonical path memo lock poisoned")
1438 .get(path)
1439 .cloned()
1440 {
1441 return hit;
1442 }
1443 match path.canonicalize() {
1444 Ok(canonical) => {
1445 memo.lock()
1446 .expect("canonical path memo lock poisoned")
1447 .insert(path.to_path_buf(), canonical.clone());
1448 canonical
1449 }
1450 Err(_) => path.to_path_buf(),
1451 }
1452}
1453
1454#[cfg(test)]
1455#[path = "tests.rs"]
1456mod tests;