1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4use crate::package_imports::acquire_package_snapshots;
5use crate::package_snapshot::PackageSnapshot;
6use harn_lexer::Span;
7use harn_parser::{Parser, SNode};
8
9pub mod asset_paths;
10mod declarations;
11pub mod fingerprint;
12pub mod host_capabilities;
13pub mod host_capability_config;
14mod import_recording;
15pub mod manifest_walk;
16mod namespace_imports;
17mod namespace_signatures;
18pub mod package_execution;
19mod package_imports;
20pub mod package_snapshot;
21pub mod personas;
22pub mod project_config;
23mod references;
24mod standalone;
25mod stdlib;
26mod symbol_reachability;
27mod type_dependencies;
28
29use declarations::{
30 callable_decl_name, collect_callable_declarations, collect_module_info,
31 collect_type_declarations, decl_site, type_decl_name,
32};
33pub use declarations::{public_declarations, DefKind, PublicDeclaration};
34pub use namespace_imports::NamespaceImportInfo;
35pub use namespace_signatures::NamespaceMemberSignature;
36pub use package_imports::{
37 resolve_import_path, resolve_import_path_with_guard, resolve_import_path_with_snapshot,
38};
39pub use references::{index_references, RefSite, ReferenceEdge, ReferenceIndex};
40pub use standalone::build_with_standalone_source;
41use standalone::PackageContext;
42pub use symbol_reachability::{
43 closed_program_reachability, ExportDemand, ModuleSymbolDemand, SymbolReachability,
44};
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct DefSite {
49 pub name: String,
50 pub file: PathBuf,
51 pub kind: DefKind,
52 pub span: Span,
53}
54
55#[derive(Debug, Clone)]
57pub enum WildcardResolution {
58 Resolved(HashSet<String>),
60 Unknown,
62}
63
64#[derive(Debug, Default)]
66pub struct ModuleGraph {
67 modules: HashMap<PathBuf, ModuleInfo>,
68 _package_snapshots: Vec<PackageSnapshot>,
70}
71
72#[derive(Debug, Clone)]
73pub struct ParsedModuleSource {
74 pub source: String,
75 pub program: Vec<SNode>,
76}
77
78#[derive(Debug, Default)]
79pub struct ModuleGraphBuild {
80 pub graph: ModuleGraph,
81 pub parsed_sources: HashMap<PathBuf, ParsedModuleSource>,
82}
83
84#[derive(Debug, Default)]
85struct ModuleInfo {
86 declarations: HashMap<String, DefSite>,
89 exports: HashSet<String>,
94 own_exports: HashSet<String>,
97 selective_re_exports: HashMap<String, Vec<PathBuf>>,
104 wildcard_re_export_paths: Vec<PathBuf>,
108 namespace_re_exports: HashMap<String, PathBuf>,
115 selective_import_names: HashSet<String>,
117 imports: Vec<ImportRef>,
119 has_unresolved_wildcard_import: bool,
121 has_unresolved_selective_import: bool,
125 has_unresolved_namespace_import: bool,
127 type_declarations: Vec<SNode>,
130 callable_declarations: Vec<SNode>,
133 load_error: Option<ModuleLoadError>,
139}
140
141#[derive(Debug, Clone)]
147pub struct ModuleLoadError {
148 pub message: String,
150 pub span: Span,
152}
153
154#[derive(Debug, Clone)]
157pub struct ImportCompileFailure {
158 pub import_raw_path: String,
160 pub import_span: Span,
162 pub module_path: PathBuf,
164 pub error: ModuleLoadError,
166}
167
168#[derive(Debug, Clone)]
169struct ImportRef {
170 raw_path: String,
171 path: Option<PathBuf>,
172 selective_names: Option<HashSet<String>>,
173 namespace_alias: Option<String>,
176 is_pub: bool,
177 import_span: Span,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct ModuleImport {
183 pub raw_path: String,
185 pub resolved_path: Option<PathBuf>,
187 pub selective_names: Option<Vec<String>>,
189 pub namespace_alias: Option<String>,
191 pub is_pub: bool,
193}
194
195pub fn read_module_source(path: &Path) -> Option<String> {
201 if let Some(stdlib_module) = stdlib_module_name(path) {
202 return stdlib::get_stdlib_source(stdlib_module).map(ToString::to_string);
203 }
204 std::fs::read_to_string(path).ok()
205}
206
207pub fn build(files: &[PathBuf]) -> ModuleGraph {
213 build_inner(
214 files,
215 ParsedSourceRetention::None,
216 None,
217 PackageContext::Project,
218 )
219 .graph
220}
221
222pub fn build_with_source(file: &Path, source: &str) -> ModuleGraph {
228 let file = normalize_path(file);
229 let source_overrides = HashMap::from([(file.clone(), source.to_string())]);
230 build_inner(
231 &[file],
232 ParsedSourceRetention::None,
233 Some(&source_overrides),
234 PackageContext::Project,
235 )
236 .graph
237}
238
239pub fn build_with_parsed_sources(files: &[PathBuf]) -> ModuleGraphBuild {
245 let parsed_source_targets = files.iter().map(|file| normalize_path(file)).collect();
246 build_inner(
247 files,
248 ParsedSourceRetention::Seeds(&parsed_source_targets),
249 None,
250 PackageContext::Project,
251 )
252}
253
254pub fn build_closed_program(files: &[PathBuf]) -> ModuleGraphBuild {
260 build_inner(
261 files,
262 ParsedSourceRetention::All,
263 None,
264 PackageContext::Project,
265 )
266}
267
268#[derive(Clone, Copy)]
269enum ParsedSourceRetention<'a> {
270 None,
271 Seeds(&'a HashSet<PathBuf>),
272 All,
273}
274
275impl ParsedSourceRetention<'_> {
276 fn retains(self, path: &Path) -> bool {
277 match self {
278 Self::None => false,
279 Self::Seeds(targets) => targets.contains(path),
280 Self::All => true,
281 }
282 }
283}
284
285pub fn build_for_reference_index(
293 files: &[PathBuf],
294 source_overrides: Option<&HashMap<PathBuf, String>>,
295) -> ModuleGraphBuild {
296 build_inner(
297 files,
298 ParsedSourceRetention::All,
299 source_overrides,
300 PackageContext::Project,
301 )
302}
303
304fn build_inner(
305 files: &[PathBuf],
306 parsed_source_retention: ParsedSourceRetention<'_>,
307 source_overrides: Option<&HashMap<PathBuf, String>>,
308 package_context: PackageContext,
309) -> ModuleGraphBuild {
310 let package_snapshots = match package_context {
311 PackageContext::Project => acquire_package_snapshots(files),
312 PackageContext::Standalone => Vec::new(),
313 };
314 let mut modules: HashMap<PathBuf, ModuleInfo> = HashMap::new();
315 let mut parsed_sources: HashMap<PathBuf, ParsedModuleSource> = HashMap::new();
316 let mut seen: HashSet<PathBuf> = HashSet::new();
317 let mut wave: Vec<PathBuf> = Vec::new();
318 for file in files {
319 let canonical = normalize_path(file);
320 if seen.insert(canonical.clone()) {
321 wave.push(canonical);
322 }
323 }
324 while !wave.is_empty() {
332 let loaded = load_wave(
333 &wave,
334 &package_snapshots,
335 parsed_source_retention,
336 source_overrides,
337 );
338 let mut next_wave: Vec<PathBuf> = Vec::new();
339 for (path, (module, parsed)) in wave.drain(..).zip(loaded) {
340 if parsed_source_retention.retains(&path) {
341 if let Some(parsed) = parsed {
342 parsed_sources.insert(path.clone(), parsed);
343 }
344 }
345 for import in &module.imports {
362 if let Some(import_path) = &import.path {
363 let canonical = normalize_path(import_path);
364 if seen.insert(canonical.clone()) {
365 next_wave.push(canonical);
366 }
367 }
368 }
369 modules.insert(path, module);
370 }
371 wave = next_wave;
372 }
373 resolve_re_exports(&mut modules);
374 ModuleGraphBuild {
375 graph: ModuleGraph {
376 modules,
377 _package_snapshots: package_snapshots,
378 },
379 parsed_sources,
380 }
381}
382
383pub const MODULE_GRAPH_JOBS_ENV: &str = "HARN_MODULE_GRAPH_JOBS";
386
387fn load_wave(
390 paths: &[PathBuf],
391 package_snapshots: &[PackageSnapshot],
392 parsed_source_retention: ParsedSourceRetention<'_>,
393 source_overrides: Option<&HashMap<PathBuf, String>>,
394) -> Vec<(ModuleInfo, Option<ParsedModuleSource>)> {
395 const MIN_PARALLEL_WAVE: usize = 8;
396 let configured = std::env::var(MODULE_GRAPH_JOBS_ENV)
397 .ok()
398 .and_then(|value| value.parse::<usize>().ok())
399 .filter(|&jobs| jobs > 0);
400 let workers = configured
401 .unwrap_or_else(|| {
402 std::thread::available_parallelism()
403 .map(std::num::NonZeroUsize::get)
404 .unwrap_or(1)
405 })
406 .min(paths.len());
407 if workers <= 1 || paths.len() < MIN_PARALLEL_WAVE {
408 return paths
409 .iter()
410 .map(|path| {
411 load_module(
412 path,
413 package_snapshots,
414 source_overrides,
415 parsed_source_retention.retains(path),
416 )
417 })
418 .collect();
419 }
420 let next = std::sync::atomic::AtomicUsize::new(0);
421 let mut produced: Vec<(usize, (ModuleInfo, Option<ParsedModuleSource>))> =
422 std::thread::scope(|scope| {
423 let handles: Vec<_> = (0..workers)
424 .map(|_| {
425 scope.spawn(|| {
426 let mut local = Vec::new();
427 loop {
428 let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
429 let Some(path) = paths.get(index) else {
430 break;
431 };
432 local.push((
433 index,
434 load_module(
435 path,
436 package_snapshots,
437 source_overrides,
438 parsed_source_retention.retains(path),
439 ),
440 ));
441 }
442 local
443 })
444 })
445 .collect();
446 handles
447 .into_iter()
448 .flat_map(|handle| match handle.join() {
449 Ok(local) => local,
450 Err(panic) => std::panic::resume_unwind(panic),
451 })
452 .collect()
453 });
454 produced.sort_unstable_by_key(|(index, _)| *index);
455 produced.into_iter().map(|(_, loaded)| loaded).collect()
456}
457
458fn resolve_re_exports(modules: &mut HashMap<PathBuf, ModuleInfo>) {
463 let keys: Vec<PathBuf> = modules.keys().cloned().collect();
464 loop {
465 let mut changed = false;
466 for path in &keys {
467 let wildcard_paths = modules
470 .get(path)
471 .map(|m| m.wildcard_re_export_paths.clone())
472 .unwrap_or_default();
473 if wildcard_paths.is_empty() {
474 continue;
475 }
476 let mut additions: Vec<String> = Vec::new();
477 for src in &wildcard_paths {
478 let src_canonical = normalize_path(src);
479 if let Some(src_module) = modules.get(src).or_else(|| modules.get(&src_canonical)) {
480 additions.extend(src_module.exports.iter().cloned());
481 }
482 }
483 if let Some(module) = modules.get_mut(path) {
484 for name in additions {
485 if module.exports.insert(name) {
486 changed = true;
487 }
488 }
489 }
490 }
491 if !changed {
492 break;
493 }
494 }
495}
496
497impl ModuleGraph {
498 pub fn module_paths(&self) -> Vec<PathBuf> {
503 let mut paths: Vec<PathBuf> = self.modules.keys().cloned().collect();
504 paths.sort();
505 paths
506 }
507
508 pub fn contains_module(&self, path: &Path) -> bool {
511 self.modules.contains_key(path) || self.modules.contains_key(&normalize_path(path))
512 }
513
514 pub fn all_selective_import_names(&self) -> HashSet<&str> {
516 let mut names = HashSet::new();
517 for module in self.modules.values() {
518 for name in &module.selective_import_names {
519 names.insert(name.as_str());
520 }
521 }
522 names
523 }
524
525 pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
528 let target = normalize_path(target);
529 let mut out: Vec<PathBuf> = self
530 .modules
531 .iter()
532 .filter(|(_, info)| {
533 info.imports.iter().any(|import| {
534 import
535 .path
536 .as_ref()
537 .is_some_and(|p| normalize_path(p) == target)
538 })
539 })
540 .map(|(path, _)| path.clone())
541 .collect();
542 out.sort();
543 out
544 }
545
546 pub fn transitive_importers_of(&self, target: &Path) -> Vec<PathBuf> {
553 let target = normalize_path(target);
554 let mut visited = HashSet::from([target.clone()]);
555 let mut pending = vec![target];
556 let mut out = Vec::new();
557
558 while let Some(current) = pending.pop() {
559 for importer in self.importers_of(¤t) {
560 let importer = normalize_path(&importer);
561 if visited.insert(importer.clone()) {
562 pending.push(importer.clone());
563 out.push(importer);
564 }
565 }
566 }
567
568 out.sort();
569 out
570 }
571
572 pub fn imports_for_module(&self, file: &Path) -> Vec<ModuleImport> {
574 let file = normalize_path(file);
575 let Some(module) = self.modules.get(&file) else {
576 return Vec::new();
577 };
578 let mut imports: Vec<ModuleImport> = module
579 .imports
580 .iter()
581 .map(|import| {
582 let mut selective_names = import
583 .selective_names
584 .as_ref()
585 .map(|names| names.iter().cloned().collect::<Vec<_>>());
586 if let Some(names) = selective_names.as_mut() {
587 names.sort();
588 }
589 ModuleImport {
590 raw_path: import.raw_path.clone(),
591 resolved_path: import.path.as_ref().map(|path| normalize_path(path)),
592 selective_names,
593 namespace_alias: import.namespace_alias.clone(),
594 is_pub: import.is_pub,
595 }
596 })
597 .collect();
598 imports.sort_by(|left, right| {
599 left.raw_path
600 .cmp(&right.raw_path)
601 .then_with(|| left.selective_names.cmp(&right.selective_names))
602 .then_with(|| left.namespace_alias.cmp(&right.namespace_alias))
603 .then_with(|| left.resolved_path.cmp(&right.resolved_path))
604 });
605 imports
606 }
607
608 pub fn exports_for_module(&self, file: &Path) -> Vec<String> {
610 let file = normalize_path(file);
611 let Some(module) = self.modules.get(&file) else {
612 return Vec::new();
613 };
614 let mut exports: Vec<String> = module.exports.iter().cloned().collect();
615 exports.sort();
616 exports
617 }
618
619 pub fn wildcard_exports_for(&self, file: &Path) -> WildcardResolution {
624 let file = normalize_path(file);
625 let Some(module) = self.modules.get(&file) else {
626 return WildcardResolution::Unknown;
627 };
628 if module.has_unresolved_wildcard_import {
629 return WildcardResolution::Unknown;
630 }
631
632 let mut names = HashSet::new();
633 for import in module
634 .imports
635 .iter()
636 .filter(|import| import.selective_names.is_none())
637 {
638 let Some(import_path) = &import.path else {
639 return WildcardResolution::Unknown;
640 };
641 let imported = self.modules.get(import_path).or_else(|| {
642 let normalized = normalize_path(import_path);
643 self.modules.get(&normalized)
644 });
645 let Some(imported) = imported else {
646 return WildcardResolution::Unknown;
647 };
648 names.extend(imported.exports.iter().cloned());
649 }
650 WildcardResolution::Resolved(names)
651 }
652
653 #[must_use]
674 pub fn import_compile_failures(&self, file: &Path) -> Vec<ImportCompileFailure> {
675 let file = normalize_path(file);
676 let Some(module) = self.modules.get(&file) else {
677 return Vec::new();
678 };
679 let mut failures = Vec::new();
680 for import in &module.imports {
681 let Some(import_path) = &import.path else {
682 continue;
683 };
684 let Some(target) = self
685 .modules
686 .get(import_path)
687 .or_else(|| self.modules.get(&normalize_path(import_path)))
688 else {
689 continue;
690 };
691 if let Some(error) = &target.load_error {
692 failures.push(ImportCompileFailure {
693 import_raw_path: import.raw_path.clone(),
694 import_span: import.import_span,
695 module_path: normalize_path(import_path),
696 error: error.clone(),
697 });
698 }
699 }
700 failures
701 }
702
703 pub fn imported_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
704 let file = normalize_path(file);
705 let module = self.modules.get(&file)?;
706 if module.has_unresolved_wildcard_import
707 || module.has_unresolved_selective_import
708 || module.has_unresolved_namespace_import
709 {
710 return None;
711 }
712
713 let mut names = HashSet::new();
714 for import in &module.imports {
715 if let Some(alias) = &import.namespace_alias {
717 names.insert(alias.clone());
718 continue;
719 }
720 let import_path = import.path.as_ref()?;
721 let imported = self
722 .modules
723 .get(import_path)
724 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
725 if imported.load_error.is_some() {
731 return None;
732 }
733 match &import.selective_names {
734 None => {
735 names.extend(imported.exports.iter().cloned());
736 }
737 Some(selective) => {
738 for name in selective {
747 if imported.declarations.contains_key(name)
748 || imported.exports.contains(name)
749 {
750 names.insert(name.clone());
751 }
752 }
753 }
754 }
755 }
756 Some(names)
757 }
758
759 pub fn imported_names_by_kind_for_file(
764 &self,
765 file: &Path,
766 kind: DefKind,
767 ) -> Option<HashSet<String>> {
768 let file = normalize_path(file);
769 let module = self.modules.get(&file)?;
770 if module.has_unresolved_wildcard_import
771 || module.has_unresolved_selective_import
772 || module.has_unresolved_namespace_import
773 {
774 return None;
775 }
776
777 let mut names = HashSet::new();
778 for import in &module.imports {
779 if import.namespace_alias.is_some() {
781 continue;
782 }
783 let import_path = import.path.as_ref()?;
784 let imported_names: Vec<String> = match &import.selective_names {
785 Some(selective) => selective.iter().cloned().collect(),
786 None => self
787 .modules
788 .get(import_path)
789 .or_else(|| self.modules.get(&normalize_path(import_path)))?
790 .exports
791 .iter()
792 .cloned()
793 .collect(),
794 };
795 for name in imported_names {
796 if self.exported_kind(import_path, &name) == Some(kind) {
797 names.insert(name);
798 }
799 }
800 }
801 Some(names)
802 }
803
804 pub fn imported_callable_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
809 let mut names = HashSet::new();
810 for kind in [
811 DefKind::Function,
812 DefKind::Pipeline,
813 DefKind::Tool,
814 DefKind::Struct,
815 ] {
816 names.extend(self.imported_names_by_kind_for_file(file, kind)?);
817 }
818 Some(names)
819 }
820
821 pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
825 let file = normalize_path(file);
826 let module = self.modules.get(&file)?;
827 if module.has_unresolved_wildcard_import
828 || module.has_unresolved_selective_import
829 || module.has_unresolved_namespace_import
830 {
831 return None;
832 }
833
834 let mut decls = Vec::new();
835 let mut seen = HashSet::new();
836 for import in &module.imports {
837 if import.namespace_alias.is_some() {
839 continue;
840 }
841 let import_path = import.path.as_ref()?;
842 let imported = self
843 .modules
844 .get(import_path)
845 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
846 if imported.load_error.is_some() {
852 return None;
853 }
854 let mut names_to_collect: Vec<String> = match &import.selective_names {
855 None => imported.exports.iter().cloned().collect(),
856 Some(selective) => selective.iter().cloned().collect(),
857 };
858 names_to_collect.sort();
859 for name in &names_to_collect {
860 let mut visited = HashSet::new();
861 if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
862 let origin = self
863 .export_definition_of(import_path, name)
864 .map_or_else(|| import_path.clone(), |definition| definition.file);
865 self.extend_type_dependency(&origin, &decl, &mut decls, &mut seen);
866 }
867 }
868 for ty_decl in &imported.type_declarations {
878 if type_decl_name(ty_decl).is_some() {
879 self.extend_type_dependency(import_path, ty_decl, &mut decls, &mut seen);
880 }
881 }
882
883 for name in &names_to_collect {
884 let mut visited = HashSet::new();
885 let Some(callable) =
886 self.find_exported_callable_decl(import_path, name, &mut visited)
887 else {
888 continue;
889 };
890 let origin = self
891 .export_definition_of(import_path, name)
892 .map_or_else(|| import_path.clone(), |definition| definition.file);
893 self.extend_callable_type_dependencies(&origin, &callable, &mut decls, &mut seen);
894 }
895 }
896 Some(decls)
897 }
898
899 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(_) => path.to_path_buf(),
1492 }
1493}
1494
1495#[cfg(test)]
1496#[path = "tests.rs"]
1497mod tests;