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