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 harn_parser::runtime_stack::scope(|scope| {
430 let handles: Vec<_> = (0..workers)
431 .map(|_| {
432 scope
433 .spawn_named("harn-module-parse", || {
434 let mut local = Vec::new();
435 loop {
436 let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
437 let Some(path) = paths.get(index) else {
438 break;
439 };
440 local.push((
441 index,
442 load_module(
443 path,
444 package_snapshots,
445 source_overrides,
446 parsed_source_retention.retains(path),
447 ),
448 ));
449 }
450 local
451 })
452 .expect("spawn module-parse worker")
453 })
454 .collect();
455 handles
456 .into_iter()
457 .flat_map(|handle| match handle.join() {
458 Ok(local) => local,
459 Err(panic) => std::panic::resume_unwind(panic),
460 })
461 .collect()
462 });
463 produced.sort_unstable_by_key(|(index, _)| *index);
464 produced.into_iter().map(|(_, loaded)| loaded).collect()
465}
466
467fn resolve_re_exports(modules: &mut HashMap<PathBuf, ModuleInfo>) {
472 let keys: Vec<PathBuf> = modules.keys().cloned().collect();
473 loop {
474 let mut changed = false;
475 for path in &keys {
476 let wildcard_paths = modules
479 .get(path)
480 .map(|m| m.wildcard_re_export_paths.clone())
481 .unwrap_or_default();
482 if wildcard_paths.is_empty() {
483 continue;
484 }
485 let mut additions: Vec<String> = Vec::new();
486 for src in &wildcard_paths {
487 let src_canonical = normalize_path(src);
488 if let Some(src_module) = modules.get(src).or_else(|| modules.get(&src_canonical)) {
489 additions.extend(src_module.exports.iter().cloned());
490 }
491 }
492 if let Some(module) = modules.get_mut(path) {
493 for name in additions {
494 if module.exports.insert(name) {
495 changed = true;
496 }
497 }
498 }
499 }
500 if !changed {
501 break;
502 }
503 }
504}
505
506impl ModuleGraph {
507 pub fn module_paths(&self) -> Vec<PathBuf> {
512 let mut paths: Vec<PathBuf> = self.modules.keys().cloned().collect();
513 paths.sort();
514 paths
515 }
516
517 pub fn contains_module(&self, path: &Path) -> bool {
520 self.modules.contains_key(path) || self.modules.contains_key(&normalize_path(path))
521 }
522
523 pub fn all_selective_import_names(&self) -> HashSet<&str> {
525 let mut names = HashSet::new();
526 for module in self.modules.values() {
527 for name in &module.selective_import_names {
528 names.insert(name.as_str());
529 }
530 }
531 names
532 }
533
534 pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
537 let target = normalize_path(target);
538 let mut out: Vec<PathBuf> = self
539 .modules
540 .iter()
541 .filter(|(_, info)| {
542 info.imports.iter().any(|import| {
543 import
544 .path
545 .as_ref()
546 .is_some_and(|p| normalize_path(p) == target)
547 })
548 })
549 .map(|(path, _)| path.clone())
550 .collect();
551 out.sort();
552 out
553 }
554
555 pub fn transitive_importers_of(&self, target: &Path) -> Vec<PathBuf> {
562 let target = normalize_path(target);
563 let mut visited = HashSet::from([target.clone()]);
564 let mut pending = vec![target];
565 let mut out = Vec::new();
566
567 while let Some(current) = pending.pop() {
568 for importer in self.importers_of(¤t) {
569 let importer = normalize_path(&importer);
570 if visited.insert(importer.clone()) {
571 pending.push(importer.clone());
572 out.push(importer);
573 }
574 }
575 }
576
577 out.sort();
578 out
579 }
580
581 pub fn imports_for_module(&self, file: &Path) -> Vec<ModuleImport> {
583 let file = normalize_path(file);
584 let Some(module) = self.modules.get(&file) else {
585 return Vec::new();
586 };
587 let mut imports: Vec<ModuleImport> = module
588 .imports
589 .iter()
590 .map(|import| {
591 let mut selective_names = import
592 .selective_names
593 .as_ref()
594 .map(|names| names.iter().cloned().collect::<Vec<_>>());
595 if let Some(names) = selective_names.as_mut() {
596 names.sort();
597 }
598 ModuleImport {
599 raw_path: import.raw_path.clone(),
600 resolved_path: import.path.as_ref().map(|path| normalize_path(path)),
601 selective_names,
602 namespace_alias: import.namespace_alias.clone(),
603 is_pub: import.is_pub,
604 }
605 })
606 .collect();
607 imports.sort_by(|left, right| {
608 left.raw_path
609 .cmp(&right.raw_path)
610 .then_with(|| left.selective_names.cmp(&right.selective_names))
611 .then_with(|| left.namespace_alias.cmp(&right.namespace_alias))
612 .then_with(|| left.resolved_path.cmp(&right.resolved_path))
613 });
614 imports
615 }
616
617 pub fn wildcard_exports_for(&self, file: &Path) -> WildcardResolution {
622 let file = normalize_path(file);
623 let Some(module) = self.modules.get(&file) else {
624 return WildcardResolution::Unknown;
625 };
626 if module.has_unresolved_wildcard_import {
627 return WildcardResolution::Unknown;
628 }
629
630 let mut names = HashSet::new();
631 for import in module
632 .imports
633 .iter()
634 .filter(|import| import.selective_names.is_none())
635 {
636 let Some(import_path) = &import.path else {
637 return WildcardResolution::Unknown;
638 };
639 let imported = self.modules.get(import_path).or_else(|| {
640 let normalized = normalize_path(import_path);
641 self.modules.get(&normalized)
642 });
643 let Some(_imported) = imported else {
644 return WildcardResolution::Unknown;
645 };
646 names.extend(self.exports_for_import(&file, import_path));
647 }
648 WildcardResolution::Resolved(names)
649 }
650
651 #[must_use]
672 pub fn import_compile_failures(&self, file: &Path) -> Vec<ImportCompileFailure> {
673 let file = normalize_path(file);
674 let Some(module) = self.modules.get(&file) else {
675 return Vec::new();
676 };
677 let mut failures = Vec::new();
678 for import in &module.imports {
679 let Some(import_path) = &import.path else {
680 continue;
681 };
682 let Some(target) = self
683 .modules
684 .get(import_path)
685 .or_else(|| self.modules.get(&normalize_path(import_path)))
686 else {
687 continue;
688 };
689 if let Some(error) = &target.load_error {
690 failures.push(ImportCompileFailure {
691 import_raw_path: import.raw_path.clone(),
692 import_span: import.import_span,
693 module_path: normalize_path(import_path),
694 error: error.clone(),
695 });
696 }
697 }
698 failures
699 }
700
701 pub fn imported_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
702 let file = normalize_path(file);
703 let module = self.modules.get(&file)?;
704 if module.has_unresolved_wildcard_import
705 || module.has_unresolved_selective_import
706 || module.has_unresolved_namespace_import
707 {
708 return None;
709 }
710
711 let mut names = HashSet::new();
712 for import in &module.imports {
713 if let Some(alias) = &import.namespace_alias {
715 names.insert(alias.clone());
716 continue;
717 }
718 let import_path = import.path.as_ref()?;
719 let imported = self
720 .modules
721 .get(import_path)
722 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
723 if imported.load_error.is_some() {
729 return None;
730 }
731 match &import.selective_names {
732 None => {
733 names.extend(self.exports_for_import(&file, import_path));
734 }
735 Some(selective) => {
736 for name in selective {
745 if imported.declarations.contains_key(name)
746 || imported.exports.contains(name)
747 {
748 names.insert(name.clone());
749 }
750 }
751 }
752 }
753 }
754 Some(names)
755 }
756
757 pub fn imported_names_by_kind_for_file(
762 &self,
763 file: &Path,
764 kind: DefKind,
765 ) -> Option<HashSet<String>> {
766 let file = normalize_path(file);
767 let module = self.modules.get(&file)?;
768 if module.has_unresolved_wildcard_import
769 || module.has_unresolved_selective_import
770 || module.has_unresolved_namespace_import
771 {
772 return None;
773 }
774
775 let mut names = HashSet::new();
776 for import in &module.imports {
777 if import.namespace_alias.is_some() {
779 continue;
780 }
781 let import_path = import.path.as_ref()?;
782 let imported_names: Vec<String> = match &import.selective_names {
783 Some(selective) => selective.iter().cloned().collect(),
784 None => self.exports_for_import(&file, import_path),
785 };
786 for name in imported_names {
787 if self.exported_kind_for_import(&file, import_path, &name) == Some(kind) {
788 names.insert(name);
789 }
790 }
791 }
792 Some(names)
793 }
794
795 pub fn imported_callable_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
800 let mut names = HashSet::new();
801 for kind in [
802 DefKind::Function,
803 DefKind::Pipeline,
804 DefKind::Tool,
805 DefKind::Struct,
806 ] {
807 names.extend(self.imported_names_by_kind_for_file(file, kind)?);
808 }
809 Some(names)
810 }
811
812 pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
816 let file = normalize_path(file);
817 let module = self.modules.get(&file)?;
818 if module.has_unresolved_wildcard_import
819 || module.has_unresolved_selective_import
820 || module.has_unresolved_namespace_import
821 {
822 return None;
823 }
824
825 let mut decls = Vec::new();
826 let mut seen = HashSet::new();
827 for import in &module.imports {
828 if import.namespace_alias.is_some() {
830 continue;
831 }
832 let import_path = import.path.as_ref()?;
833 let imported = self
834 .modules
835 .get(import_path)
836 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
837 if imported.load_error.is_some() {
843 return None;
844 }
845 let mut names_to_collect: Vec<String> = match &import.selective_names {
846 None => imported.exports.iter().cloned().collect(),
847 Some(selective) => selective.iter().cloned().collect(),
848 };
849 names_to_collect.sort();
850 for name in &names_to_collect {
851 let mut visited = HashSet::new();
852 if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
853 let origin = self
854 .export_definition_of(import_path, name)
855 .map_or_else(|| import_path.clone(), |definition| definition.file);
856 self.extend_type_dependency(&origin, &decl, &mut decls, &mut seen);
857 }
858 }
859 for ty_decl in &imported.type_declarations {
869 if type_decl_name(ty_decl).is_some() {
870 self.extend_type_dependency(import_path, ty_decl, &mut decls, &mut seen);
871 }
872 }
873
874 for name in &names_to_collect {
875 let mut visited = HashSet::new();
876 let Some(callable) =
877 self.find_exported_callable_decl(import_path, name, &mut visited)
878 else {
879 continue;
880 };
881 let origin = self
882 .export_definition_of(import_path, name)
883 .map_or_else(|| import_path.clone(), |definition| definition.file);
884 self.extend_callable_type_dependencies(&origin, &callable, &mut decls, &mut seen);
885 }
886 }
887 Some(decls)
888 }
889
890 pub fn imported_callable_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
892 let file = normalize_path(file);
893 let module = self.modules.get(&file)?;
894 if module.has_unresolved_wildcard_import
895 || module.has_unresolved_selective_import
896 || module.has_unresolved_namespace_import
897 {
898 return None;
899 }
900
901 let mut decls = Vec::new();
902 for import in &module.imports {
903 if import.namespace_alias.is_some() {
905 continue;
906 }
907 let import_path = import.path.as_ref()?;
908 let imported = self
909 .modules
910 .get(import_path)
911 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
912 if imported.load_error.is_some() {
918 return None;
919 }
920 let selective_import = import.selective_names.is_some();
921 let names_to_collect: Vec<String> = match &import.selective_names {
922 None => self.exports_for_import(&file, import_path),
923 Some(selective) => selective.iter().cloned().collect(),
924 };
925 for name in &names_to_collect {
926 if selective_import
927 || imported.own_exports.contains(name)
928 || (sibling_module_access(&file, import_path)
929 && imported.sibling_exports.contains(name))
930 {
931 if let Some(decl) = imported
932 .callable_declarations
933 .iter()
934 .find(|decl| callable_decl_name(decl) == Some(name.as_str()))
935 {
936 decls.push(decl.clone());
937 continue;
938 }
939 }
940 let mut visited = HashSet::new();
941 if let Some(decl) =
942 self.find_exported_callable_decl(import_path, name, &mut visited)
943 {
944 decls.push(decl);
945 }
946 }
947 }
948 Some(decls)
949 }
950
951 fn find_exported_type_decl(
954 &self,
955 path: &Path,
956 name: &str,
957 visited: &mut HashSet<PathBuf>,
958 ) -> Option<SNode> {
959 let canonical = normalize_path(path);
960 if !visited.insert(canonical.clone()) {
961 return None;
962 }
963 let module = self
964 .modules
965 .get(&canonical)
966 .or_else(|| self.modules.get(path))?;
967 for decl in &module.type_declarations {
968 if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
969 return Some(decl.clone());
970 }
971 }
972 if let Some(sources) = module.selective_re_exports.get(name) {
973 for source in sources {
974 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
975 return Some(decl);
976 }
977 }
978 }
979 for source in &module.wildcard_re_export_paths {
980 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
981 return Some(decl);
982 }
983 }
984 None
985 }
986
987 fn find_exported_callable_decl(
988 &self,
989 path: &Path,
990 name: &str,
991 visited: &mut HashSet<PathBuf>,
992 ) -> Option<SNode> {
993 let canonical = normalize_path(path);
994 if !visited.insert(canonical.clone()) {
995 return None;
996 }
997 let module = self
998 .modules
999 .get(&canonical)
1000 .or_else(|| self.modules.get(path))?;
1001 for decl in &module.callable_declarations {
1002 if callable_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
1003 return Some(decl.clone());
1004 }
1005 }
1006 if let Some(sources) = module.selective_re_exports.get(name) {
1007 for source in sources {
1008 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
1009 return Some(decl);
1010 }
1011 }
1012 }
1013 for source in &module.wildcard_re_export_paths {
1014 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
1015 return Some(decl);
1016 }
1017 }
1018 None
1019 }
1020
1021 pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
1027 let mut visited = HashSet::new();
1028 self.definition_of_inner(file, name, &mut visited)
1029 }
1030
1031 pub fn export_definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
1038 let mut visited = HashSet::new();
1039 self.export_definition_of_inner(file, name, &mut visited)
1040 }
1041
1042 pub fn declared_names_for_file(&self, file: &Path) -> Option<Vec<&str>> {
1046 let module = self.modules.get(&normalize_path(file))?;
1047 let mut names: Vec<&str> = module.declarations.keys().map(String::as_str).collect();
1048 names.sort_unstable();
1049 Some(names)
1050 }
1051
1052 fn definition_of_inner(
1053 &self,
1054 file: &Path,
1055 name: &str,
1056 visited: &mut HashSet<PathBuf>,
1057 ) -> Option<DefSite> {
1058 let file = normalize_path(file);
1059 if !visited.insert(file.clone()) {
1060 return None;
1061 }
1062 let current = self.modules.get(&file)?;
1063
1064 if let Some(local) = current.declarations.get(name) {
1065 return Some(local.clone());
1066 }
1067
1068 if let Some(sources) = current.selective_re_exports.get(name) {
1073 for source in sources {
1074 if let Some(def) = self.definition_of_inner(source, name, visited) {
1075 return Some(def);
1076 }
1077 }
1078 }
1079
1080 for source in ¤t.wildcard_re_export_paths {
1082 if let Some(def) = self.definition_of_inner(source, name, visited) {
1083 return Some(def);
1084 }
1085 }
1086
1087 for import in ¤t.imports {
1089 let Some(selective_names) = &import.selective_names else {
1090 continue;
1091 };
1092 if !selective_names.contains(name) {
1093 continue;
1094 }
1095 if let Some(path) = &import.path {
1096 if let Some(def) = self.definition_of_inner(path, name, visited) {
1097 return Some(def);
1098 }
1099 }
1100 }
1101
1102 for import in ¤t.imports {
1104 if import.selective_names.is_some() || import.namespace_alias.is_some() {
1105 continue;
1106 }
1107 if let Some(path) = &import.path {
1108 if let Some(def) = self.definition_of_inner(path, name, visited) {
1109 return Some(def);
1110 }
1111 }
1112 }
1113
1114 None
1115 }
1116
1117 fn export_definition_of_inner(
1118 &self,
1119 file: &Path,
1120 name: &str,
1121 visited: &mut HashSet<PathBuf>,
1122 ) -> Option<DefSite> {
1123 let file = normalize_path(file);
1124 if !visited.insert(file.clone()) {
1125 return None;
1126 }
1127 let current = self.modules.get(&file)?;
1128
1129 if current.own_exports.contains(name) {
1130 if let Some(local) = current.declarations.get(name) {
1131 return Some(local.clone());
1132 }
1133 }
1134 if let Some(sources) = current.selective_re_exports.get(name) {
1135 for source in sources {
1136 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1137 return Some(definition);
1138 }
1139 }
1140 }
1141 for source in ¤t.wildcard_re_export_paths {
1142 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1143 return Some(definition);
1144 }
1145 }
1146 None
1147 }
1148
1149 pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
1153 let file = normalize_path(file);
1154 let Some(module) = self.modules.get(&file) else {
1155 return Vec::new();
1156 };
1157
1158 let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
1162
1163 for (name, srcs) in &module.selective_re_exports {
1164 sources
1165 .entry(name.clone())
1166 .or_default()
1167 .extend(srcs.iter().cloned());
1168 }
1169 for src in &module.wildcard_re_export_paths {
1170 let canonical = normalize_path(src);
1171 let Some(src_module) = self
1172 .modules
1173 .get(&canonical)
1174 .or_else(|| self.modules.get(src))
1175 else {
1176 continue;
1177 };
1178 for name in &src_module.exports {
1179 sources
1180 .entry(name.clone())
1181 .or_default()
1182 .push(canonical.clone());
1183 }
1184 }
1185
1186 for name in &module.own_exports {
1190 if let Some(entry) = sources.get_mut(name) {
1191 entry.push(file.clone());
1192 }
1193 }
1194
1195 let mut conflicts = Vec::new();
1196 for (name, mut srcs) in sources {
1197 srcs.sort();
1198 srcs.dedup();
1199 if srcs.len() > 1 {
1200 conflicts.push(ReExportConflict {
1201 name,
1202 sources: srcs,
1203 });
1204 }
1205 }
1206 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
1207 conflicts
1208 }
1209
1210 pub fn selective_import_issues(&self, file: &Path) -> Vec<SelectiveImportIssue> {
1216 let file = normalize_path(file);
1217 let Some(module) = self.modules.get(&file) else {
1218 return Vec::new();
1219 };
1220
1221 let mut out = Vec::new();
1222 for import in &module.imports {
1223 let Some(selective) = &import.selective_names else {
1224 continue;
1225 };
1226 let Some(import_path) = &import.path else {
1227 continue;
1228 };
1229 let Some(target) = self
1230 .modules
1231 .get(import_path)
1232 .or_else(|| self.modules.get(&normalize_path(import_path)))
1233 else {
1234 continue;
1235 };
1236 if target.load_error.is_some() {
1237 continue;
1238 }
1239 let visible: HashSet<_> = self
1240 .exports_for_import(&file, import_path)
1241 .into_iter()
1242 .collect();
1243 for name in selective {
1244 let can_bind = visible.contains(name);
1245 let can_publish = !import.is_pub || target.exports.contains(name);
1246 let kind = if can_bind && can_publish {
1247 continue;
1248 } else if target.declarations.contains_key(name) {
1249 SelectiveImportIssueKind::Private
1250 } else {
1251 SelectiveImportIssueKind::Missing
1252 };
1253 out.push(SelectiveImportIssue {
1254 name: name.clone(),
1255 module: import.raw_path.clone(),
1256 span: import.import_span,
1257 kind,
1258 });
1259 }
1260 }
1261 out.sort_by(|a, b| (&a.name, &a.module, a.kind).cmp(&(&b.name, &b.module, b.kind)));
1262 out.dedup();
1263 out
1264 }
1265
1266 pub fn exported_kind(&self, file: &Path, name: &str) -> Option<DefKind> {
1270 self.exported_kind_inner(file, name, &mut HashSet::new())
1271 }
1272
1273 fn exported_kind_inner(
1274 &self,
1275 file: &Path,
1276 name: &str,
1277 visited: &mut HashSet<PathBuf>,
1278 ) -> Option<DefKind> {
1279 let file = normalize_path(file);
1280 if !visited.insert(file.clone()) {
1281 return None;
1282 }
1283 let result = self.modules.get(&file).and_then(|module| {
1284 if module.own_exports.contains(name) {
1285 return module
1286 .declarations
1287 .get(name)
1288 .map(|definition| definition.kind)
1289 .or_else(|| {
1290 stdlib_module_name(&file).and_then(|stdlib_module| {
1291 stdlib::builtin_reexports(stdlib_module)
1292 .contains(&name)
1293 .then_some(DefKind::Function)
1294 })
1295 });
1296 }
1297 if let Some(sources) = module.selective_re_exports.get(name) {
1298 for source in sources {
1299 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1300 return Some(kind);
1301 }
1302 }
1303 }
1304 for source in &module.wildcard_re_export_paths {
1305 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1306 return Some(kind);
1307 }
1308 }
1309 None
1310 });
1311 visited.remove(&file);
1312 result
1313 }
1314}
1315
1316#[derive(Debug, Clone, PartialEq, Eq)]
1319pub struct ReExportConflict {
1320 pub name: String,
1321 pub sources: Vec<PathBuf>,
1322}
1323
1324#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1326pub enum SelectiveImportIssueKind {
1327 Missing,
1329 Private,
1331}
1332
1333#[derive(Debug, Clone, PartialEq, Eq)]
1335pub struct SelectiveImportIssue {
1336 pub name: String,
1338 pub module: String,
1340 pub span: Span,
1342 pub kind: SelectiveImportIssueKind,
1344}
1345
1346impl SelectiveImportIssue {
1347 #[must_use]
1349 pub fn message(&self) -> String {
1350 match self.kind {
1351 SelectiveImportIssueKind::Missing => format!(
1352 "imported symbol `{}` does not exist in `{}`",
1353 self.name, self.module
1354 ),
1355 SelectiveImportIssueKind::Private => format!(
1356 "imported symbol `{}` is not exported by `{}` — it is defined there but not `pub`",
1357 self.name, self.module
1358 ),
1359 }
1360 }
1361
1362 #[must_use]
1364 pub fn help(&self) -> String {
1365 match self.kind {
1366 SelectiveImportIssueKind::Missing => format!(
1367 "update the import to a symbol exported by `{}`",
1368 self.module
1369 ),
1370 SelectiveImportIssueKind::Private => {
1371 format!(
1372 "mark `{}` as `pub` in `{}` to export it",
1373 self.name, self.module
1374 )
1375 }
1376 }
1377 }
1378}
1379
1380fn load_module(
1381 path: &Path,
1382 package_snapshots: &[PackageSnapshot],
1383 source_overrides: Option<&HashMap<PathBuf, String>>,
1384 retain_parsed_source: bool,
1385) -> (ModuleInfo, Option<ParsedModuleSource>) {
1386 let source = source_overrides
1387 .and_then(|overrides| overrides.get(&normalize_path(path)).cloned())
1388 .or_else(|| read_module_source(path));
1389 let Some(source) = source else {
1390 return (ModuleInfo::default(), None);
1391 };
1392 let mut lexer = harn_lexer::Lexer::new(&source);
1393 let tokens = match lexer.tokenize() {
1394 Ok(tokens) => tokens,
1395 Err(error) => {
1396 let module = ModuleInfo {
1397 load_error: Some(ModuleLoadError {
1398 message: error.to_string(),
1399 span: error.span(),
1400 }),
1401 ..ModuleInfo::default()
1402 };
1403 return (module, None);
1404 }
1405 };
1406 let mut parser = Parser::new(tokens);
1407 let program = match parser.parse() {
1408 Ok(program) => program,
1409 Err(error) => {
1410 let module = ModuleInfo {
1411 load_error: Some(ModuleLoadError {
1412 message: error.to_string(),
1413 span: error.span(),
1414 }),
1415 ..ModuleInfo::default()
1416 };
1417 return (module, None);
1418 }
1419 };
1420
1421 let mut module = ModuleInfo::default();
1422 for node in &program {
1423 collect_module_info(path, node, &mut module, package_snapshots);
1424 collect_type_declarations(node, &mut module.type_declarations);
1425 collect_callable_declarations(node, &mut module.callable_declarations);
1426 }
1427 if let Some(stdlib_module) = stdlib_module_name(path) {
1428 module.own_exports.extend(
1429 stdlib::builtin_reexports(stdlib_module)
1430 .iter()
1431 .map(|name| (*name).to_string()),
1432 );
1433 }
1434 module.exports.extend(module.own_exports.iter().cloned());
1438 module
1439 .exports
1440 .extend(module.selective_re_exports.keys().cloned());
1441 let parsed = retain_parsed_source.then_some(ParsedModuleSource { source, program });
1442 (module, parsed)
1443}
1444
1445pub fn stdlib_module_name(path: &Path) -> Option<&str> {
1448 let s = path.to_str()?;
1449 s.strip_prefix("<std>/")
1450}
1451
1452fn normalize_path(path: &Path) -> PathBuf {
1453 canonical_path(path)
1454}
1455
1456pub fn canonical_path(path: &Path) -> PathBuf {
1469 use std::sync::OnceLock;
1470 if stdlib_module_name(path).is_some() {
1471 return path.to_path_buf();
1472 }
1473 static MEMO: OnceLock<std::sync::Mutex<HashMap<PathBuf, PathBuf>>> = OnceLock::new();
1474 let memo = MEMO.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
1475 if let Some(hit) = memo
1476 .lock()
1477 .expect("canonical path memo lock poisoned")
1478 .get(path)
1479 .cloned()
1480 {
1481 return hit;
1482 }
1483 match path.canonicalize() {
1484 Ok(canonical) => {
1485 memo.lock()
1486 .expect("canonical path memo lock poisoned")
1487 .insert(path.to_path_buf(), canonical.clone());
1488 canonical
1489 }
1490 Err(_) => manifest_walk::normalize_lexically(path),
1493 }
1494}
1495
1496#[cfg(test)]
1497#[path = "tests.rs"]
1498mod tests;