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;
29mod visibility;
30
31use declarations::{
32 callable_decl_name, collect_callable_declarations, collect_module_info,
33 collect_type_declarations, decl_site, type_decl_name,
34};
35pub use declarations::{public_declarations, sibling_declarations, DefKind, PublicDeclaration};
36pub use namespace_imports::NamespaceImportInfo;
37pub use namespace_signatures::NamespaceMemberSignature;
38pub use package_imports::{
39 resolve_import_path, resolve_import_path_with_guard, resolve_import_path_with_snapshot,
40 unresolved_package_alias, PackageImport,
41};
42pub use references::{index_references, RefSite, ReferenceEdge, ReferenceIndex};
43pub use standalone::build_with_standalone_source;
44use standalone::PackageContext;
45pub use symbol_reachability::{
46 closed_program_reachability, ExportDemand, ModuleSymbolDemand, SymbolReachability,
47};
48pub use visibility::{sibling_directory_access, sibling_module_access};
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct DefSite {
53 pub name: String,
54 pub file: PathBuf,
55 pub kind: DefKind,
56 pub span: Span,
57}
58
59#[derive(Debug, Clone)]
61pub enum WildcardResolution {
62 Resolved(HashSet<String>),
64 Unknown,
66}
67
68#[derive(Debug, Default)]
70pub struct ModuleGraph {
71 modules: HashMap<PathBuf, ModuleInfo>,
72 _package_snapshots: Vec<PackageSnapshot>,
74}
75
76#[derive(Debug, Clone)]
77pub struct ParsedModuleSource {
78 pub source: String,
79 pub program: Vec<SNode>,
80}
81
82#[derive(Debug, Default)]
83pub struct ModuleGraphBuild {
84 pub graph: ModuleGraph,
85 pub parsed_sources: HashMap<PathBuf, ParsedModuleSource>,
86}
87
88#[derive(Debug, Default)]
89struct ModuleInfo {
90 declarations: HashMap<String, DefSite>,
93 exports: HashSet<String>,
98 own_exports: HashSet<String>,
101 sibling_exports: HashSet<String>,
104 selective_re_exports: HashMap<String, Vec<PathBuf>>,
111 wildcard_re_export_paths: Vec<PathBuf>,
115 namespace_re_exports: HashMap<String, PathBuf>,
122 selective_import_names: HashSet<String>,
124 imports: Vec<ImportRef>,
126 has_unresolved_wildcard_import: bool,
128 has_unresolved_selective_import: bool,
132 has_unresolved_namespace_import: bool,
134 type_declarations: Vec<SNode>,
137 callable_declarations: Vec<SNode>,
140 load_error: Option<ModuleLoadError>,
146}
147
148#[derive(Debug, Clone)]
154pub struct ModuleLoadError {
155 pub message: String,
157 pub span: Span,
159}
160
161#[derive(Debug, Clone)]
164pub struct ImportCompileFailure {
165 pub import_raw_path: String,
167 pub import_span: Span,
169 pub module_path: PathBuf,
171 pub error: ModuleLoadError,
173}
174
175#[derive(Debug, Clone)]
176struct ImportRef {
177 raw_path: String,
178 path: Option<PathBuf>,
179 selective_names: Option<HashSet<String>>,
180 namespace_alias: Option<String>,
183 is_pub: bool,
184 import_span: Span,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct ModuleImport {
190 pub raw_path: String,
192 pub resolved_path: Option<PathBuf>,
194 pub selective_names: Option<Vec<String>>,
196 pub namespace_alias: Option<String>,
198 pub is_pub: bool,
200}
201
202pub fn read_module_source(path: &Path) -> Option<String> {
208 if let Some(stdlib_module) = stdlib_module_name(path) {
209 return stdlib::get_stdlib_source(stdlib_module).map(ToString::to_string);
210 }
211 std::fs::read_to_string(path).ok()
212}
213
214pub fn build(files: &[PathBuf]) -> ModuleGraph {
220 build_inner(
221 files,
222 ParsedSourceRetention::None,
223 None,
224 PackageContext::Project,
225 )
226 .graph
227}
228
229pub fn build_with_source(file: &Path, source: &str) -> ModuleGraph {
235 let file = normalize_path(file);
236 let source_overrides = HashMap::from([(file.clone(), source.to_string())]);
237 build_inner(
238 &[file],
239 ParsedSourceRetention::None,
240 Some(&source_overrides),
241 PackageContext::Project,
242 )
243 .graph
244}
245
246pub fn build_with_parsed_sources(files: &[PathBuf]) -> ModuleGraphBuild {
252 let parsed_source_targets = files.iter().map(|file| normalize_path(file)).collect();
253 build_inner(
254 files,
255 ParsedSourceRetention::Seeds(&parsed_source_targets),
256 None,
257 PackageContext::Project,
258 )
259}
260
261pub fn build_closed_program(files: &[PathBuf]) -> ModuleGraphBuild {
267 build_inner(
268 files,
269 ParsedSourceRetention::All,
270 None,
271 PackageContext::Project,
272 )
273}
274
275#[derive(Clone, Copy)]
276enum ParsedSourceRetention<'a> {
277 None,
278 Seeds(&'a HashSet<PathBuf>),
279 All,
280}
281
282impl ParsedSourceRetention<'_> {
283 fn retains(self, path: &Path) -> bool {
284 match self {
285 Self::None => false,
286 Self::Seeds(targets) => targets.contains(path),
287 Self::All => true,
288 }
289 }
290}
291
292pub fn build_for_reference_index(
300 files: &[PathBuf],
301 source_overrides: Option<&HashMap<PathBuf, String>>,
302) -> ModuleGraphBuild {
303 build_inner(
304 files,
305 ParsedSourceRetention::All,
306 source_overrides,
307 PackageContext::Project,
308 )
309}
310
311fn build_inner(
312 files: &[PathBuf],
313 parsed_source_retention: ParsedSourceRetention<'_>,
314 source_overrides: Option<&HashMap<PathBuf, String>>,
315 package_context: PackageContext,
316) -> ModuleGraphBuild {
317 let package_snapshots = match package_context {
318 PackageContext::Project => acquire_package_snapshots(files),
319 PackageContext::Standalone => Vec::new(),
320 };
321 let mut modules: HashMap<PathBuf, ModuleInfo> = HashMap::new();
322 let mut parsed_sources: HashMap<PathBuf, ParsedModuleSource> = HashMap::new();
323 let mut seen: HashSet<PathBuf> = HashSet::new();
324 let mut wave: Vec<PathBuf> = Vec::new();
325 for file in files {
326 let canonical = normalize_path(file);
327 if seen.insert(canonical.clone()) {
328 wave.push(canonical);
329 }
330 }
331 while !wave.is_empty() {
339 let loaded = load_wave(
340 &wave,
341 &package_snapshots,
342 parsed_source_retention,
343 source_overrides,
344 );
345 let mut next_wave: Vec<PathBuf> = Vec::new();
346 for (path, (module, parsed)) in wave.drain(..).zip(loaded) {
347 if parsed_source_retention.retains(&path) {
348 if let Some(parsed) = parsed {
349 parsed_sources.insert(path.clone(), parsed);
350 }
351 }
352 for import in &module.imports {
369 if let Some(import_path) = &import.path {
370 let canonical = normalize_path(import_path);
371 if seen.insert(canonical.clone()) {
372 next_wave.push(canonical);
373 }
374 }
375 }
376 modules.insert(path, module);
377 }
378 wave = next_wave;
379 }
380 resolve_re_exports(&mut modules);
381 ModuleGraphBuild {
382 graph: ModuleGraph {
383 modules,
384 _package_snapshots: package_snapshots,
385 },
386 parsed_sources,
387 }
388}
389
390pub const MODULE_GRAPH_JOBS_ENV: &str = "HARN_MODULE_GRAPH_JOBS";
393
394fn load_wave(
397 paths: &[PathBuf],
398 package_snapshots: &[PackageSnapshot],
399 parsed_source_retention: ParsedSourceRetention<'_>,
400 source_overrides: Option<&HashMap<PathBuf, String>>,
401) -> Vec<(ModuleInfo, Option<ParsedModuleSource>)> {
402 const MIN_PARALLEL_WAVE: usize = 8;
403 let configured = std::env::var(MODULE_GRAPH_JOBS_ENV)
404 .ok()
405 .and_then(|value| value.parse::<usize>().ok())
406 .filter(|&jobs| jobs > 0);
407 let workers = configured
408 .unwrap_or_else(|| {
409 std::thread::available_parallelism()
410 .map(std::num::NonZeroUsize::get)
411 .unwrap_or(1)
412 })
413 .min(paths.len());
414 if workers <= 1 || paths.len() < MIN_PARALLEL_WAVE {
415 return paths
416 .iter()
417 .map(|path| {
418 load_module(
419 path,
420 package_snapshots,
421 source_overrides,
422 parsed_source_retention.retains(path),
423 )
424 })
425 .collect();
426 }
427 let next = std::sync::atomic::AtomicUsize::new(0);
428 let mut produced: Vec<(usize, (ModuleInfo, Option<ParsedModuleSource>))> =
429 std::thread::scope(|scope| {
430 let handles: Vec<_> = (0..workers)
431 .map(|_| {
432 scope.spawn(|| {
433 let mut local = Vec::new();
434 loop {
435 let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
436 let Some(path) = paths.get(index) else {
437 break;
438 };
439 local.push((
440 index,
441 load_module(
442 path,
443 package_snapshots,
444 source_overrides,
445 parsed_source_retention.retains(path),
446 ),
447 ));
448 }
449 local
450 })
451 })
452 .collect();
453 handles
454 .into_iter()
455 .flat_map(|handle| match handle.join() {
456 Ok(local) => local,
457 Err(panic) => std::panic::resume_unwind(panic),
458 })
459 .collect()
460 });
461 produced.sort_unstable_by_key(|(index, _)| *index);
462 produced.into_iter().map(|(_, loaded)| loaded).collect()
463}
464
465fn resolve_re_exports(modules: &mut HashMap<PathBuf, ModuleInfo>) {
470 let keys: Vec<PathBuf> = modules.keys().cloned().collect();
471 loop {
472 let mut changed = false;
473 for path in &keys {
474 let wildcard_paths = modules
477 .get(path)
478 .map(|m| m.wildcard_re_export_paths.clone())
479 .unwrap_or_default();
480 if wildcard_paths.is_empty() {
481 continue;
482 }
483 let mut additions: Vec<String> = Vec::new();
484 for src in &wildcard_paths {
485 let src_canonical = normalize_path(src);
486 if let Some(src_module) = modules.get(src).or_else(|| modules.get(&src_canonical)) {
487 additions.extend(src_module.exports.iter().cloned());
488 }
489 }
490 if let Some(module) = modules.get_mut(path) {
491 for name in additions {
492 if module.exports.insert(name) {
493 changed = true;
494 }
495 }
496 }
497 }
498 if !changed {
499 break;
500 }
501 }
502}
503
504impl ModuleGraph {
505 pub fn module_paths(&self) -> Vec<PathBuf> {
510 let mut paths: Vec<PathBuf> = self.modules.keys().cloned().collect();
511 paths.sort();
512 paths
513 }
514
515 pub fn contains_module(&self, path: &Path) -> bool {
518 self.modules.contains_key(path) || self.modules.contains_key(&normalize_path(path))
519 }
520
521 pub fn all_selective_import_names(&self) -> HashSet<&str> {
523 let mut names = HashSet::new();
524 for module in self.modules.values() {
525 for name in &module.selective_import_names {
526 names.insert(name.as_str());
527 }
528 }
529 names
530 }
531
532 pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
535 let target = normalize_path(target);
536 let mut out: Vec<PathBuf> = self
537 .modules
538 .iter()
539 .filter(|(_, info)| {
540 info.imports.iter().any(|import| {
541 import
542 .path
543 .as_ref()
544 .is_some_and(|p| normalize_path(p) == target)
545 })
546 })
547 .map(|(path, _)| path.clone())
548 .collect();
549 out.sort();
550 out
551 }
552
553 pub fn transitive_importers_of(&self, target: &Path) -> Vec<PathBuf> {
560 let target = normalize_path(target);
561 let mut visited = HashSet::from([target.clone()]);
562 let mut pending = vec![target];
563 let mut out = Vec::new();
564
565 while let Some(current) = pending.pop() {
566 for importer in self.importers_of(¤t) {
567 let importer = normalize_path(&importer);
568 if visited.insert(importer.clone()) {
569 pending.push(importer.clone());
570 out.push(importer);
571 }
572 }
573 }
574
575 out.sort();
576 out
577 }
578
579 pub fn imports_for_module(&self, file: &Path) -> Vec<ModuleImport> {
581 let file = normalize_path(file);
582 let Some(module) = self.modules.get(&file) else {
583 return Vec::new();
584 };
585 let mut imports: Vec<ModuleImport> = module
586 .imports
587 .iter()
588 .map(|import| {
589 let mut selective_names = import
590 .selective_names
591 .as_ref()
592 .map(|names| names.iter().cloned().collect::<Vec<_>>());
593 if let Some(names) = selective_names.as_mut() {
594 names.sort();
595 }
596 ModuleImport {
597 raw_path: import.raw_path.clone(),
598 resolved_path: import.path.as_ref().map(|path| normalize_path(path)),
599 selective_names,
600 namespace_alias: import.namespace_alias.clone(),
601 is_pub: import.is_pub,
602 }
603 })
604 .collect();
605 imports.sort_by(|left, right| {
606 left.raw_path
607 .cmp(&right.raw_path)
608 .then_with(|| left.selective_names.cmp(&right.selective_names))
609 .then_with(|| left.namespace_alias.cmp(&right.namespace_alias))
610 .then_with(|| left.resolved_path.cmp(&right.resolved_path))
611 });
612 imports
613 }
614
615 pub fn wildcard_exports_for(&self, file: &Path) -> WildcardResolution {
620 let file = normalize_path(file);
621 let Some(module) = self.modules.get(&file) else {
622 return WildcardResolution::Unknown;
623 };
624 if module.has_unresolved_wildcard_import {
625 return WildcardResolution::Unknown;
626 }
627
628 let mut names = HashSet::new();
629 for import in module
630 .imports
631 .iter()
632 .filter(|import| import.selective_names.is_none())
633 {
634 let Some(import_path) = &import.path else {
635 return WildcardResolution::Unknown;
636 };
637 let imported = self.modules.get(import_path).or_else(|| {
638 let normalized = normalize_path(import_path);
639 self.modules.get(&normalized)
640 });
641 let Some(_imported) = imported else {
642 return WildcardResolution::Unknown;
643 };
644 names.extend(self.exports_for_import(&file, import_path));
645 }
646 WildcardResolution::Resolved(names)
647 }
648
649 #[must_use]
670 pub fn import_compile_failures(&self, file: &Path) -> Vec<ImportCompileFailure> {
671 let file = normalize_path(file);
672 let Some(module) = self.modules.get(&file) else {
673 return Vec::new();
674 };
675 let mut failures = Vec::new();
676 for import in &module.imports {
677 let Some(import_path) = &import.path else {
678 continue;
679 };
680 let Some(target) = self
681 .modules
682 .get(import_path)
683 .or_else(|| self.modules.get(&normalize_path(import_path)))
684 else {
685 continue;
686 };
687 if let Some(error) = &target.load_error {
688 failures.push(ImportCompileFailure {
689 import_raw_path: import.raw_path.clone(),
690 import_span: import.import_span,
691 module_path: normalize_path(import_path),
692 error: error.clone(),
693 });
694 }
695 }
696 failures
697 }
698
699 pub fn imported_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
700 let file = normalize_path(file);
701 let module = self.modules.get(&file)?;
702 if module.has_unresolved_wildcard_import
703 || module.has_unresolved_selective_import
704 || module.has_unresolved_namespace_import
705 {
706 return None;
707 }
708
709 let mut names = HashSet::new();
710 for import in &module.imports {
711 if let Some(alias) = &import.namespace_alias {
713 names.insert(alias.clone());
714 continue;
715 }
716 let import_path = import.path.as_ref()?;
717 let imported = self
718 .modules
719 .get(import_path)
720 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
721 if imported.load_error.is_some() {
727 return None;
728 }
729 match &import.selective_names {
730 None => {
731 names.extend(self.exports_for_import(&file, import_path));
732 }
733 Some(selective) => {
734 for name in selective {
743 if imported.declarations.contains_key(name)
744 || imported.exports.contains(name)
745 {
746 names.insert(name.clone());
747 }
748 }
749 }
750 }
751 }
752 Some(names)
753 }
754
755 pub fn imported_names_by_kind_for_file(
760 &self,
761 file: &Path,
762 kind: DefKind,
763 ) -> Option<HashSet<String>> {
764 let file = normalize_path(file);
765 let module = self.modules.get(&file)?;
766 if module.has_unresolved_wildcard_import
767 || module.has_unresolved_selective_import
768 || module.has_unresolved_namespace_import
769 {
770 return None;
771 }
772
773 let mut names = HashSet::new();
774 for import in &module.imports {
775 if import.namespace_alias.is_some() {
777 continue;
778 }
779 let import_path = import.path.as_ref()?;
780 let imported_names: Vec<String> = match &import.selective_names {
781 Some(selective) => selective.iter().cloned().collect(),
782 None => self.exports_for_import(&file, import_path),
783 };
784 for name in imported_names {
785 if self.exported_kind_for_import(&file, import_path, &name) == Some(kind) {
786 names.insert(name);
787 }
788 }
789 }
790 Some(names)
791 }
792
793 pub fn imported_callable_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
798 let mut names = HashSet::new();
799 for kind in [
800 DefKind::Function,
801 DefKind::Pipeline,
802 DefKind::Tool,
803 DefKind::Struct,
804 ] {
805 names.extend(self.imported_names_by_kind_for_file(file, kind)?);
806 }
807 Some(names)
808 }
809
810 pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
814 let file = normalize_path(file);
815 let module = self.modules.get(&file)?;
816 if module.has_unresolved_wildcard_import
817 || module.has_unresolved_selective_import
818 || module.has_unresolved_namespace_import
819 {
820 return None;
821 }
822
823 let mut decls = Vec::new();
824 let mut seen = HashSet::new();
825 for import in &module.imports {
826 if import.namespace_alias.is_some() {
828 continue;
829 }
830 let import_path = import.path.as_ref()?;
831 let imported = self
832 .modules
833 .get(import_path)
834 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
835 if imported.load_error.is_some() {
841 return None;
842 }
843 let mut names_to_collect: Vec<String> = match &import.selective_names {
844 None => imported.exports.iter().cloned().collect(),
845 Some(selective) => selective.iter().cloned().collect(),
846 };
847 names_to_collect.sort();
848 for name in &names_to_collect {
849 let mut visited = HashSet::new();
850 if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
851 let origin = self
852 .export_definition_of(import_path, name)
853 .map_or_else(|| import_path.clone(), |definition| definition.file);
854 self.extend_type_dependency(&origin, &decl, &mut decls, &mut seen);
855 }
856 }
857 for ty_decl in &imported.type_declarations {
867 if type_decl_name(ty_decl).is_some() {
868 self.extend_type_dependency(import_path, ty_decl, &mut decls, &mut seen);
869 }
870 }
871
872 for name in &names_to_collect {
873 let mut visited = HashSet::new();
874 let Some(callable) =
875 self.find_exported_callable_decl(import_path, name, &mut visited)
876 else {
877 continue;
878 };
879 let origin = self
880 .export_definition_of(import_path, name)
881 .map_or_else(|| import_path.clone(), |definition| definition.file);
882 self.extend_callable_type_dependencies(&origin, &callable, &mut decls, &mut seen);
883 }
884 }
885 Some(decls)
886 }
887
888 pub fn imported_callable_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
890 let file = normalize_path(file);
891 let module = self.modules.get(&file)?;
892 if module.has_unresolved_wildcard_import
893 || module.has_unresolved_selective_import
894 || module.has_unresolved_namespace_import
895 {
896 return None;
897 }
898
899 let mut decls = Vec::new();
900 for import in &module.imports {
901 if import.namespace_alias.is_some() {
903 continue;
904 }
905 let import_path = import.path.as_ref()?;
906 let imported = self
907 .modules
908 .get(import_path)
909 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
910 if imported.load_error.is_some() {
916 return None;
917 }
918 let selective_import = import.selective_names.is_some();
919 let names_to_collect: Vec<String> = match &import.selective_names {
920 None => self.exports_for_import(&file, import_path),
921 Some(selective) => selective.iter().cloned().collect(),
922 };
923 for name in &names_to_collect {
924 if selective_import
925 || imported.own_exports.contains(name)
926 || (sibling_module_access(&file, import_path)
927 && imported.sibling_exports.contains(name))
928 {
929 if let Some(decl) = imported
930 .callable_declarations
931 .iter()
932 .find(|decl| callable_decl_name(decl) == Some(name.as_str()))
933 {
934 decls.push(decl.clone());
935 continue;
936 }
937 }
938 let mut visited = HashSet::new();
939 if let Some(decl) =
940 self.find_exported_callable_decl(import_path, name, &mut visited)
941 {
942 decls.push(decl);
943 }
944 }
945 }
946 Some(decls)
947 }
948
949 fn find_exported_type_decl(
952 &self,
953 path: &Path,
954 name: &str,
955 visited: &mut HashSet<PathBuf>,
956 ) -> Option<SNode> {
957 let canonical = normalize_path(path);
958 if !visited.insert(canonical.clone()) {
959 return None;
960 }
961 let module = self
962 .modules
963 .get(&canonical)
964 .or_else(|| self.modules.get(path))?;
965 for decl in &module.type_declarations {
966 if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
967 return Some(decl.clone());
968 }
969 }
970 if let Some(sources) = module.selective_re_exports.get(name) {
971 for source in sources {
972 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
973 return Some(decl);
974 }
975 }
976 }
977 for source in &module.wildcard_re_export_paths {
978 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
979 return Some(decl);
980 }
981 }
982 None
983 }
984
985 fn find_exported_callable_decl(
986 &self,
987 path: &Path,
988 name: &str,
989 visited: &mut HashSet<PathBuf>,
990 ) -> Option<SNode> {
991 let canonical = normalize_path(path);
992 if !visited.insert(canonical.clone()) {
993 return None;
994 }
995 let module = self
996 .modules
997 .get(&canonical)
998 .or_else(|| self.modules.get(path))?;
999 for decl in &module.callable_declarations {
1000 if callable_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
1001 return Some(decl.clone());
1002 }
1003 }
1004 if let Some(sources) = module.selective_re_exports.get(name) {
1005 for source in sources {
1006 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
1007 return Some(decl);
1008 }
1009 }
1010 }
1011 for source in &module.wildcard_re_export_paths {
1012 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
1013 return Some(decl);
1014 }
1015 }
1016 None
1017 }
1018
1019 pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
1025 let mut visited = HashSet::new();
1026 self.definition_of_inner(file, name, &mut visited)
1027 }
1028
1029 pub fn export_definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
1036 let mut visited = HashSet::new();
1037 self.export_definition_of_inner(file, name, &mut visited)
1038 }
1039
1040 pub fn declared_names_for_file(&self, file: &Path) -> Option<Vec<&str>> {
1044 let module = self.modules.get(&normalize_path(file))?;
1045 let mut names: Vec<&str> = module.declarations.keys().map(String::as_str).collect();
1046 names.sort_unstable();
1047 Some(names)
1048 }
1049
1050 fn definition_of_inner(
1051 &self,
1052 file: &Path,
1053 name: &str,
1054 visited: &mut HashSet<PathBuf>,
1055 ) -> Option<DefSite> {
1056 let file = normalize_path(file);
1057 if !visited.insert(file.clone()) {
1058 return None;
1059 }
1060 let current = self.modules.get(&file)?;
1061
1062 if let Some(local) = current.declarations.get(name) {
1063 return Some(local.clone());
1064 }
1065
1066 if let Some(sources) = current.selective_re_exports.get(name) {
1071 for source in sources {
1072 if let Some(def) = self.definition_of_inner(source, name, visited) {
1073 return Some(def);
1074 }
1075 }
1076 }
1077
1078 for source in ¤t.wildcard_re_export_paths {
1080 if let Some(def) = self.definition_of_inner(source, name, visited) {
1081 return Some(def);
1082 }
1083 }
1084
1085 for import in ¤t.imports {
1087 let Some(selective_names) = &import.selective_names else {
1088 continue;
1089 };
1090 if !selective_names.contains(name) {
1091 continue;
1092 }
1093 if let Some(path) = &import.path {
1094 if let Some(def) = self.definition_of_inner(path, name, visited) {
1095 return Some(def);
1096 }
1097 }
1098 }
1099
1100 for import in ¤t.imports {
1102 if import.selective_names.is_some() || import.namespace_alias.is_some() {
1103 continue;
1104 }
1105 if let Some(path) = &import.path {
1106 if let Some(def) = self.definition_of_inner(path, name, visited) {
1107 return Some(def);
1108 }
1109 }
1110 }
1111
1112 None
1113 }
1114
1115 fn export_definition_of_inner(
1116 &self,
1117 file: &Path,
1118 name: &str,
1119 visited: &mut HashSet<PathBuf>,
1120 ) -> Option<DefSite> {
1121 let file = normalize_path(file);
1122 if !visited.insert(file.clone()) {
1123 return None;
1124 }
1125 let current = self.modules.get(&file)?;
1126
1127 if current.own_exports.contains(name) {
1128 if let Some(local) = current.declarations.get(name) {
1129 return Some(local.clone());
1130 }
1131 }
1132 if let Some(sources) = current.selective_re_exports.get(name) {
1133 for source in sources {
1134 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1135 return Some(definition);
1136 }
1137 }
1138 }
1139 for source in ¤t.wildcard_re_export_paths {
1140 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1141 return Some(definition);
1142 }
1143 }
1144 None
1145 }
1146
1147 pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
1151 let file = normalize_path(file);
1152 let Some(module) = self.modules.get(&file) else {
1153 return Vec::new();
1154 };
1155
1156 let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
1160
1161 for (name, srcs) in &module.selective_re_exports {
1162 sources
1163 .entry(name.clone())
1164 .or_default()
1165 .extend(srcs.iter().cloned());
1166 }
1167 for src in &module.wildcard_re_export_paths {
1168 let canonical = normalize_path(src);
1169 let Some(src_module) = self
1170 .modules
1171 .get(&canonical)
1172 .or_else(|| self.modules.get(src))
1173 else {
1174 continue;
1175 };
1176 for name in &src_module.exports {
1177 sources
1178 .entry(name.clone())
1179 .or_default()
1180 .push(canonical.clone());
1181 }
1182 }
1183
1184 for name in &module.own_exports {
1188 if let Some(entry) = sources.get_mut(name) {
1189 entry.push(file.clone());
1190 }
1191 }
1192
1193 let mut conflicts = Vec::new();
1194 for (name, mut srcs) in sources {
1195 srcs.sort();
1196 srcs.dedup();
1197 if srcs.len() > 1 {
1198 conflicts.push(ReExportConflict {
1199 name,
1200 sources: srcs,
1201 });
1202 }
1203 }
1204 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
1205 conflicts
1206 }
1207
1208 pub fn selective_import_issues(&self, file: &Path) -> Vec<SelectiveImportIssue> {
1214 let file = normalize_path(file);
1215 let Some(module) = self.modules.get(&file) else {
1216 return Vec::new();
1217 };
1218
1219 let mut out = Vec::new();
1220 for import in &module.imports {
1221 let Some(selective) = &import.selective_names else {
1222 continue;
1223 };
1224 let Some(import_path) = &import.path else {
1225 continue;
1226 };
1227 let Some(target) = self
1228 .modules
1229 .get(import_path)
1230 .or_else(|| self.modules.get(&normalize_path(import_path)))
1231 else {
1232 continue;
1233 };
1234 if target.load_error.is_some() {
1235 continue;
1236 }
1237 let visible: HashSet<_> = self
1238 .exports_for_import(&file, import_path)
1239 .into_iter()
1240 .collect();
1241 for name in selective {
1242 let can_bind = visible.contains(name);
1243 let can_publish = !import.is_pub || target.exports.contains(name);
1244 let kind = if can_bind && can_publish {
1245 continue;
1246 } else if target.declarations.contains_key(name) {
1247 SelectiveImportIssueKind::Private
1248 } else {
1249 SelectiveImportIssueKind::Missing
1250 };
1251 out.push(SelectiveImportIssue {
1252 name: name.clone(),
1253 module: import.raw_path.clone(),
1254 span: import.import_span,
1255 kind,
1256 });
1257 }
1258 }
1259 out.sort_by(|a, b| (&a.name, &a.module, a.kind).cmp(&(&b.name, &b.module, b.kind)));
1260 out.dedup();
1261 out
1262 }
1263
1264 pub fn exported_kind(&self, file: &Path, name: &str) -> Option<DefKind> {
1268 self.exported_kind_inner(file, name, &mut HashSet::new())
1269 }
1270
1271 fn exported_kind_inner(
1272 &self,
1273 file: &Path,
1274 name: &str,
1275 visited: &mut HashSet<PathBuf>,
1276 ) -> Option<DefKind> {
1277 let file = normalize_path(file);
1278 if !visited.insert(file.clone()) {
1279 return None;
1280 }
1281 let result = self.modules.get(&file).and_then(|module| {
1282 if module.own_exports.contains(name) {
1283 return module
1284 .declarations
1285 .get(name)
1286 .map(|definition| definition.kind)
1287 .or_else(|| {
1288 stdlib_module_name(&file).and_then(|stdlib_module| {
1289 stdlib::builtin_reexports(stdlib_module)
1290 .contains(&name)
1291 .then_some(DefKind::Function)
1292 })
1293 });
1294 }
1295 if let Some(sources) = module.selective_re_exports.get(name) {
1296 for source in sources {
1297 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1298 return Some(kind);
1299 }
1300 }
1301 }
1302 for source in &module.wildcard_re_export_paths {
1303 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1304 return Some(kind);
1305 }
1306 }
1307 None
1308 });
1309 visited.remove(&file);
1310 result
1311 }
1312}
1313
1314#[derive(Debug, Clone, PartialEq, Eq)]
1317pub struct ReExportConflict {
1318 pub name: String,
1319 pub sources: Vec<PathBuf>,
1320}
1321
1322#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1324pub enum SelectiveImportIssueKind {
1325 Missing,
1327 Private,
1329}
1330
1331#[derive(Debug, Clone, PartialEq, Eq)]
1333pub struct SelectiveImportIssue {
1334 pub name: String,
1336 pub module: String,
1338 pub span: Span,
1340 pub kind: SelectiveImportIssueKind,
1342}
1343
1344impl SelectiveImportIssue {
1345 #[must_use]
1347 pub fn message(&self) -> String {
1348 match self.kind {
1349 SelectiveImportIssueKind::Missing => format!(
1350 "imported symbol `{}` does not exist in `{}`",
1351 self.name, self.module
1352 ),
1353 SelectiveImportIssueKind::Private => format!(
1354 "imported symbol `{}` is not exported by `{}` — it is defined there but not `pub`",
1355 self.name, self.module
1356 ),
1357 }
1358 }
1359
1360 #[must_use]
1362 pub fn help(&self) -> String {
1363 match self.kind {
1364 SelectiveImportIssueKind::Missing => format!(
1365 "update the import to a symbol exported by `{}`",
1366 self.module
1367 ),
1368 SelectiveImportIssueKind::Private => {
1369 format!(
1370 "mark `{}` as `pub` in `{}` to export it",
1371 self.name, self.module
1372 )
1373 }
1374 }
1375 }
1376}
1377
1378fn load_module(
1379 path: &Path,
1380 package_snapshots: &[PackageSnapshot],
1381 source_overrides: Option<&HashMap<PathBuf, String>>,
1382 retain_parsed_source: bool,
1383) -> (ModuleInfo, Option<ParsedModuleSource>) {
1384 let source = source_overrides
1385 .and_then(|overrides| overrides.get(&normalize_path(path)).cloned())
1386 .or_else(|| read_module_source(path));
1387 let Some(source) = source else {
1388 return (ModuleInfo::default(), None);
1389 };
1390 let mut lexer = harn_lexer::Lexer::new(&source);
1391 let tokens = match lexer.tokenize() {
1392 Ok(tokens) => tokens,
1393 Err(error) => {
1394 let module = ModuleInfo {
1395 load_error: Some(ModuleLoadError {
1396 message: error.to_string(),
1397 span: error.span(),
1398 }),
1399 ..ModuleInfo::default()
1400 };
1401 return (module, None);
1402 }
1403 };
1404 let mut parser = Parser::new(tokens);
1405 let program = match parser.parse() {
1406 Ok(program) => program,
1407 Err(error) => {
1408 let module = ModuleInfo {
1409 load_error: Some(ModuleLoadError {
1410 message: error.to_string(),
1411 span: error.span(),
1412 }),
1413 ..ModuleInfo::default()
1414 };
1415 return (module, None);
1416 }
1417 };
1418
1419 let mut module = ModuleInfo::default();
1420 for node in &program {
1421 collect_module_info(path, node, &mut module, package_snapshots);
1422 collect_type_declarations(node, &mut module.type_declarations);
1423 collect_callable_declarations(node, &mut module.callable_declarations);
1424 }
1425 if let Some(stdlib_module) = stdlib_module_name(path) {
1426 module.own_exports.extend(
1427 stdlib::builtin_reexports(stdlib_module)
1428 .iter()
1429 .map(|name| (*name).to_string()),
1430 );
1431 }
1432 module.exports.extend(module.own_exports.iter().cloned());
1436 module
1437 .exports
1438 .extend(module.selective_re_exports.keys().cloned());
1439 let parsed = retain_parsed_source.then_some(ParsedModuleSource { source, program });
1440 (module, parsed)
1441}
1442
1443pub fn stdlib_module_name(path: &Path) -> Option<&str> {
1446 let s = path.to_str()?;
1447 s.strip_prefix("<std>/")
1448}
1449
1450fn normalize_path(path: &Path) -> PathBuf {
1451 canonical_path(path)
1452}
1453
1454pub fn canonical_path(path: &Path) -> PathBuf {
1467 use std::sync::OnceLock;
1468 if stdlib_module_name(path).is_some() {
1469 return path.to_path_buf();
1470 }
1471 static MEMO: OnceLock<std::sync::Mutex<HashMap<PathBuf, PathBuf>>> = OnceLock::new();
1472 let memo = MEMO.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
1473 if let Some(hit) = memo
1474 .lock()
1475 .expect("canonical path memo lock poisoned")
1476 .get(path)
1477 .cloned()
1478 {
1479 return hit;
1480 }
1481 match path.canonicalize() {
1482 Ok(canonical) => {
1483 memo.lock()
1484 .expect("canonical path memo lock poisoned")
1485 .insert(path.to_path_buf(), canonical.clone());
1486 canonical
1487 }
1488 Err(_) => manifest_walk::normalize_lexically(path),
1491 }
1492}
1493
1494#[cfg(test)]
1495#[path = "tests.rs"]
1496mod tests;