1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4use crate::{package_imports::acquire_package_snapshots, package_snapshot::PackageSnapshot};
5use harn_lexer::Span;
6use harn_parser::{Parser, SNode};
7
8pub mod asset_paths;
9mod construction;
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 source_reads;
24pub use source_reads::{
25 build_with_source, build_with_sources, module_source_disk_reads, read_module_source,
26};
27mod references;
28mod standalone;
29mod stdlib;
30pub use stdlib::{stdlib_catalog, try_stdlib_catalog};
31mod symbol_reachability;
32mod type_dependencies;
33mod typecheck;
34mod visibility;
35
36pub use construction::{
37 build, build_closed_program, build_for_reference_index, build_with_module_projection,
38 build_with_parsed_sources,
39};
40
41use declarations::{
42 callable_decl_name, collect_callable_declarations, collect_module_info,
43 collect_type_declarations, decl_site, type_decl_name,
44};
45pub use declarations::{public_declarations, sibling_declarations, DefKind, PublicDeclaration};
46pub use namespace_imports::NamespaceImportInfo;
47pub use namespace_signatures::NamespaceMemberSignature;
48pub use package_imports::{
49 resolve_import_path, resolve_import_path_with_guard, resolve_import_path_with_snapshot,
50 unresolved_package_alias, PackageImport,
51};
52pub use references::{index_references, RefSite, ReferenceEdge, ReferenceIndex};
53pub use standalone::build_with_standalone_source;
54use standalone::PackageContext;
55pub use symbol_reachability::{
56 closed_program_reachability, ExportDemand, ModuleSymbolDemand, SymbolReachability,
57};
58pub use visibility::{sibling_directory_access, sibling_module_access};
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct DefSite {
63 pub name: String,
64 pub file: PathBuf,
65 pub kind: DefKind,
66 pub span: Span,
67}
68
69#[derive(Debug, Clone)]
71pub enum WildcardResolution {
72 Resolved(HashSet<String>),
74 Unknown,
76}
77
78#[derive(Debug, Default)]
80pub struct ModuleGraph {
81 modules: HashMap<PathBuf, ModuleInfo>,
82 _package_snapshots: Vec<PackageSnapshot>,
84}
85
86#[derive(Debug, Clone)]
87pub struct ParsedModuleSource {
88 pub source: String,
89 pub program: Vec<SNode>,
90}
91
92#[derive(Debug, Default)]
93pub struct ModuleGraphBuild {
94 pub graph: ModuleGraph,
95 pub parsed_sources: HashMap<PathBuf, ParsedModuleSource>,
96}
97
98#[derive(Debug, Default)]
99struct ModuleInfo {
100 declarations: HashMap<String, DefSite>,
103 exports: HashSet<String>,
108 own_exports: HashSet<String>,
111 sibling_exports: HashSet<String>,
114 selective_re_exports: HashMap<String, Vec<PathBuf>>,
121 wildcard_re_export_paths: Vec<PathBuf>,
125 namespace_re_exports: HashMap<String, PathBuf>,
132 selective_import_names: HashSet<String>,
134 imports: Vec<ImportRef>,
136 has_unresolved_wildcard_import: bool,
138 has_unresolved_selective_import: bool,
142 has_unresolved_namespace_import: bool,
144 type_declarations: Vec<SNode>,
147 callable_declarations: Vec<SNode>,
150 load_error: Option<ModuleLoadError>,
156}
157
158#[derive(Debug, Clone)]
164pub struct ModuleLoadError {
165 pub message: String,
167 pub span: Span,
169}
170
171#[derive(Debug, Clone)]
174pub struct ImportCompileFailure {
175 pub import_raw_path: String,
177 pub import_span: Span,
179 pub module_path: PathBuf,
181 pub error: ModuleLoadError,
183}
184
185#[derive(Debug, Clone)]
186struct ImportRef {
187 raw_path: String,
188 path: Option<PathBuf>,
189 selective_names: Option<HashSet<String>>,
190 namespace_alias: Option<String>,
193 is_pub: bool,
194 import_span: Span,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
199pub struct ModuleImport {
200 pub raw_path: String,
202 pub resolved_path: Option<PathBuf>,
204 pub selective_names: Option<Vec<String>>,
206 pub namespace_alias: Option<String>,
208 pub is_pub: bool,
210}
211
212type ModuleObserver<'a> = dyn Fn(&Path, &str, Option<&[SNode]>) + Sync + 'a;
213
214#[derive(Clone, Copy)]
215enum ParsedSourceRetention<'a> {
216 None,
217 Seeds(&'a HashSet<PathBuf>),
218 All,
219}
220
221impl ParsedSourceRetention<'_> {
222 fn retains(self, path: &Path) -> bool {
223 match self {
224 Self::None => false,
225 Self::Seeds(targets) => targets.contains(path),
226 Self::All => true,
227 }
228 }
229}
230
231fn build_inner(
232 files: &[PathBuf],
233 parsed_source_retention: ParsedSourceRetention<'_>,
234 source_overrides: Option<&HashMap<PathBuf, String>>,
235 package_context: PackageContext,
236 observer: Option<&ModuleObserver<'_>>,
237) -> ModuleGraphBuild {
238 let package_snapshots = match package_context {
239 PackageContext::Project => acquire_package_snapshots(files),
240 PackageContext::Standalone => Vec::new(),
241 };
242 let mut modules: HashMap<PathBuf, ModuleInfo> = HashMap::new();
243 let mut parsed_sources: HashMap<PathBuf, ParsedModuleSource> = HashMap::new();
244 let mut seen: HashSet<PathBuf> = HashSet::new();
245 let mut wave: Vec<PathBuf> = Vec::new();
246 for file in files {
247 let canonical = normalize_path(file);
248 if seen.insert(canonical.clone()) {
249 wave.push(canonical);
250 }
251 }
252 while !wave.is_empty() {
260 let loaded = load_wave(
261 &wave,
262 &package_snapshots,
263 parsed_source_retention,
264 source_overrides,
265 observer,
266 );
267 let mut next_wave: Vec<PathBuf> = Vec::new();
268 for (path, (module, parsed)) in wave.drain(..).zip(loaded) {
269 if parsed_source_retention.retains(&path) {
270 if let Some(parsed) = parsed {
271 parsed_sources.insert(path.clone(), parsed);
272 }
273 }
274 for import in &module.imports {
291 if let Some(import_path) = &import.path {
292 let canonical = normalize_path(import_path);
293 if seen.insert(canonical.clone()) {
294 next_wave.push(canonical);
295 }
296 }
297 }
298 modules.insert(path, module);
299 }
300 wave = next_wave;
301 }
302 resolve_re_exports(&mut modules);
303 ModuleGraphBuild {
304 graph: ModuleGraph {
305 modules,
306 _package_snapshots: package_snapshots,
307 },
308 parsed_sources,
309 }
310}
311
312pub const MODULE_GRAPH_JOBS_ENV: &str = "HARN_MODULE_GRAPH_JOBS";
315
316fn load_wave(
319 paths: &[PathBuf],
320 package_snapshots: &[PackageSnapshot],
321 parsed_source_retention: ParsedSourceRetention<'_>,
322 source_overrides: Option<&HashMap<PathBuf, String>>,
323 observer: Option<&ModuleObserver<'_>>,
324) -> Vec<(ModuleInfo, Option<ParsedModuleSource>)> {
325 const MIN_PARALLEL_WAVE: usize = 8;
326 let configured = std::env::var(MODULE_GRAPH_JOBS_ENV)
327 .ok()
328 .and_then(|value| value.parse::<usize>().ok())
329 .filter(|&jobs| jobs > 0);
330 let workers = configured
331 .unwrap_or_else(|| {
332 std::thread::available_parallelism()
333 .map(std::num::NonZeroUsize::get)
334 .unwrap_or(1)
335 })
336 .min(paths.len());
337 if workers <= 1 || paths.len() < MIN_PARALLEL_WAVE {
338 return paths
339 .iter()
340 .map(|path| {
341 load_module(
342 path,
343 package_snapshots,
344 source_overrides,
345 parsed_source_retention.retains(path),
346 observer,
347 )
348 })
349 .collect();
350 }
351 let next = std::sync::atomic::AtomicUsize::new(0);
352 let mut produced: Vec<(usize, (ModuleInfo, Option<ParsedModuleSource>))> =
353 harn_parser::runtime_stack::scope(|scope| {
354 let handles: Vec<_> = (0..workers)
355 .map(|_| {
356 scope
357 .spawn_named("harn-module-parse", || {
358 let mut local = Vec::new();
359 loop {
360 let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
361 let Some(path) = paths.get(index) else {
362 break;
363 };
364 local.push((
365 index,
366 load_module(
367 path,
368 package_snapshots,
369 source_overrides,
370 parsed_source_retention.retains(path),
371 observer,
372 ),
373 ));
374 }
375 local
376 })
377 .expect("spawn module-parse worker")
378 })
379 .collect();
380 handles
381 .into_iter()
382 .flat_map(|handle| match handle.join() {
383 Ok(local) => local,
384 Err(panic) => std::panic::resume_unwind(panic),
385 })
386 .collect()
387 });
388 produced.sort_unstable_by_key(|(index, _)| *index);
389 produced.into_iter().map(|(_, loaded)| loaded).collect()
390}
391
392fn resolve_re_exports(modules: &mut HashMap<PathBuf, ModuleInfo>) {
397 let keys: Vec<PathBuf> = modules.keys().cloned().collect();
398 loop {
399 let mut changed = false;
400 for path in &keys {
401 let wildcard_paths = modules
404 .get(path)
405 .map(|m| m.wildcard_re_export_paths.clone())
406 .unwrap_or_default();
407 if wildcard_paths.is_empty() {
408 continue;
409 }
410 let mut additions: Vec<String> = Vec::new();
411 for src in &wildcard_paths {
412 let src_canonical = normalize_path(src);
413 if let Some(src_module) = modules.get(src).or_else(|| modules.get(&src_canonical)) {
414 additions.extend(src_module.exports.iter().cloned());
415 }
416 }
417 if let Some(module) = modules.get_mut(path) {
418 for name in additions {
419 if module.exports.insert(name) {
420 changed = true;
421 }
422 }
423 }
424 }
425 if !changed {
426 break;
427 }
428 }
429}
430
431impl ModuleGraph {
432 pub fn module_paths(&self) -> Vec<PathBuf> {
437 let mut paths: Vec<PathBuf> = self.modules.keys().cloned().collect();
438 paths.sort();
439 paths
440 }
441
442 pub fn contains_module(&self, path: &Path) -> bool {
445 self.modules.contains_key(path) || self.modules.contains_key(&normalize_path(path))
446 }
447
448 pub fn all_selective_import_names(&self) -> HashSet<&str> {
450 let mut names = HashSet::new();
451 for module in self.modules.values() {
452 for name in &module.selective_import_names {
453 names.insert(name.as_str());
454 }
455 }
456 names
457 }
458
459 pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
462 let target = normalize_path(target);
463 let mut out: Vec<PathBuf> = self
464 .modules
465 .iter()
466 .filter(|(_, info)| {
467 info.imports.iter().any(|import| {
468 import
469 .path
470 .as_ref()
471 .is_some_and(|p| normalize_path(p) == target)
472 })
473 })
474 .map(|(path, _)| path.clone())
475 .collect();
476 out.sort();
477 out
478 }
479
480 pub fn transitive_importers_of(&self, target: &Path) -> Vec<PathBuf> {
487 let target = normalize_path(target);
488 let mut visited = HashSet::from([target.clone()]);
489 let mut pending = vec![target];
490 let mut out = Vec::new();
491
492 while let Some(current) = pending.pop() {
493 for importer in self.importers_of(¤t) {
494 let importer = normalize_path(&importer);
495 if visited.insert(importer.clone()) {
496 pending.push(importer.clone());
497 out.push(importer);
498 }
499 }
500 }
501
502 out.sort();
503 out
504 }
505
506 pub fn imports_for_module(&self, file: &Path) -> Vec<ModuleImport> {
508 let file = normalize_path(file);
509 let Some(module) = self.modules.get(&file) else {
510 return Vec::new();
511 };
512 let mut imports: Vec<ModuleImport> = module
513 .imports
514 .iter()
515 .map(|import| {
516 let mut selective_names = import
517 .selective_names
518 .as_ref()
519 .map(|names| names.iter().cloned().collect::<Vec<_>>());
520 if let Some(names) = selective_names.as_mut() {
521 names.sort();
522 }
523 ModuleImport {
524 raw_path: import.raw_path.clone(),
525 resolved_path: import.path.as_ref().map(|path| normalize_path(path)),
526 selective_names,
527 namespace_alias: import.namespace_alias.clone(),
528 is_pub: import.is_pub,
529 }
530 })
531 .collect();
532 imports.sort_by(|left, right| {
533 left.raw_path
534 .cmp(&right.raw_path)
535 .then_with(|| left.selective_names.cmp(&right.selective_names))
536 .then_with(|| left.namespace_alias.cmp(&right.namespace_alias))
537 .then_with(|| left.resolved_path.cmp(&right.resolved_path))
538 });
539 imports
540 }
541
542 pub fn wildcard_exports_for(&self, file: &Path) -> WildcardResolution {
547 let file = normalize_path(file);
548 let Some(module) = self.modules.get(&file) else {
549 return WildcardResolution::Unknown;
550 };
551 if module.has_unresolved_wildcard_import {
552 return WildcardResolution::Unknown;
553 }
554
555 let mut names = HashSet::new();
556 for import in module
557 .imports
558 .iter()
559 .filter(|import| import.selective_names.is_none())
560 {
561 let Some(import_path) = &import.path else {
562 return WildcardResolution::Unknown;
563 };
564 let imported = self.modules.get(import_path).or_else(|| {
565 let normalized = normalize_path(import_path);
566 self.modules.get(&normalized)
567 });
568 let Some(_imported) = imported else {
569 return WildcardResolution::Unknown;
570 };
571 names.extend(self.exports_for_import(&file, import_path));
572 }
573 WildcardResolution::Resolved(names)
574 }
575
576 #[must_use]
597 pub fn import_compile_failures(&self, file: &Path) -> Vec<ImportCompileFailure> {
598 let file = normalize_path(file);
599 let Some(module) = self.modules.get(&file) else {
600 return Vec::new();
601 };
602 let mut failures = Vec::new();
603 for import in &module.imports {
604 let Some(import_path) = &import.path else {
605 continue;
606 };
607 let Some(target) = self
608 .modules
609 .get(import_path)
610 .or_else(|| self.modules.get(&normalize_path(import_path)))
611 else {
612 continue;
613 };
614 if let Some(error) = &target.load_error {
615 failures.push(ImportCompileFailure {
616 import_raw_path: import.raw_path.clone(),
617 import_span: import.import_span,
618 module_path: normalize_path(import_path),
619 error: error.clone(),
620 });
621 }
622 }
623 failures
624 }
625
626 pub fn imported_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
627 let file = normalize_path(file);
628 let module = self.modules.get(&file)?;
629 if module.has_unresolved_wildcard_import
630 || module.has_unresolved_selective_import
631 || module.has_unresolved_namespace_import
632 {
633 return None;
634 }
635
636 let mut names = HashSet::new();
637 for import in &module.imports {
638 if let Some(alias) = &import.namespace_alias {
640 names.insert(alias.clone());
641 continue;
642 }
643 let import_path = import.path.as_ref()?;
644 let imported = self
645 .modules
646 .get(import_path)
647 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
648 if imported.load_error.is_some() {
654 return None;
655 }
656 match &import.selective_names {
657 None => {
658 names.extend(self.exports_for_import(&file, import_path));
659 }
660 Some(selective) => {
661 for name in selective {
670 if imported.declarations.contains_key(name)
671 || imported.exports.contains(name)
672 {
673 names.insert(name.clone());
674 }
675 }
676 }
677 }
678 }
679 Some(names)
680 }
681
682 pub fn imported_names_by_kind_for_file(
687 &self,
688 file: &Path,
689 kind: DefKind,
690 ) -> Option<HashSet<String>> {
691 let file = normalize_path(file);
692 let module = self.modules.get(&file)?;
693 if module.has_unresolved_wildcard_import
694 || module.has_unresolved_selective_import
695 || module.has_unresolved_namespace_import
696 {
697 return None;
698 }
699
700 let mut names = HashSet::new();
701 for import in &module.imports {
702 if import.namespace_alias.is_some() {
704 continue;
705 }
706 let import_path = import.path.as_ref()?;
707 let imported_names: Vec<String> = match &import.selective_names {
708 Some(selective) => selective.iter().cloned().collect(),
709 None => self.exports_for_import(&file, import_path),
710 };
711 for name in imported_names {
712 if self.exported_kind_for_import(&file, import_path, &name) == Some(kind) {
713 names.insert(name);
714 }
715 }
716 }
717 Some(names)
718 }
719
720 pub fn imported_callable_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
725 let mut names = HashSet::new();
726 for kind in [
727 DefKind::Function,
728 DefKind::Pipeline,
729 DefKind::Tool,
730 DefKind::Struct,
731 ] {
732 names.extend(self.imported_names_by_kind_for_file(file, kind)?);
733 }
734 Some(names)
735 }
736
737 pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
741 let file = normalize_path(file);
742 let module = self.modules.get(&file)?;
743 if module.has_unresolved_wildcard_import
744 || module.has_unresolved_selective_import
745 || module.has_unresolved_namespace_import
746 {
747 return None;
748 }
749
750 let mut decls = Vec::new();
751 let mut seen = HashSet::new();
752 for import in &module.imports {
753 if import.namespace_alias.is_some() {
755 continue;
756 }
757 let import_path = import.path.as_ref()?;
758 let imported = self
759 .modules
760 .get(import_path)
761 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
762 if imported.load_error.is_some() {
768 return None;
769 }
770 let mut names_to_collect: Vec<String> = match &import.selective_names {
771 None => imported.exports.iter().cloned().collect(),
772 Some(selective) => selective.iter().cloned().collect(),
773 };
774 names_to_collect.sort();
775 for name in &names_to_collect {
776 let mut visited = HashSet::new();
777 if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
778 let origin = self
779 .export_definition_of(import_path, name)
780 .map_or_else(|| import_path.clone(), |definition| definition.file);
781 self.extend_type_dependency(&origin, &decl, &mut decls, &mut seen);
782 }
783 }
784 for ty_decl in &imported.type_declarations {
794 if type_decl_name(ty_decl).is_some() {
795 self.extend_type_dependency(import_path, ty_decl, &mut decls, &mut seen);
796 }
797 }
798
799 for name in &names_to_collect {
800 let mut visited = HashSet::new();
801 let Some(callable) =
802 self.find_exported_callable_decl(import_path, name, &mut visited)
803 else {
804 continue;
805 };
806 let origin = self
807 .export_definition_of(import_path, name)
808 .map_or_else(|| import_path.clone(), |definition| definition.file);
809 self.extend_callable_type_dependencies(&origin, &callable, &mut decls, &mut seen);
810 }
811 }
812 Some(decls)
813 }
814
815 pub fn imported_callable_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
817 let file = normalize_path(file);
818 let module = self.modules.get(&file)?;
819 if module.has_unresolved_wildcard_import
820 || module.has_unresolved_selective_import
821 || module.has_unresolved_namespace_import
822 {
823 return None;
824 }
825
826 let mut decls = Vec::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 selective_import = import.selective_names.is_some();
846 let names_to_collect: Vec<String> = match &import.selective_names {
847 None => self.exports_for_import(&file, import_path),
848 Some(selective) => selective.iter().cloned().collect(),
849 };
850 for name in &names_to_collect {
851 if selective_import
852 || imported.own_exports.contains(name)
853 || (sibling_module_access(&file, import_path)
854 && imported.sibling_exports.contains(name))
855 {
856 if let Some(decl) = imported
857 .callable_declarations
858 .iter()
859 .find(|decl| callable_decl_name(decl) == Some(name.as_str()))
860 {
861 decls.push(decl.clone());
862 continue;
863 }
864 }
865 let mut visited = HashSet::new();
866 if let Some(decl) =
867 self.find_exported_callable_decl(import_path, name, &mut visited)
868 {
869 decls.push(decl);
870 }
871 }
872 }
873 Some(decls)
874 }
875
876 fn find_exported_type_decl(
879 &self,
880 path: &Path,
881 name: &str,
882 visited: &mut HashSet<PathBuf>,
883 ) -> Option<SNode> {
884 let canonical = normalize_path(path);
885 if !visited.insert(canonical.clone()) {
886 return None;
887 }
888 let module = self
889 .modules
890 .get(&canonical)
891 .or_else(|| self.modules.get(path))?;
892 for decl in &module.type_declarations {
893 if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
894 return Some(decl.clone());
895 }
896 }
897 if let Some(sources) = module.selective_re_exports.get(name) {
898 for source in sources {
899 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
900 return Some(decl);
901 }
902 }
903 }
904 for source in &module.wildcard_re_export_paths {
905 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
906 return Some(decl);
907 }
908 }
909 None
910 }
911
912 fn find_exported_callable_decl(
913 &self,
914 path: &Path,
915 name: &str,
916 visited: &mut HashSet<PathBuf>,
917 ) -> Option<SNode> {
918 let canonical = normalize_path(path);
919 if !visited.insert(canonical.clone()) {
920 return None;
921 }
922 let module = self
923 .modules
924 .get(&canonical)
925 .or_else(|| self.modules.get(path))?;
926 for decl in &module.callable_declarations {
927 if callable_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
928 return Some(decl.clone());
929 }
930 }
931 if let Some(sources) = module.selective_re_exports.get(name) {
932 for source in sources {
933 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
934 return Some(decl);
935 }
936 }
937 }
938 for source in &module.wildcard_re_export_paths {
939 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
940 return Some(decl);
941 }
942 }
943 None
944 }
945
946 pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
952 let mut visited = HashSet::new();
953 self.definition_of_inner(file, name, &mut visited)
954 }
955
956 pub fn export_definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
963 let mut visited = HashSet::new();
964 self.export_definition_of_inner(file, name, &mut visited)
965 }
966
967 pub fn declared_names_for_file(&self, file: &Path) -> Option<Vec<&str>> {
971 let module = self.modules.get(&normalize_path(file))?;
972 let mut names: Vec<&str> = module.declarations.keys().map(String::as_str).collect();
973 names.sort_unstable();
974 Some(names)
975 }
976
977 fn definition_of_inner(
978 &self,
979 file: &Path,
980 name: &str,
981 visited: &mut HashSet<PathBuf>,
982 ) -> Option<DefSite> {
983 let file = normalize_path(file);
984 if !visited.insert(file.clone()) {
985 return None;
986 }
987 let current = self.modules.get(&file)?;
988
989 if let Some(local) = current.declarations.get(name) {
990 return Some(local.clone());
991 }
992
993 if let Some(sources) = current.selective_re_exports.get(name) {
998 for source in sources {
999 if let Some(def) = self.definition_of_inner(source, name, visited) {
1000 return Some(def);
1001 }
1002 }
1003 }
1004
1005 for source in ¤t.wildcard_re_export_paths {
1007 if let Some(def) = self.definition_of_inner(source, name, visited) {
1008 return Some(def);
1009 }
1010 }
1011
1012 for import in ¤t.imports {
1014 let Some(selective_names) = &import.selective_names else {
1015 continue;
1016 };
1017 if !selective_names.contains(name) {
1018 continue;
1019 }
1020 if let Some(path) = &import.path {
1021 if let Some(def) = self.definition_of_inner(path, name, visited) {
1022 return Some(def);
1023 }
1024 }
1025 }
1026
1027 for import in ¤t.imports {
1029 if import.selective_names.is_some() || import.namespace_alias.is_some() {
1030 continue;
1031 }
1032 if let Some(path) = &import.path {
1033 if let Some(def) = self.definition_of_inner(path, name, visited) {
1034 return Some(def);
1035 }
1036 }
1037 }
1038
1039 None
1040 }
1041
1042 fn export_definition_of_inner(
1043 &self,
1044 file: &Path,
1045 name: &str,
1046 visited: &mut HashSet<PathBuf>,
1047 ) -> Option<DefSite> {
1048 let file = normalize_path(file);
1049 if !visited.insert(file.clone()) {
1050 return None;
1051 }
1052 let current = self.modules.get(&file)?;
1053
1054 if current.own_exports.contains(name) {
1055 if let Some(local) = current.declarations.get(name) {
1056 return Some(local.clone());
1057 }
1058 }
1059 if let Some(sources) = current.selective_re_exports.get(name) {
1060 for source in sources {
1061 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1062 return Some(definition);
1063 }
1064 }
1065 }
1066 for source in ¤t.wildcard_re_export_paths {
1067 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1068 return Some(definition);
1069 }
1070 }
1071 None
1072 }
1073
1074 pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
1078 let file = normalize_path(file);
1079 let Some(module) = self.modules.get(&file) else {
1080 return Vec::new();
1081 };
1082
1083 let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
1087
1088 for (name, srcs) in &module.selective_re_exports {
1089 sources
1090 .entry(name.clone())
1091 .or_default()
1092 .extend(srcs.iter().cloned());
1093 }
1094 for src in &module.wildcard_re_export_paths {
1095 let canonical = normalize_path(src);
1096 let Some(src_module) = self
1097 .modules
1098 .get(&canonical)
1099 .or_else(|| self.modules.get(src))
1100 else {
1101 continue;
1102 };
1103 for name in &src_module.exports {
1104 sources
1105 .entry(name.clone())
1106 .or_default()
1107 .push(canonical.clone());
1108 }
1109 }
1110
1111 for name in &module.own_exports {
1115 if let Some(entry) = sources.get_mut(name) {
1116 entry.push(file.clone());
1117 }
1118 }
1119
1120 let mut conflicts = Vec::new();
1121 for (name, mut srcs) in sources {
1122 srcs.sort();
1123 srcs.dedup();
1124 if srcs.len() > 1 {
1125 conflicts.push(ReExportConflict {
1126 name,
1127 sources: srcs,
1128 });
1129 }
1130 }
1131 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
1132 conflicts
1133 }
1134
1135 pub fn selective_import_issues(&self, file: &Path) -> Vec<SelectiveImportIssue> {
1141 let file = normalize_path(file);
1142 let Some(module) = self.modules.get(&file) else {
1143 return Vec::new();
1144 };
1145
1146 let mut out = Vec::new();
1147 for import in &module.imports {
1148 let Some(selective) = &import.selective_names else {
1149 continue;
1150 };
1151 let Some(import_path) = &import.path else {
1152 continue;
1153 };
1154 let Some(target) = self
1155 .modules
1156 .get(import_path)
1157 .or_else(|| self.modules.get(&normalize_path(import_path)))
1158 else {
1159 continue;
1160 };
1161 if target.load_error.is_some() {
1162 continue;
1163 }
1164 let visible: HashSet<_> = self
1165 .exports_for_import(&file, import_path)
1166 .into_iter()
1167 .collect();
1168 for name in selective {
1169 let can_bind = visible.contains(name);
1170 let can_publish = !import.is_pub || target.exports.contains(name);
1171 let kind = if can_bind && can_publish {
1172 continue;
1173 } else if target.declarations.contains_key(name) {
1174 SelectiveImportIssueKind::Private
1175 } else {
1176 SelectiveImportIssueKind::Missing
1177 };
1178 out.push(SelectiveImportIssue {
1179 name: name.clone(),
1180 module: import.raw_path.clone(),
1181 span: import.import_span,
1182 kind,
1183 });
1184 }
1185 }
1186 out.sort_by(|a, b| (&a.name, &a.module, a.kind).cmp(&(&b.name, &b.module, b.kind)));
1187 out.dedup();
1188 out
1189 }
1190
1191 pub fn exported_kind(&self, file: &Path, name: &str) -> Option<DefKind> {
1195 self.exported_kind_inner(file, name, &mut HashSet::new())
1196 }
1197
1198 fn exported_kind_inner(
1199 &self,
1200 file: &Path,
1201 name: &str,
1202 visited: &mut HashSet<PathBuf>,
1203 ) -> Option<DefKind> {
1204 let file = normalize_path(file);
1205 if !visited.insert(file.clone()) {
1206 return None;
1207 }
1208 let result = self.modules.get(&file).and_then(|module| {
1209 if module.own_exports.contains(name) {
1210 return module
1211 .declarations
1212 .get(name)
1213 .map(|definition| definition.kind)
1214 .or_else(|| {
1215 stdlib_module_name(&file).and_then(|stdlib_module| {
1216 stdlib::builtin_reexports(stdlib_module)
1217 .contains(&name)
1218 .then_some(DefKind::Function)
1219 })
1220 });
1221 }
1222 if let Some(sources) = module.selective_re_exports.get(name) {
1223 for source in sources {
1224 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1225 return Some(kind);
1226 }
1227 }
1228 }
1229 for source in &module.wildcard_re_export_paths {
1230 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1231 return Some(kind);
1232 }
1233 }
1234 None
1235 });
1236 visited.remove(&file);
1237 result
1238 }
1239}
1240
1241#[derive(Debug, Clone, PartialEq, Eq)]
1244pub struct ReExportConflict {
1245 pub name: String,
1246 pub sources: Vec<PathBuf>,
1247}
1248
1249#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1251pub enum SelectiveImportIssueKind {
1252 Missing,
1254 Private,
1256}
1257
1258#[derive(Debug, Clone, PartialEq, Eq)]
1260pub struct SelectiveImportIssue {
1261 pub name: String,
1263 pub module: String,
1265 pub span: Span,
1267 pub kind: SelectiveImportIssueKind,
1269}
1270
1271impl SelectiveImportIssue {
1272 #[must_use]
1274 pub fn message(&self) -> String {
1275 match self.kind {
1276 SelectiveImportIssueKind::Missing => format!(
1277 "imported symbol `{}` does not exist in `{}`",
1278 self.name, self.module
1279 ),
1280 SelectiveImportIssueKind::Private => format!(
1281 "imported symbol `{}` is not exported by `{}` — it is defined there but not `pub`",
1282 self.name, self.module
1283 ),
1284 }
1285 }
1286
1287 #[must_use]
1289 pub fn help(&self) -> String {
1290 match self.kind {
1291 SelectiveImportIssueKind::Missing => format!(
1292 "update the import to a symbol exported by `{}`",
1293 self.module
1294 ),
1295 SelectiveImportIssueKind::Private => {
1296 format!(
1297 "mark `{}` as `pub` in `{}` to export it",
1298 self.name, self.module
1299 )
1300 }
1301 }
1302 }
1303}
1304
1305fn load_module(
1306 path: &Path,
1307 package_snapshots: &[PackageSnapshot],
1308 source_overrides: Option<&HashMap<PathBuf, String>>,
1309 retain_parsed_source: bool,
1310 observer: Option<&ModuleObserver<'_>>,
1311) -> (ModuleInfo, Option<ParsedModuleSource>) {
1312 let source = source_overrides
1313 .and_then(|overrides| overrides.get(&normalize_path(path)).cloned())
1314 .or_else(|| read_module_source(path));
1315 let Some(source) = source else {
1316 return (ModuleInfo::default(), None);
1317 };
1318 let mut lexer = harn_lexer::Lexer::new(&source);
1319 let tokens = match lexer.tokenize() {
1320 Ok(tokens) => tokens,
1321 Err(error) => {
1322 if let Some(observer) = observer {
1323 observer(path, &source, None);
1324 }
1325 let module = ModuleInfo {
1326 load_error: Some(ModuleLoadError {
1327 message: error.to_string(),
1328 span: error.span(),
1329 }),
1330 ..ModuleInfo::default()
1331 };
1332 return (module, None);
1333 }
1334 };
1335 let mut parser = Parser::new(tokens);
1336 let program = match parser.parse() {
1337 Ok(program) => program,
1338 Err(error) => {
1339 if let Some(observer) = observer {
1340 observer(path, &source, None);
1341 }
1342 let module = ModuleInfo {
1343 load_error: Some(ModuleLoadError {
1344 message: error.to_string(),
1345 span: error.span(),
1346 }),
1347 ..ModuleInfo::default()
1348 };
1349 return (module, None);
1350 }
1351 };
1352
1353 if let Some(observer) = observer {
1354 observer(path, &source, Some(&program));
1355 }
1356 let mut module = ModuleInfo::default();
1357 for node in &program {
1358 collect_module_info(path, node, &mut module, package_snapshots);
1359 collect_type_declarations(node, &mut module.type_declarations);
1360 collect_callable_declarations(node, &mut module.callable_declarations);
1361 }
1362 if let Some(stdlib_module) = stdlib_module_name(path) {
1363 module.own_exports.extend(
1364 stdlib::builtin_reexports(stdlib_module)
1365 .iter()
1366 .map(|name| (*name).to_string()),
1367 );
1368 }
1369 module.exports.extend(module.own_exports.iter().cloned());
1373 module
1374 .exports
1375 .extend(module.selective_re_exports.keys().cloned());
1376 let parsed = retain_parsed_source.then_some(ParsedModuleSource { source, program });
1377 (module, parsed)
1378}
1379
1380pub fn stdlib_module_name(path: &Path) -> Option<&str> {
1383 let s = path.to_str()?;
1384 s.strip_prefix("<std>/")
1385}
1386
1387fn normalize_path(path: &Path) -> PathBuf {
1388 canonical_path(path)
1389}
1390
1391pub fn canonical_path(path: &Path) -> PathBuf {
1404 use std::sync::OnceLock;
1405 if let Some(virtual_path) = stdlib::canonical_virtual_path(path) {
1406 return virtual_path;
1407 }
1408 static MEMO: OnceLock<std::sync::RwLock<HashMap<PathBuf, PathBuf>>> = OnceLock::new();
1411 let memo = MEMO.get_or_init(|| std::sync::RwLock::new(HashMap::new()));
1412 if let Some(hit) = memo
1413 .read()
1414 .expect("canonical path memo lock poisoned")
1415 .get(path)
1416 .cloned()
1417 {
1418 return hit;
1419 }
1420 match path.canonicalize() {
1421 Ok(canonical) => {
1422 memo.write()
1423 .expect("canonical path memo lock poisoned")
1424 .insert(path.to_path_buf(), canonical.clone());
1425 canonical
1426 }
1427 Err(_) => manifest_walk::normalize_lexically(path),
1430 }
1431}
1432
1433#[cfg(test)]
1434#[path = "tests.rs"]
1435mod tests;