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 std::thread::Builder::new()
433 .name("harn-module-parse".to_owned())
434 .stack_size(harn_parser::PARSE_STACK_SIZE)
435 .spawn_scoped(scope, || {
436 let mut local = Vec::new();
437 loop {
438 let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
439 let Some(path) = paths.get(index) else {
440 break;
441 };
442 local.push((
443 index,
444 load_module(
445 path,
446 package_snapshots,
447 source_overrides,
448 parsed_source_retention.retains(path),
449 ),
450 ));
451 }
452 local
453 })
454 .expect("spawn module-parse worker")
455 })
456 .collect();
457 handles
458 .into_iter()
459 .flat_map(|handle| match handle.join() {
460 Ok(local) => local,
461 Err(panic) => std::panic::resume_unwind(panic),
462 })
463 .collect()
464 });
465 produced.sort_unstable_by_key(|(index, _)| *index);
466 produced.into_iter().map(|(_, loaded)| loaded).collect()
467}
468
469fn resolve_re_exports(modules: &mut HashMap<PathBuf, ModuleInfo>) {
474 let keys: Vec<PathBuf> = modules.keys().cloned().collect();
475 loop {
476 let mut changed = false;
477 for path in &keys {
478 let wildcard_paths = modules
481 .get(path)
482 .map(|m| m.wildcard_re_export_paths.clone())
483 .unwrap_or_default();
484 if wildcard_paths.is_empty() {
485 continue;
486 }
487 let mut additions: Vec<String> = Vec::new();
488 for src in &wildcard_paths {
489 let src_canonical = normalize_path(src);
490 if let Some(src_module) = modules.get(src).or_else(|| modules.get(&src_canonical)) {
491 additions.extend(src_module.exports.iter().cloned());
492 }
493 }
494 if let Some(module) = modules.get_mut(path) {
495 for name in additions {
496 if module.exports.insert(name) {
497 changed = true;
498 }
499 }
500 }
501 }
502 if !changed {
503 break;
504 }
505 }
506}
507
508impl ModuleGraph {
509 pub fn module_paths(&self) -> Vec<PathBuf> {
514 let mut paths: Vec<PathBuf> = self.modules.keys().cloned().collect();
515 paths.sort();
516 paths
517 }
518
519 pub fn contains_module(&self, path: &Path) -> bool {
522 self.modules.contains_key(path) || self.modules.contains_key(&normalize_path(path))
523 }
524
525 pub fn all_selective_import_names(&self) -> HashSet<&str> {
527 let mut names = HashSet::new();
528 for module in self.modules.values() {
529 for name in &module.selective_import_names {
530 names.insert(name.as_str());
531 }
532 }
533 names
534 }
535
536 pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
539 let target = normalize_path(target);
540 let mut out: Vec<PathBuf> = self
541 .modules
542 .iter()
543 .filter(|(_, info)| {
544 info.imports.iter().any(|import| {
545 import
546 .path
547 .as_ref()
548 .is_some_and(|p| normalize_path(p) == target)
549 })
550 })
551 .map(|(path, _)| path.clone())
552 .collect();
553 out.sort();
554 out
555 }
556
557 pub fn transitive_importers_of(&self, target: &Path) -> Vec<PathBuf> {
564 let target = normalize_path(target);
565 let mut visited = HashSet::from([target.clone()]);
566 let mut pending = vec![target];
567 let mut out = Vec::new();
568
569 while let Some(current) = pending.pop() {
570 for importer in self.importers_of(¤t) {
571 let importer = normalize_path(&importer);
572 if visited.insert(importer.clone()) {
573 pending.push(importer.clone());
574 out.push(importer);
575 }
576 }
577 }
578
579 out.sort();
580 out
581 }
582
583 pub fn imports_for_module(&self, file: &Path) -> Vec<ModuleImport> {
585 let file = normalize_path(file);
586 let Some(module) = self.modules.get(&file) else {
587 return Vec::new();
588 };
589 let mut imports: Vec<ModuleImport> = module
590 .imports
591 .iter()
592 .map(|import| {
593 let mut selective_names = import
594 .selective_names
595 .as_ref()
596 .map(|names| names.iter().cloned().collect::<Vec<_>>());
597 if let Some(names) = selective_names.as_mut() {
598 names.sort();
599 }
600 ModuleImport {
601 raw_path: import.raw_path.clone(),
602 resolved_path: import.path.as_ref().map(|path| normalize_path(path)),
603 selective_names,
604 namespace_alias: import.namespace_alias.clone(),
605 is_pub: import.is_pub,
606 }
607 })
608 .collect();
609 imports.sort_by(|left, right| {
610 left.raw_path
611 .cmp(&right.raw_path)
612 .then_with(|| left.selective_names.cmp(&right.selective_names))
613 .then_with(|| left.namespace_alias.cmp(&right.namespace_alias))
614 .then_with(|| left.resolved_path.cmp(&right.resolved_path))
615 });
616 imports
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(self.exports_for_import(&file, import_path));
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(self.exports_for_import(&file, import_path));
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.exports_for_import(&file, import_path),
787 };
788 for name in imported_names {
789 if self.exported_kind_for_import(&file, import_path, &name) == Some(kind) {
790 names.insert(name);
791 }
792 }
793 }
794 Some(names)
795 }
796
797 pub fn imported_callable_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
802 let mut names = HashSet::new();
803 for kind in [
804 DefKind::Function,
805 DefKind::Pipeline,
806 DefKind::Tool,
807 DefKind::Struct,
808 ] {
809 names.extend(self.imported_names_by_kind_for_file(file, kind)?);
810 }
811 Some(names)
812 }
813
814 pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
818 let file = normalize_path(file);
819 let module = self.modules.get(&file)?;
820 if module.has_unresolved_wildcard_import
821 || module.has_unresolved_selective_import
822 || module.has_unresolved_namespace_import
823 {
824 return None;
825 }
826
827 let mut decls = Vec::new();
828 let mut seen = HashSet::new();
829 for import in &module.imports {
830 if import.namespace_alias.is_some() {
832 continue;
833 }
834 let import_path = import.path.as_ref()?;
835 let imported = self
836 .modules
837 .get(import_path)
838 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
839 if imported.load_error.is_some() {
845 return None;
846 }
847 let mut names_to_collect: Vec<String> = match &import.selective_names {
848 None => imported.exports.iter().cloned().collect(),
849 Some(selective) => selective.iter().cloned().collect(),
850 };
851 names_to_collect.sort();
852 for name in &names_to_collect {
853 let mut visited = HashSet::new();
854 if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
855 let origin = self
856 .export_definition_of(import_path, name)
857 .map_or_else(|| import_path.clone(), |definition| definition.file);
858 self.extend_type_dependency(&origin, &decl, &mut decls, &mut seen);
859 }
860 }
861 for ty_decl in &imported.type_declarations {
871 if type_decl_name(ty_decl).is_some() {
872 self.extend_type_dependency(import_path, ty_decl, &mut decls, &mut seen);
873 }
874 }
875
876 for name in &names_to_collect {
877 let mut visited = HashSet::new();
878 let Some(callable) =
879 self.find_exported_callable_decl(import_path, name, &mut visited)
880 else {
881 continue;
882 };
883 let origin = self
884 .export_definition_of(import_path, name)
885 .map_or_else(|| import_path.clone(), |definition| definition.file);
886 self.extend_callable_type_dependencies(&origin, &callable, &mut decls, &mut seen);
887 }
888 }
889 Some(decls)
890 }
891
892 pub fn imported_callable_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
894 let file = normalize_path(file);
895 let module = self.modules.get(&file)?;
896 if module.has_unresolved_wildcard_import
897 || module.has_unresolved_selective_import
898 || module.has_unresolved_namespace_import
899 {
900 return None;
901 }
902
903 let mut decls = Vec::new();
904 for import in &module.imports {
905 if import.namespace_alias.is_some() {
907 continue;
908 }
909 let import_path = import.path.as_ref()?;
910 let imported = self
911 .modules
912 .get(import_path)
913 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
914 if imported.load_error.is_some() {
920 return None;
921 }
922 let selective_import = import.selective_names.is_some();
923 let names_to_collect: Vec<String> = match &import.selective_names {
924 None => self.exports_for_import(&file, import_path),
925 Some(selective) => selective.iter().cloned().collect(),
926 };
927 for name in &names_to_collect {
928 if selective_import
929 || imported.own_exports.contains(name)
930 || (sibling_module_access(&file, import_path)
931 && imported.sibling_exports.contains(name))
932 {
933 if let Some(decl) = imported
934 .callable_declarations
935 .iter()
936 .find(|decl| callable_decl_name(decl) == Some(name.as_str()))
937 {
938 decls.push(decl.clone());
939 continue;
940 }
941 }
942 let mut visited = HashSet::new();
943 if let Some(decl) =
944 self.find_exported_callable_decl(import_path, name, &mut visited)
945 {
946 decls.push(decl);
947 }
948 }
949 }
950 Some(decls)
951 }
952
953 fn find_exported_type_decl(
956 &self,
957 path: &Path,
958 name: &str,
959 visited: &mut HashSet<PathBuf>,
960 ) -> Option<SNode> {
961 let canonical = normalize_path(path);
962 if !visited.insert(canonical.clone()) {
963 return None;
964 }
965 let module = self
966 .modules
967 .get(&canonical)
968 .or_else(|| self.modules.get(path))?;
969 for decl in &module.type_declarations {
970 if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
971 return Some(decl.clone());
972 }
973 }
974 if let Some(sources) = module.selective_re_exports.get(name) {
975 for source in sources {
976 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
977 return Some(decl);
978 }
979 }
980 }
981 for source in &module.wildcard_re_export_paths {
982 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
983 return Some(decl);
984 }
985 }
986 None
987 }
988
989 fn find_exported_callable_decl(
990 &self,
991 path: &Path,
992 name: &str,
993 visited: &mut HashSet<PathBuf>,
994 ) -> Option<SNode> {
995 let canonical = normalize_path(path);
996 if !visited.insert(canonical.clone()) {
997 return None;
998 }
999 let module = self
1000 .modules
1001 .get(&canonical)
1002 .or_else(|| self.modules.get(path))?;
1003 for decl in &module.callable_declarations {
1004 if callable_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
1005 return Some(decl.clone());
1006 }
1007 }
1008 if let Some(sources) = module.selective_re_exports.get(name) {
1009 for source in sources {
1010 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
1011 return Some(decl);
1012 }
1013 }
1014 }
1015 for source in &module.wildcard_re_export_paths {
1016 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
1017 return Some(decl);
1018 }
1019 }
1020 None
1021 }
1022
1023 pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
1029 let mut visited = HashSet::new();
1030 self.definition_of_inner(file, name, &mut visited)
1031 }
1032
1033 pub fn export_definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
1040 let mut visited = HashSet::new();
1041 self.export_definition_of_inner(file, name, &mut visited)
1042 }
1043
1044 pub fn declared_names_for_file(&self, file: &Path) -> Option<Vec<&str>> {
1048 let module = self.modules.get(&normalize_path(file))?;
1049 let mut names: Vec<&str> = module.declarations.keys().map(String::as_str).collect();
1050 names.sort_unstable();
1051 Some(names)
1052 }
1053
1054 fn definition_of_inner(
1055 &self,
1056 file: &Path,
1057 name: &str,
1058 visited: &mut HashSet<PathBuf>,
1059 ) -> Option<DefSite> {
1060 let file = normalize_path(file);
1061 if !visited.insert(file.clone()) {
1062 return None;
1063 }
1064 let current = self.modules.get(&file)?;
1065
1066 if let Some(local) = current.declarations.get(name) {
1067 return Some(local.clone());
1068 }
1069
1070 if let Some(sources) = current.selective_re_exports.get(name) {
1075 for source in sources {
1076 if let Some(def) = self.definition_of_inner(source, name, visited) {
1077 return Some(def);
1078 }
1079 }
1080 }
1081
1082 for source in ¤t.wildcard_re_export_paths {
1084 if let Some(def) = self.definition_of_inner(source, name, visited) {
1085 return Some(def);
1086 }
1087 }
1088
1089 for import in ¤t.imports {
1091 let Some(selective_names) = &import.selective_names else {
1092 continue;
1093 };
1094 if !selective_names.contains(name) {
1095 continue;
1096 }
1097 if let Some(path) = &import.path {
1098 if let Some(def) = self.definition_of_inner(path, name, visited) {
1099 return Some(def);
1100 }
1101 }
1102 }
1103
1104 for import in ¤t.imports {
1106 if import.selective_names.is_some() || import.namespace_alias.is_some() {
1107 continue;
1108 }
1109 if let Some(path) = &import.path {
1110 if let Some(def) = self.definition_of_inner(path, name, visited) {
1111 return Some(def);
1112 }
1113 }
1114 }
1115
1116 None
1117 }
1118
1119 fn export_definition_of_inner(
1120 &self,
1121 file: &Path,
1122 name: &str,
1123 visited: &mut HashSet<PathBuf>,
1124 ) -> Option<DefSite> {
1125 let file = normalize_path(file);
1126 if !visited.insert(file.clone()) {
1127 return None;
1128 }
1129 let current = self.modules.get(&file)?;
1130
1131 if current.own_exports.contains(name) {
1132 if let Some(local) = current.declarations.get(name) {
1133 return Some(local.clone());
1134 }
1135 }
1136 if let Some(sources) = current.selective_re_exports.get(name) {
1137 for source in sources {
1138 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1139 return Some(definition);
1140 }
1141 }
1142 }
1143 for source in ¤t.wildcard_re_export_paths {
1144 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1145 return Some(definition);
1146 }
1147 }
1148 None
1149 }
1150
1151 pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
1155 let file = normalize_path(file);
1156 let Some(module) = self.modules.get(&file) else {
1157 return Vec::new();
1158 };
1159
1160 let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
1164
1165 for (name, srcs) in &module.selective_re_exports {
1166 sources
1167 .entry(name.clone())
1168 .or_default()
1169 .extend(srcs.iter().cloned());
1170 }
1171 for src in &module.wildcard_re_export_paths {
1172 let canonical = normalize_path(src);
1173 let Some(src_module) = self
1174 .modules
1175 .get(&canonical)
1176 .or_else(|| self.modules.get(src))
1177 else {
1178 continue;
1179 };
1180 for name in &src_module.exports {
1181 sources
1182 .entry(name.clone())
1183 .or_default()
1184 .push(canonical.clone());
1185 }
1186 }
1187
1188 for name in &module.own_exports {
1192 if let Some(entry) = sources.get_mut(name) {
1193 entry.push(file.clone());
1194 }
1195 }
1196
1197 let mut conflicts = Vec::new();
1198 for (name, mut srcs) in sources {
1199 srcs.sort();
1200 srcs.dedup();
1201 if srcs.len() > 1 {
1202 conflicts.push(ReExportConflict {
1203 name,
1204 sources: srcs,
1205 });
1206 }
1207 }
1208 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
1209 conflicts
1210 }
1211
1212 pub fn selective_import_issues(&self, file: &Path) -> Vec<SelectiveImportIssue> {
1218 let file = normalize_path(file);
1219 let Some(module) = self.modules.get(&file) else {
1220 return Vec::new();
1221 };
1222
1223 let mut out = Vec::new();
1224 for import in &module.imports {
1225 let Some(selective) = &import.selective_names else {
1226 continue;
1227 };
1228 let Some(import_path) = &import.path else {
1229 continue;
1230 };
1231 let Some(target) = self
1232 .modules
1233 .get(import_path)
1234 .or_else(|| self.modules.get(&normalize_path(import_path)))
1235 else {
1236 continue;
1237 };
1238 if target.load_error.is_some() {
1239 continue;
1240 }
1241 let visible: HashSet<_> = self
1242 .exports_for_import(&file, import_path)
1243 .into_iter()
1244 .collect();
1245 for name in selective {
1246 let can_bind = visible.contains(name);
1247 let can_publish = !import.is_pub || target.exports.contains(name);
1248 let kind = if can_bind && can_publish {
1249 continue;
1250 } else if target.declarations.contains_key(name) {
1251 SelectiveImportIssueKind::Private
1252 } else {
1253 SelectiveImportIssueKind::Missing
1254 };
1255 out.push(SelectiveImportIssue {
1256 name: name.clone(),
1257 module: import.raw_path.clone(),
1258 span: import.import_span,
1259 kind,
1260 });
1261 }
1262 }
1263 out.sort_by(|a, b| (&a.name, &a.module, a.kind).cmp(&(&b.name, &b.module, b.kind)));
1264 out.dedup();
1265 out
1266 }
1267
1268 pub fn exported_kind(&self, file: &Path, name: &str) -> Option<DefKind> {
1272 self.exported_kind_inner(file, name, &mut HashSet::new())
1273 }
1274
1275 fn exported_kind_inner(
1276 &self,
1277 file: &Path,
1278 name: &str,
1279 visited: &mut HashSet<PathBuf>,
1280 ) -> Option<DefKind> {
1281 let file = normalize_path(file);
1282 if !visited.insert(file.clone()) {
1283 return None;
1284 }
1285 let result = self.modules.get(&file).and_then(|module| {
1286 if module.own_exports.contains(name) {
1287 return module
1288 .declarations
1289 .get(name)
1290 .map(|definition| definition.kind)
1291 .or_else(|| {
1292 stdlib_module_name(&file).and_then(|stdlib_module| {
1293 stdlib::builtin_reexports(stdlib_module)
1294 .contains(&name)
1295 .then_some(DefKind::Function)
1296 })
1297 });
1298 }
1299 if let Some(sources) = module.selective_re_exports.get(name) {
1300 for source in sources {
1301 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1302 return Some(kind);
1303 }
1304 }
1305 }
1306 for source in &module.wildcard_re_export_paths {
1307 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1308 return Some(kind);
1309 }
1310 }
1311 None
1312 });
1313 visited.remove(&file);
1314 result
1315 }
1316}
1317
1318#[derive(Debug, Clone, PartialEq, Eq)]
1321pub struct ReExportConflict {
1322 pub name: String,
1323 pub sources: Vec<PathBuf>,
1324}
1325
1326#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1328pub enum SelectiveImportIssueKind {
1329 Missing,
1331 Private,
1333}
1334
1335#[derive(Debug, Clone, PartialEq, Eq)]
1337pub struct SelectiveImportIssue {
1338 pub name: String,
1340 pub module: String,
1342 pub span: Span,
1344 pub kind: SelectiveImportIssueKind,
1346}
1347
1348impl SelectiveImportIssue {
1349 #[must_use]
1351 pub fn message(&self) -> String {
1352 match self.kind {
1353 SelectiveImportIssueKind::Missing => format!(
1354 "imported symbol `{}` does not exist in `{}`",
1355 self.name, self.module
1356 ),
1357 SelectiveImportIssueKind::Private => format!(
1358 "imported symbol `{}` is not exported by `{}` — it is defined there but not `pub`",
1359 self.name, self.module
1360 ),
1361 }
1362 }
1363
1364 #[must_use]
1366 pub fn help(&self) -> String {
1367 match self.kind {
1368 SelectiveImportIssueKind::Missing => format!(
1369 "update the import to a symbol exported by `{}`",
1370 self.module
1371 ),
1372 SelectiveImportIssueKind::Private => {
1373 format!(
1374 "mark `{}` as `pub` in `{}` to export it",
1375 self.name, self.module
1376 )
1377 }
1378 }
1379 }
1380}
1381
1382fn load_module(
1383 path: &Path,
1384 package_snapshots: &[PackageSnapshot],
1385 source_overrides: Option<&HashMap<PathBuf, String>>,
1386 retain_parsed_source: bool,
1387) -> (ModuleInfo, Option<ParsedModuleSource>) {
1388 let source = source_overrides
1389 .and_then(|overrides| overrides.get(&normalize_path(path)).cloned())
1390 .or_else(|| read_module_source(path));
1391 let Some(source) = source else {
1392 return (ModuleInfo::default(), None);
1393 };
1394 let mut lexer = harn_lexer::Lexer::new(&source);
1395 let tokens = match lexer.tokenize() {
1396 Ok(tokens) => tokens,
1397 Err(error) => {
1398 let module = ModuleInfo {
1399 load_error: Some(ModuleLoadError {
1400 message: error.to_string(),
1401 span: error.span(),
1402 }),
1403 ..ModuleInfo::default()
1404 };
1405 return (module, None);
1406 }
1407 };
1408 let mut parser = Parser::new(tokens);
1409 let program = match parser.parse() {
1410 Ok(program) => program,
1411 Err(error) => {
1412 let module = ModuleInfo {
1413 load_error: Some(ModuleLoadError {
1414 message: error.to_string(),
1415 span: error.span(),
1416 }),
1417 ..ModuleInfo::default()
1418 };
1419 return (module, None);
1420 }
1421 };
1422
1423 let mut module = ModuleInfo::default();
1424 for node in &program {
1425 collect_module_info(path, node, &mut module, package_snapshots);
1426 collect_type_declarations(node, &mut module.type_declarations);
1427 collect_callable_declarations(node, &mut module.callable_declarations);
1428 }
1429 if let Some(stdlib_module) = stdlib_module_name(path) {
1430 module.own_exports.extend(
1431 stdlib::builtin_reexports(stdlib_module)
1432 .iter()
1433 .map(|name| (*name).to_string()),
1434 );
1435 }
1436 module.exports.extend(module.own_exports.iter().cloned());
1440 module
1441 .exports
1442 .extend(module.selective_re_exports.keys().cloned());
1443 let parsed = retain_parsed_source.then_some(ParsedModuleSource { source, program });
1444 (module, parsed)
1445}
1446
1447pub fn stdlib_module_name(path: &Path) -> Option<&str> {
1450 let s = path.to_str()?;
1451 s.strip_prefix("<std>/")
1452}
1453
1454fn normalize_path(path: &Path) -> PathBuf {
1455 canonical_path(path)
1456}
1457
1458pub fn canonical_path(path: &Path) -> PathBuf {
1471 use std::sync::OnceLock;
1472 if stdlib_module_name(path).is_some() {
1473 return path.to_path_buf();
1474 }
1475 static MEMO: OnceLock<std::sync::Mutex<HashMap<PathBuf, PathBuf>>> = OnceLock::new();
1476 let memo = MEMO.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
1477 if let Some(hit) = memo
1478 .lock()
1479 .expect("canonical path memo lock poisoned")
1480 .get(path)
1481 .cloned()
1482 {
1483 return hit;
1484 }
1485 match path.canonicalize() {
1486 Ok(canonical) => {
1487 memo.lock()
1488 .expect("canonical path memo lock poisoned")
1489 .insert(path.to_path_buf(), canonical.clone());
1490 canonical
1491 }
1492 Err(_) => manifest_walk::normalize_lexically(path),
1495 }
1496}
1497
1498#[cfg(test)]
1499#[path = "tests.rs"]
1500mod tests;