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