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 references;
24mod standalone;
25mod stdlib;
26mod symbol_reachability;
27mod type_dependencies;
28
29use declarations::{
30 callable_decl_name, collect_callable_declarations, collect_module_info,
31 collect_type_declarations, decl_site, type_decl_name,
32};
33pub use declarations::{public_declarations, DefKind, PublicDeclaration};
34pub use namespace_imports::NamespaceImportInfo;
35pub use namespace_signatures::NamespaceMemberSignature;
36pub use package_imports::{
37 resolve_import_path, resolve_import_path_with_guard, resolve_import_path_with_snapshot,
38 PackageImport,
39};
40pub use references::{index_references, RefSite, ReferenceEdge, ReferenceIndex};
41pub use standalone::build_with_standalone_source;
42use standalone::PackageContext;
43pub use symbol_reachability::{
44 closed_program_reachability, ExportDemand, ModuleSymbolDemand, SymbolReachability,
45};
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct DefSite {
50 pub name: String,
51 pub file: PathBuf,
52 pub kind: DefKind,
53 pub span: Span,
54}
55
56#[derive(Debug, Clone)]
58pub enum WildcardResolution {
59 Resolved(HashSet<String>),
61 Unknown,
63}
64
65#[derive(Debug, Default)]
67pub struct ModuleGraph {
68 modules: HashMap<PathBuf, ModuleInfo>,
69 _package_snapshots: Vec<PackageSnapshot>,
71}
72
73#[derive(Debug, Clone)]
74pub struct ParsedModuleSource {
75 pub source: String,
76 pub program: Vec<SNode>,
77}
78
79#[derive(Debug, Default)]
80pub struct ModuleGraphBuild {
81 pub graph: ModuleGraph,
82 pub parsed_sources: HashMap<PathBuf, ParsedModuleSource>,
83}
84
85#[derive(Debug, Default)]
86struct ModuleInfo {
87 declarations: HashMap<String, DefSite>,
90 exports: HashSet<String>,
95 own_exports: HashSet<String>,
98 selective_re_exports: HashMap<String, Vec<PathBuf>>,
105 wildcard_re_export_paths: Vec<PathBuf>,
109 namespace_re_exports: HashMap<String, PathBuf>,
116 selective_import_names: HashSet<String>,
118 imports: Vec<ImportRef>,
120 has_unresolved_wildcard_import: bool,
122 has_unresolved_selective_import: bool,
126 has_unresolved_namespace_import: bool,
128 type_declarations: Vec<SNode>,
131 callable_declarations: Vec<SNode>,
134 load_error: Option<ModuleLoadError>,
140}
141
142#[derive(Debug, Clone)]
148pub struct ModuleLoadError {
149 pub message: String,
151 pub span: Span,
153}
154
155#[derive(Debug, Clone)]
158pub struct ImportCompileFailure {
159 pub import_raw_path: String,
161 pub import_span: Span,
163 pub module_path: PathBuf,
165 pub error: ModuleLoadError,
167}
168
169#[derive(Debug, Clone)]
170struct ImportRef {
171 raw_path: String,
172 path: Option<PathBuf>,
173 selective_names: Option<HashSet<String>>,
174 namespace_alias: Option<String>,
177 is_pub: bool,
178 import_span: Span,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct ModuleImport {
184 pub raw_path: String,
186 pub resolved_path: Option<PathBuf>,
188 pub selective_names: Option<Vec<String>>,
190 pub namespace_alias: Option<String>,
192 pub is_pub: bool,
194}
195
196pub fn read_module_source(path: &Path) -> Option<String> {
202 if let Some(stdlib_module) = stdlib_module_name(path) {
203 return stdlib::get_stdlib_source(stdlib_module).map(ToString::to_string);
204 }
205 std::fs::read_to_string(path).ok()
206}
207
208pub fn build(files: &[PathBuf]) -> ModuleGraph {
214 build_inner(
215 files,
216 ParsedSourceRetention::None,
217 None,
218 PackageContext::Project,
219 )
220 .graph
221}
222
223pub fn build_with_source(file: &Path, source: &str) -> ModuleGraph {
229 let file = normalize_path(file);
230 let source_overrides = HashMap::from([(file.clone(), source.to_string())]);
231 build_inner(
232 &[file],
233 ParsedSourceRetention::None,
234 Some(&source_overrides),
235 PackageContext::Project,
236 )
237 .graph
238}
239
240pub fn build_with_parsed_sources(files: &[PathBuf]) -> ModuleGraphBuild {
246 let parsed_source_targets = files.iter().map(|file| normalize_path(file)).collect();
247 build_inner(
248 files,
249 ParsedSourceRetention::Seeds(&parsed_source_targets),
250 None,
251 PackageContext::Project,
252 )
253}
254
255pub fn build_closed_program(files: &[PathBuf]) -> ModuleGraphBuild {
261 build_inner(
262 files,
263 ParsedSourceRetention::All,
264 None,
265 PackageContext::Project,
266 )
267}
268
269#[derive(Clone, Copy)]
270enum ParsedSourceRetention<'a> {
271 None,
272 Seeds(&'a HashSet<PathBuf>),
273 All,
274}
275
276impl ParsedSourceRetention<'_> {
277 fn retains(self, path: &Path) -> bool {
278 match self {
279 Self::None => false,
280 Self::Seeds(targets) => targets.contains(path),
281 Self::All => true,
282 }
283 }
284}
285
286pub fn build_for_reference_index(
294 files: &[PathBuf],
295 source_overrides: Option<&HashMap<PathBuf, String>>,
296) -> ModuleGraphBuild {
297 build_inner(
298 files,
299 ParsedSourceRetention::All,
300 source_overrides,
301 PackageContext::Project,
302 )
303}
304
305fn build_inner(
306 files: &[PathBuf],
307 parsed_source_retention: ParsedSourceRetention<'_>,
308 source_overrides: Option<&HashMap<PathBuf, String>>,
309 package_context: PackageContext,
310) -> ModuleGraphBuild {
311 let package_snapshots = match package_context {
312 PackageContext::Project => acquire_package_snapshots(files),
313 PackageContext::Standalone => Vec::new(),
314 };
315 let mut modules: HashMap<PathBuf, ModuleInfo> = HashMap::new();
316 let mut parsed_sources: HashMap<PathBuf, ParsedModuleSource> = HashMap::new();
317 let mut seen: HashSet<PathBuf> = HashSet::new();
318 let mut wave: Vec<PathBuf> = Vec::new();
319 for file in files {
320 let canonical = normalize_path(file);
321 if seen.insert(canonical.clone()) {
322 wave.push(canonical);
323 }
324 }
325 while !wave.is_empty() {
333 let loaded = load_wave(
334 &wave,
335 &package_snapshots,
336 parsed_source_retention,
337 source_overrides,
338 );
339 let mut next_wave: Vec<PathBuf> = Vec::new();
340 for (path, (module, parsed)) in wave.drain(..).zip(loaded) {
341 if parsed_source_retention.retains(&path) {
342 if let Some(parsed) = parsed {
343 parsed_sources.insert(path.clone(), parsed);
344 }
345 }
346 for import in &module.imports {
363 if let Some(import_path) = &import.path {
364 let canonical = normalize_path(import_path);
365 if seen.insert(canonical.clone()) {
366 next_wave.push(canonical);
367 }
368 }
369 }
370 modules.insert(path, module);
371 }
372 wave = next_wave;
373 }
374 resolve_re_exports(&mut modules);
375 ModuleGraphBuild {
376 graph: ModuleGraph {
377 modules,
378 _package_snapshots: package_snapshots,
379 },
380 parsed_sources,
381 }
382}
383
384pub const MODULE_GRAPH_JOBS_ENV: &str = "HARN_MODULE_GRAPH_JOBS";
387
388fn load_wave(
391 paths: &[PathBuf],
392 package_snapshots: &[PackageSnapshot],
393 parsed_source_retention: ParsedSourceRetention<'_>,
394 source_overrides: Option<&HashMap<PathBuf, String>>,
395) -> Vec<(ModuleInfo, Option<ParsedModuleSource>)> {
396 const MIN_PARALLEL_WAVE: usize = 8;
397 let configured = std::env::var(MODULE_GRAPH_JOBS_ENV)
398 .ok()
399 .and_then(|value| value.parse::<usize>().ok())
400 .filter(|&jobs| jobs > 0);
401 let workers = configured
402 .unwrap_or_else(|| {
403 std::thread::available_parallelism()
404 .map(std::num::NonZeroUsize::get)
405 .unwrap_or(1)
406 })
407 .min(paths.len());
408 if workers <= 1 || paths.len() < MIN_PARALLEL_WAVE {
409 return paths
410 .iter()
411 .map(|path| {
412 load_module(
413 path,
414 package_snapshots,
415 source_overrides,
416 parsed_source_retention.retains(path),
417 )
418 })
419 .collect();
420 }
421 let next = std::sync::atomic::AtomicUsize::new(0);
422 let mut produced: Vec<(usize, (ModuleInfo, Option<ParsedModuleSource>))> =
423 std::thread::scope(|scope| {
424 let handles: Vec<_> = (0..workers)
425 .map(|_| {
426 scope.spawn(|| {
427 let mut local = Vec::new();
428 loop {
429 let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
430 let Some(path) = paths.get(index) else {
431 break;
432 };
433 local.push((
434 index,
435 load_module(
436 path,
437 package_snapshots,
438 source_overrides,
439 parsed_source_retention.retains(path),
440 ),
441 ));
442 }
443 local
444 })
445 })
446 .collect();
447 handles
448 .into_iter()
449 .flat_map(|handle| match handle.join() {
450 Ok(local) => local,
451 Err(panic) => std::panic::resume_unwind(panic),
452 })
453 .collect()
454 });
455 produced.sort_unstable_by_key(|(index, _)| *index);
456 produced.into_iter().map(|(_, loaded)| loaded).collect()
457}
458
459fn resolve_re_exports(modules: &mut HashMap<PathBuf, ModuleInfo>) {
464 let keys: Vec<PathBuf> = modules.keys().cloned().collect();
465 loop {
466 let mut changed = false;
467 for path in &keys {
468 let wildcard_paths = modules
471 .get(path)
472 .map(|m| m.wildcard_re_export_paths.clone())
473 .unwrap_or_default();
474 if wildcard_paths.is_empty() {
475 continue;
476 }
477 let mut additions: Vec<String> = Vec::new();
478 for src in &wildcard_paths {
479 let src_canonical = normalize_path(src);
480 if let Some(src_module) = modules.get(src).or_else(|| modules.get(&src_canonical)) {
481 additions.extend(src_module.exports.iter().cloned());
482 }
483 }
484 if let Some(module) = modules.get_mut(path) {
485 for name in additions {
486 if module.exports.insert(name) {
487 changed = true;
488 }
489 }
490 }
491 }
492 if !changed {
493 break;
494 }
495 }
496}
497
498impl ModuleGraph {
499 pub fn module_paths(&self) -> Vec<PathBuf> {
504 let mut paths: Vec<PathBuf> = self.modules.keys().cloned().collect();
505 paths.sort();
506 paths
507 }
508
509 pub fn contains_module(&self, path: &Path) -> bool {
512 self.modules.contains_key(path) || self.modules.contains_key(&normalize_path(path))
513 }
514
515 pub fn all_selective_import_names(&self) -> HashSet<&str> {
517 let mut names = HashSet::new();
518 for module in self.modules.values() {
519 for name in &module.selective_import_names {
520 names.insert(name.as_str());
521 }
522 }
523 names
524 }
525
526 pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
529 let target = normalize_path(target);
530 let mut out: Vec<PathBuf> = self
531 .modules
532 .iter()
533 .filter(|(_, info)| {
534 info.imports.iter().any(|import| {
535 import
536 .path
537 .as_ref()
538 .is_some_and(|p| normalize_path(p) == target)
539 })
540 })
541 .map(|(path, _)| path.clone())
542 .collect();
543 out.sort();
544 out
545 }
546
547 pub fn transitive_importers_of(&self, target: &Path) -> Vec<PathBuf> {
554 let target = normalize_path(target);
555 let mut visited = HashSet::from([target.clone()]);
556 let mut pending = vec![target];
557 let mut out = Vec::new();
558
559 while let Some(current) = pending.pop() {
560 for importer in self.importers_of(¤t) {
561 let importer = normalize_path(&importer);
562 if visited.insert(importer.clone()) {
563 pending.push(importer.clone());
564 out.push(importer);
565 }
566 }
567 }
568
569 out.sort();
570 out
571 }
572
573 pub fn imports_for_module(&self, file: &Path) -> Vec<ModuleImport> {
575 let file = normalize_path(file);
576 let Some(module) = self.modules.get(&file) else {
577 return Vec::new();
578 };
579 let mut imports: Vec<ModuleImport> = module
580 .imports
581 .iter()
582 .map(|import| {
583 let mut selective_names = import
584 .selective_names
585 .as_ref()
586 .map(|names| names.iter().cloned().collect::<Vec<_>>());
587 if let Some(names) = selective_names.as_mut() {
588 names.sort();
589 }
590 ModuleImport {
591 raw_path: import.raw_path.clone(),
592 resolved_path: import.path.as_ref().map(|path| normalize_path(path)),
593 selective_names,
594 namespace_alias: import.namespace_alias.clone(),
595 is_pub: import.is_pub,
596 }
597 })
598 .collect();
599 imports.sort_by(|left, right| {
600 left.raw_path
601 .cmp(&right.raw_path)
602 .then_with(|| left.selective_names.cmp(&right.selective_names))
603 .then_with(|| left.namespace_alias.cmp(&right.namespace_alias))
604 .then_with(|| left.resolved_path.cmp(&right.resolved_path))
605 });
606 imports
607 }
608
609 pub fn exports_for_module(&self, file: &Path) -> Vec<String> {
611 let file = normalize_path(file);
612 let Some(module) = self.modules.get(&file) else {
613 return Vec::new();
614 };
615 let mut exports: Vec<String> = module.exports.iter().cloned().collect();
616 exports.sort();
617 exports
618 }
619
620 pub fn wildcard_exports_for(&self, file: &Path) -> WildcardResolution {
625 let file = normalize_path(file);
626 let Some(module) = self.modules.get(&file) else {
627 return WildcardResolution::Unknown;
628 };
629 if module.has_unresolved_wildcard_import {
630 return WildcardResolution::Unknown;
631 }
632
633 let mut names = HashSet::new();
634 for import in module
635 .imports
636 .iter()
637 .filter(|import| import.selective_names.is_none())
638 {
639 let Some(import_path) = &import.path else {
640 return WildcardResolution::Unknown;
641 };
642 let imported = self.modules.get(import_path).or_else(|| {
643 let normalized = normalize_path(import_path);
644 self.modules.get(&normalized)
645 });
646 let Some(imported) = imported else {
647 return WildcardResolution::Unknown;
648 };
649 names.extend(imported.exports.iter().cloned());
650 }
651 WildcardResolution::Resolved(names)
652 }
653
654 #[must_use]
675 pub fn import_compile_failures(&self, file: &Path) -> Vec<ImportCompileFailure> {
676 let file = normalize_path(file);
677 let Some(module) = self.modules.get(&file) else {
678 return Vec::new();
679 };
680 let mut failures = Vec::new();
681 for import in &module.imports {
682 let Some(import_path) = &import.path else {
683 continue;
684 };
685 let Some(target) = self
686 .modules
687 .get(import_path)
688 .or_else(|| self.modules.get(&normalize_path(import_path)))
689 else {
690 continue;
691 };
692 if let Some(error) = &target.load_error {
693 failures.push(ImportCompileFailure {
694 import_raw_path: import.raw_path.clone(),
695 import_span: import.import_span,
696 module_path: normalize_path(import_path),
697 error: error.clone(),
698 });
699 }
700 }
701 failures
702 }
703
704 pub fn imported_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
705 let file = normalize_path(file);
706 let module = self.modules.get(&file)?;
707 if module.has_unresolved_wildcard_import
708 || module.has_unresolved_selective_import
709 || module.has_unresolved_namespace_import
710 {
711 return None;
712 }
713
714 let mut names = HashSet::new();
715 for import in &module.imports {
716 if let Some(alias) = &import.namespace_alias {
718 names.insert(alias.clone());
719 continue;
720 }
721 let import_path = import.path.as_ref()?;
722 let imported = self
723 .modules
724 .get(import_path)
725 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
726 if imported.load_error.is_some() {
732 return None;
733 }
734 match &import.selective_names {
735 None => {
736 names.extend(imported.exports.iter().cloned());
737 }
738 Some(selective) => {
739 for name in selective {
748 if imported.declarations.contains_key(name)
749 || imported.exports.contains(name)
750 {
751 names.insert(name.clone());
752 }
753 }
754 }
755 }
756 }
757 Some(names)
758 }
759
760 pub fn imported_names_by_kind_for_file(
765 &self,
766 file: &Path,
767 kind: DefKind,
768 ) -> Option<HashSet<String>> {
769 let file = normalize_path(file);
770 let module = self.modules.get(&file)?;
771 if module.has_unresolved_wildcard_import
772 || module.has_unresolved_selective_import
773 || module.has_unresolved_namespace_import
774 {
775 return None;
776 }
777
778 let mut names = HashSet::new();
779 for import in &module.imports {
780 if import.namespace_alias.is_some() {
782 continue;
783 }
784 let import_path = import.path.as_ref()?;
785 let imported_names: Vec<String> = match &import.selective_names {
786 Some(selective) => selective.iter().cloned().collect(),
787 None => self
788 .modules
789 .get(import_path)
790 .or_else(|| self.modules.get(&normalize_path(import_path)))?
791 .exports
792 .iter()
793 .cloned()
794 .collect(),
795 };
796 for name in imported_names {
797 if self.exported_kind(import_path, &name) == Some(kind) {
798 names.insert(name);
799 }
800 }
801 }
802 Some(names)
803 }
804
805 pub fn imported_callable_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
810 let mut names = HashSet::new();
811 for kind in [
812 DefKind::Function,
813 DefKind::Pipeline,
814 DefKind::Tool,
815 DefKind::Struct,
816 ] {
817 names.extend(self.imported_names_by_kind_for_file(file, kind)?);
818 }
819 Some(names)
820 }
821
822 pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
826 let file = normalize_path(file);
827 let module = self.modules.get(&file)?;
828 if module.has_unresolved_wildcard_import
829 || module.has_unresolved_selective_import
830 || module.has_unresolved_namespace_import
831 {
832 return None;
833 }
834
835 let mut decls = Vec::new();
836 let mut seen = HashSet::new();
837 for import in &module.imports {
838 if import.namespace_alias.is_some() {
840 continue;
841 }
842 let import_path = import.path.as_ref()?;
843 let imported = self
844 .modules
845 .get(import_path)
846 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
847 if imported.load_error.is_some() {
853 return None;
854 }
855 let mut names_to_collect: Vec<String> = match &import.selective_names {
856 None => imported.exports.iter().cloned().collect(),
857 Some(selective) => selective.iter().cloned().collect(),
858 };
859 names_to_collect.sort();
860 for name in &names_to_collect {
861 let mut visited = HashSet::new();
862 if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
863 let origin = self
864 .export_definition_of(import_path, name)
865 .map_or_else(|| import_path.clone(), |definition| definition.file);
866 self.extend_type_dependency(&origin, &decl, &mut decls, &mut seen);
867 }
868 }
869 for ty_decl in &imported.type_declarations {
879 if type_decl_name(ty_decl).is_some() {
880 self.extend_type_dependency(import_path, ty_decl, &mut decls, &mut seen);
881 }
882 }
883
884 for name in &names_to_collect {
885 let mut visited = HashSet::new();
886 let Some(callable) =
887 self.find_exported_callable_decl(import_path, name, &mut visited)
888 else {
889 continue;
890 };
891 let origin = self
892 .export_definition_of(import_path, name)
893 .map_or_else(|| import_path.clone(), |definition| definition.file);
894 self.extend_callable_type_dependencies(&origin, &callable, &mut decls, &mut seen);
895 }
896 }
897 Some(decls)
898 }
899
900 pub fn imported_callable_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
904 let file = normalize_path(file);
905 let module = self.modules.get(&file)?;
906 if module.has_unresolved_wildcard_import
907 || module.has_unresolved_selective_import
908 || module.has_unresolved_namespace_import
909 {
910 return None;
911 }
912
913 let mut decls = Vec::new();
914 for import in &module.imports {
915 if import.namespace_alias.is_some() {
917 continue;
918 }
919 let import_path = import.path.as_ref()?;
920 let imported = self
921 .modules
922 .get(import_path)
923 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
924 if imported.load_error.is_some() {
930 return None;
931 }
932 let selective_import = import.selective_names.is_some();
933 let names_to_collect: Vec<String> = match &import.selective_names {
934 None => imported.exports.iter().cloned().collect(),
935 Some(selective) => selective.iter().cloned().collect(),
936 };
937 for name in &names_to_collect {
938 if selective_import || imported.own_exports.contains(name) {
939 if let Some(decl) = imported
940 .callable_declarations
941 .iter()
942 .find(|decl| callable_decl_name(decl) == Some(name.as_str()))
943 {
944 decls.push(decl.clone());
945 continue;
946 }
947 }
948 let mut visited = HashSet::new();
949 if let Some(decl) =
950 self.find_exported_callable_decl(import_path, name, &mut visited)
951 {
952 decls.push(decl);
953 }
954 }
955 }
956 Some(decls)
957 }
958
959 fn find_exported_type_decl(
962 &self,
963 path: &Path,
964 name: &str,
965 visited: &mut HashSet<PathBuf>,
966 ) -> Option<SNode> {
967 let canonical = normalize_path(path);
968 if !visited.insert(canonical.clone()) {
969 return None;
970 }
971 let module = self
972 .modules
973 .get(&canonical)
974 .or_else(|| self.modules.get(path))?;
975 for decl in &module.type_declarations {
976 if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
977 return Some(decl.clone());
978 }
979 }
980 if let Some(sources) = module.selective_re_exports.get(name) {
981 for source in sources {
982 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
983 return Some(decl);
984 }
985 }
986 }
987 for source in &module.wildcard_re_export_paths {
988 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
989 return Some(decl);
990 }
991 }
992 None
993 }
994
995 fn find_exported_callable_decl(
996 &self,
997 path: &Path,
998 name: &str,
999 visited: &mut HashSet<PathBuf>,
1000 ) -> Option<SNode> {
1001 let canonical = normalize_path(path);
1002 if !visited.insert(canonical.clone()) {
1003 return None;
1004 }
1005 let module = self
1006 .modules
1007 .get(&canonical)
1008 .or_else(|| self.modules.get(path))?;
1009 for decl in &module.callable_declarations {
1010 if callable_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
1011 return Some(decl.clone());
1012 }
1013 }
1014 if let Some(sources) = module.selective_re_exports.get(name) {
1015 for source in sources {
1016 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
1017 return Some(decl);
1018 }
1019 }
1020 }
1021 for source in &module.wildcard_re_export_paths {
1022 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
1023 return Some(decl);
1024 }
1025 }
1026 None
1027 }
1028
1029 pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
1035 let mut visited = HashSet::new();
1036 self.definition_of_inner(file, name, &mut visited)
1037 }
1038
1039 pub fn export_definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
1046 let mut visited = HashSet::new();
1047 self.export_definition_of_inner(file, name, &mut visited)
1048 }
1049
1050 pub fn declared_names_for_file(&self, file: &Path) -> Option<Vec<&str>> {
1054 let module = self.modules.get(&normalize_path(file))?;
1055 let mut names: Vec<&str> = module.declarations.keys().map(String::as_str).collect();
1056 names.sort_unstable();
1057 Some(names)
1058 }
1059
1060 fn definition_of_inner(
1061 &self,
1062 file: &Path,
1063 name: &str,
1064 visited: &mut HashSet<PathBuf>,
1065 ) -> Option<DefSite> {
1066 let file = normalize_path(file);
1067 if !visited.insert(file.clone()) {
1068 return None;
1069 }
1070 let current = self.modules.get(&file)?;
1071
1072 if let Some(local) = current.declarations.get(name) {
1073 return Some(local.clone());
1074 }
1075
1076 if let Some(sources) = current.selective_re_exports.get(name) {
1081 for source in sources {
1082 if let Some(def) = self.definition_of_inner(source, name, visited) {
1083 return Some(def);
1084 }
1085 }
1086 }
1087
1088 for source in ¤t.wildcard_re_export_paths {
1090 if let Some(def) = self.definition_of_inner(source, name, visited) {
1091 return Some(def);
1092 }
1093 }
1094
1095 for import in ¤t.imports {
1097 let Some(selective_names) = &import.selective_names else {
1098 continue;
1099 };
1100 if !selective_names.contains(name) {
1101 continue;
1102 }
1103 if let Some(path) = &import.path {
1104 if let Some(def) = self.definition_of_inner(path, name, visited) {
1105 return Some(def);
1106 }
1107 }
1108 }
1109
1110 for import in ¤t.imports {
1112 if import.selective_names.is_some() || import.namespace_alias.is_some() {
1113 continue;
1114 }
1115 if let Some(path) = &import.path {
1116 if let Some(def) = self.definition_of_inner(path, name, visited) {
1117 return Some(def);
1118 }
1119 }
1120 }
1121
1122 None
1123 }
1124
1125 fn export_definition_of_inner(
1126 &self,
1127 file: &Path,
1128 name: &str,
1129 visited: &mut HashSet<PathBuf>,
1130 ) -> Option<DefSite> {
1131 let file = normalize_path(file);
1132 if !visited.insert(file.clone()) {
1133 return None;
1134 }
1135 let current = self.modules.get(&file)?;
1136
1137 if current.own_exports.contains(name) {
1138 if let Some(local) = current.declarations.get(name) {
1139 return Some(local.clone());
1140 }
1141 }
1142 if let Some(sources) = current.selective_re_exports.get(name) {
1143 for source in sources {
1144 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1145 return Some(definition);
1146 }
1147 }
1148 }
1149 for source in ¤t.wildcard_re_export_paths {
1150 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1151 return Some(definition);
1152 }
1153 }
1154 None
1155 }
1156
1157 pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
1161 let file = normalize_path(file);
1162 let Some(module) = self.modules.get(&file) else {
1163 return Vec::new();
1164 };
1165
1166 let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
1170
1171 for (name, srcs) in &module.selective_re_exports {
1172 sources
1173 .entry(name.clone())
1174 .or_default()
1175 .extend(srcs.iter().cloned());
1176 }
1177 for src in &module.wildcard_re_export_paths {
1178 let canonical = normalize_path(src);
1179 let Some(src_module) = self
1180 .modules
1181 .get(&canonical)
1182 .or_else(|| self.modules.get(src))
1183 else {
1184 continue;
1185 };
1186 for name in &src_module.exports {
1187 sources
1188 .entry(name.clone())
1189 .or_default()
1190 .push(canonical.clone());
1191 }
1192 }
1193
1194 for name in &module.own_exports {
1198 if let Some(entry) = sources.get_mut(name) {
1199 entry.push(file.clone());
1200 }
1201 }
1202
1203 let mut conflicts = Vec::new();
1204 for (name, mut srcs) in sources {
1205 srcs.sort();
1206 srcs.dedup();
1207 if srcs.len() > 1 {
1208 conflicts.push(ReExportConflict {
1209 name,
1210 sources: srcs,
1211 });
1212 }
1213 }
1214 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
1215 conflicts
1216 }
1217
1218 pub fn selective_import_issues(&self, file: &Path) -> Vec<SelectiveImportIssue> {
1224 let file = normalize_path(file);
1225 let Some(module) = self.modules.get(&file) else {
1226 return Vec::new();
1227 };
1228
1229 let mut out = Vec::new();
1230 for import in &module.imports {
1231 let Some(selective) = &import.selective_names else {
1232 continue;
1233 };
1234 let Some(import_path) = &import.path else {
1235 continue;
1236 };
1237 let Some(target) = self
1238 .modules
1239 .get(import_path)
1240 .or_else(|| self.modules.get(&normalize_path(import_path)))
1241 else {
1242 continue;
1243 };
1244 if target.load_error.is_some() {
1245 continue;
1246 }
1247 for name in selective {
1248 let kind = if target.exports.contains(name) {
1249 continue;
1250 } else if target.declarations.contains_key(name) {
1251 SelectiveImportIssueKind::Private
1252 } else {
1253 SelectiveImportIssueKind::Missing
1254 };
1255 out.push(SelectiveImportIssue {
1256 name: name.clone(),
1257 module: import.raw_path.clone(),
1258 span: import.import_span,
1259 kind,
1260 });
1261 }
1262 }
1263 out.sort_by(|a, b| (&a.name, &a.module, a.kind).cmp(&(&b.name, &b.module, b.kind)));
1264 out.dedup();
1265 out
1266 }
1267
1268 pub fn exported_kind(&self, file: &Path, name: &str) -> Option<DefKind> {
1272 self.exported_kind_inner(file, name, &mut HashSet::new())
1273 }
1274
1275 fn exported_kind_inner(
1276 &self,
1277 file: &Path,
1278 name: &str,
1279 visited: &mut HashSet<PathBuf>,
1280 ) -> Option<DefKind> {
1281 let file = normalize_path(file);
1282 if !visited.insert(file.clone()) {
1283 return None;
1284 }
1285 let result = self.modules.get(&file).and_then(|module| {
1286 if module.own_exports.contains(name) {
1287 return module
1288 .declarations
1289 .get(name)
1290 .map(|definition| definition.kind)
1291 .or_else(|| {
1292 stdlib_module_name(&file).and_then(|stdlib_module| {
1293 stdlib::builtin_reexports(stdlib_module)
1294 .contains(&name)
1295 .then_some(DefKind::Function)
1296 })
1297 });
1298 }
1299 if let Some(sources) = module.selective_re_exports.get(name) {
1300 for source in sources {
1301 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1302 return Some(kind);
1303 }
1304 }
1305 }
1306 for source in &module.wildcard_re_export_paths {
1307 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1308 return Some(kind);
1309 }
1310 }
1311 None
1312 });
1313 visited.remove(&file);
1314 result
1315 }
1316}
1317
1318#[derive(Debug, Clone, PartialEq, Eq)]
1321pub struct ReExportConflict {
1322 pub name: String,
1323 pub sources: Vec<PathBuf>,
1324}
1325
1326#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1328pub enum SelectiveImportIssueKind {
1329 Missing,
1331 Private,
1333}
1334
1335#[derive(Debug, Clone, PartialEq, Eq)]
1337pub struct SelectiveImportIssue {
1338 pub name: String,
1340 pub module: String,
1342 pub span: Span,
1344 pub kind: SelectiveImportIssueKind,
1346}
1347
1348impl SelectiveImportIssue {
1349 #[must_use]
1351 pub fn message(&self) -> String {
1352 match self.kind {
1353 SelectiveImportIssueKind::Missing => format!(
1354 "imported symbol `{}` does not exist in `{}`",
1355 self.name, self.module
1356 ),
1357 SelectiveImportIssueKind::Private => format!(
1358 "imported symbol `{}` is not exported by `{}` — it is defined there but not `pub`",
1359 self.name, self.module
1360 ),
1361 }
1362 }
1363
1364 #[must_use]
1366 pub fn help(&self) -> String {
1367 match self.kind {
1368 SelectiveImportIssueKind::Missing => format!(
1369 "update the import to a symbol exported by `{}`",
1370 self.module
1371 ),
1372 SelectiveImportIssueKind::Private => {
1373 format!(
1374 "mark `{}` as `pub` in `{}` to export it",
1375 self.name, self.module
1376 )
1377 }
1378 }
1379 }
1380}
1381
1382fn load_module(
1383 path: &Path,
1384 package_snapshots: &[PackageSnapshot],
1385 source_overrides: Option<&HashMap<PathBuf, String>>,
1386 retain_parsed_source: bool,
1387) -> (ModuleInfo, Option<ParsedModuleSource>) {
1388 let source = source_overrides
1389 .and_then(|overrides| overrides.get(&normalize_path(path)).cloned())
1390 .or_else(|| read_module_source(path));
1391 let Some(source) = source else {
1392 return (ModuleInfo::default(), None);
1393 };
1394 let mut lexer = harn_lexer::Lexer::new(&source);
1395 let tokens = match lexer.tokenize() {
1396 Ok(tokens) => tokens,
1397 Err(error) => {
1398 let module = ModuleInfo {
1399 load_error: Some(ModuleLoadError {
1400 message: error.to_string(),
1401 span: error.span(),
1402 }),
1403 ..ModuleInfo::default()
1404 };
1405 return (module, None);
1406 }
1407 };
1408 let mut parser = Parser::new(tokens);
1409 let program = match parser.parse() {
1410 Ok(program) => program,
1411 Err(error) => {
1412 let module = ModuleInfo {
1413 load_error: Some(ModuleLoadError {
1414 message: error.to_string(),
1415 span: error.span(),
1416 }),
1417 ..ModuleInfo::default()
1418 };
1419 return (module, None);
1420 }
1421 };
1422
1423 let mut module = ModuleInfo::default();
1424 for node in &program {
1425 collect_module_info(path, node, &mut module, package_snapshots);
1426 collect_type_declarations(node, &mut module.type_declarations);
1427 collect_callable_declarations(node, &mut module.callable_declarations);
1428 }
1429 if let Some(stdlib_module) = stdlib_module_name(path) {
1430 module.own_exports.extend(
1431 stdlib::builtin_reexports(stdlib_module)
1432 .iter()
1433 .map(|name| (*name).to_string()),
1434 );
1435 }
1436 module.exports.extend(module.own_exports.iter().cloned());
1440 module
1441 .exports
1442 .extend(module.selective_re_exports.keys().cloned());
1443 let parsed = retain_parsed_source.then_some(ParsedModuleSource { source, program });
1444 (module, parsed)
1445}
1446
1447pub fn stdlib_module_name(path: &Path) -> Option<&str> {
1450 let s = path.to_str()?;
1451 s.strip_prefix("<std>/")
1452}
1453
1454fn normalize_path(path: &Path) -> PathBuf {
1455 canonical_path(path)
1456}
1457
1458pub fn canonical_path(path: &Path) -> PathBuf {
1471 use std::sync::OnceLock;
1472 if stdlib_module_name(path).is_some() {
1473 return path.to_path_buf();
1474 }
1475 static MEMO: OnceLock<std::sync::Mutex<HashMap<PathBuf, PathBuf>>> = OnceLock::new();
1476 let memo = MEMO.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
1477 if let Some(hit) = memo
1478 .lock()
1479 .expect("canonical path memo lock poisoned")
1480 .get(path)
1481 .cloned()
1482 {
1483 return hit;
1484 }
1485 match path.canonicalize() {
1486 Ok(canonical) => {
1487 memo.lock()
1488 .expect("canonical path memo lock poisoned")
1489 .insert(path.to_path_buf(), canonical.clone());
1490 canonical
1491 }
1492 Err(_) => path.to_path_buf(),
1493 }
1494}
1495
1496#[cfg(test)]
1497#[path = "tests.rs"]
1498mod tests;