1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4use crate::package_imports::{acquire_package_snapshots, resolve_import_path_with_snapshots};
5use crate::package_snapshot::PackageSnapshot;
6use harn_lexer::Span;
7use harn_parser::{BindingPattern, Node, Parser, SNode};
8
9pub mod asset_paths;
10pub mod fingerprint;
11pub mod package_execution;
12mod package_imports;
13pub mod package_snapshot;
14pub mod personas;
15pub mod project_config;
16mod stdlib;
17
18pub use package_imports::{
19 resolve_import_path, resolve_import_path_with_guard, resolve_import_path_with_snapshot,
20};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum DefKind {
25 Function,
26 Pipeline,
27 Tool,
28 Skill,
29 Struct,
30 Enum,
31 Interface,
32 Type,
33 Variable,
34 Parameter,
35}
36
37#[derive(Debug, Clone)]
39pub struct DefSite {
40 pub name: String,
41 pub file: PathBuf,
42 pub kind: DefKind,
43 pub span: Span,
44}
45
46#[derive(Debug, Clone)]
48pub enum WildcardResolution {
49 Resolved(HashSet<String>),
51 Unknown,
53}
54
55#[derive(Debug, Default)]
57pub struct ModuleGraph {
58 modules: HashMap<PathBuf, ModuleInfo>,
59 _package_snapshots: Vec<PackageSnapshot>,
61}
62
63#[derive(Debug, Clone)]
64pub struct ParsedModuleSource {
65 pub source: String,
66 pub program: Vec<SNode>,
67}
68
69#[derive(Debug, Default)]
70pub struct ModuleGraphBuild {
71 pub graph: ModuleGraph,
72 pub parsed_sources: HashMap<PathBuf, ParsedModuleSource>,
73}
74
75#[derive(Debug, Default)]
76struct ModuleInfo {
77 declarations: HashMap<String, DefSite>,
80 exports: HashSet<String>,
85 own_exports: HashSet<String>,
88 selective_re_exports: HashMap<String, Vec<PathBuf>>,
95 wildcard_re_export_paths: Vec<PathBuf>,
99 selective_import_names: HashSet<String>,
101 imports: Vec<ImportRef>,
103 has_unresolved_wildcard_import: bool,
105 has_unresolved_selective_import: bool,
109 type_declarations: Vec<SNode>,
112 callable_declarations: Vec<SNode>,
115 load_error: Option<ModuleLoadError>,
121}
122
123#[derive(Debug, Clone)]
129pub struct ModuleLoadError {
130 pub message: String,
132 pub span: Span,
134}
135
136#[derive(Debug, Clone)]
139pub struct ImportCompileFailure {
140 pub import_raw_path: String,
142 pub import_span: Span,
144 pub module_path: PathBuf,
146 pub error: ModuleLoadError,
148}
149
150#[derive(Debug, Clone)]
151struct ImportRef {
152 raw_path: String,
153 path: Option<PathBuf>,
154 selective_names: Option<HashSet<String>>,
155 import_span: Span,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct ModuleImport {
161 pub raw_path: String,
163 pub resolved_path: Option<PathBuf>,
165 pub selective_names: Option<Vec<String>>,
167}
168
169pub fn read_module_source(path: &Path) -> Option<String> {
175 if let Some(stdlib_module) = stdlib_module_from_path(path) {
176 return stdlib::get_stdlib_source(stdlib_module).map(ToString::to_string);
177 }
178 std::fs::read_to_string(path).ok()
179}
180
181pub fn build(files: &[PathBuf]) -> ModuleGraph {
187 build_inner(files, None, None).graph
188}
189
190pub fn build_with_source(file: &Path, source: &str) -> ModuleGraph {
196 let file = normalize_path(file);
197 let source_overrides = HashMap::from([(file.clone(), source.to_string())]);
198 build_inner(&[file], None, Some(&source_overrides)).graph
199}
200
201pub fn build_with_parsed_sources(files: &[PathBuf]) -> ModuleGraphBuild {
207 let parsed_source_targets = files.iter().map(|file| normalize_path(file)).collect();
208 build_inner(files, Some(&parsed_source_targets), None)
209}
210
211fn build_inner(
212 files: &[PathBuf],
213 parsed_source_targets: Option<&HashSet<PathBuf>>,
214 source_overrides: Option<&HashMap<PathBuf, String>>,
215) -> ModuleGraphBuild {
216 let package_snapshots = acquire_package_snapshots(files);
217 let mut modules: HashMap<PathBuf, ModuleInfo> = HashMap::new();
218 let mut parsed_sources: HashMap<PathBuf, ParsedModuleSource> = HashMap::new();
219 let mut seen: HashSet<PathBuf> = HashSet::new();
220 let mut wave: Vec<PathBuf> = Vec::new();
221 for file in files {
222 let canonical = normalize_path(file);
223 if seen.insert(canonical.clone()) {
224 wave.push(canonical);
225 }
226 }
227 while !wave.is_empty() {
235 let loaded = load_wave(&wave, &package_snapshots, source_overrides);
236 let mut next_wave: Vec<PathBuf> = Vec::new();
237 for (path, (module, parsed)) in wave.drain(..).zip(loaded) {
238 let retain_parsed_source =
239 parsed_source_targets.is_some_and(|targets| targets.contains(&path));
240 if retain_parsed_source {
241 if let Some(parsed) = parsed {
242 parsed_sources.insert(path.clone(), parsed);
243 }
244 }
245 for import in &module.imports {
262 if let Some(import_path) = &import.path {
263 let canonical = normalize_path(import_path);
264 if seen.insert(canonical.clone()) {
265 next_wave.push(canonical);
266 }
267 }
268 }
269 modules.insert(path, module);
270 }
271 wave = next_wave;
272 }
273 resolve_re_exports(&mut modules);
274 ModuleGraphBuild {
275 graph: ModuleGraph {
276 modules,
277 _package_snapshots: package_snapshots,
278 },
279 parsed_sources,
280 }
281}
282
283pub const MODULE_GRAPH_JOBS_ENV: &str = "HARN_MODULE_GRAPH_JOBS";
286
287fn load_wave(
290 paths: &[PathBuf],
291 package_snapshots: &[PackageSnapshot],
292 source_overrides: Option<&HashMap<PathBuf, String>>,
293) -> Vec<(ModuleInfo, Option<ParsedModuleSource>)> {
294 const MIN_PARALLEL_WAVE: usize = 8;
295 let configured = std::env::var(MODULE_GRAPH_JOBS_ENV)
296 .ok()
297 .and_then(|value| value.parse::<usize>().ok())
298 .filter(|&jobs| jobs > 0);
299 let workers = configured
300 .unwrap_or_else(|| {
301 std::thread::available_parallelism()
302 .map(std::num::NonZeroUsize::get)
303 .unwrap_or(1)
304 })
305 .min(paths.len());
306 if workers <= 1 || paths.len() < MIN_PARALLEL_WAVE {
307 return paths
308 .iter()
309 .map(|path| load_module(path, package_snapshots, source_overrides))
310 .collect();
311 }
312 let next = std::sync::atomic::AtomicUsize::new(0);
313 let mut produced: Vec<(usize, (ModuleInfo, Option<ParsedModuleSource>))> =
314 std::thread::scope(|scope| {
315 let handles: Vec<_> = (0..workers)
316 .map(|_| {
317 scope.spawn(|| {
318 let mut local = Vec::new();
319 loop {
320 let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
321 let Some(path) = paths.get(index) else {
322 break;
323 };
324 local.push((
325 index,
326 load_module(path, package_snapshots, source_overrides),
327 ));
328 }
329 local
330 })
331 })
332 .collect();
333 handles
334 .into_iter()
335 .flat_map(|handle| match handle.join() {
336 Ok(local) => local,
337 Err(panic) => std::panic::resume_unwind(panic),
338 })
339 .collect()
340 });
341 produced.sort_unstable_by_key(|(index, _)| *index);
342 produced.into_iter().map(|(_, loaded)| loaded).collect()
343}
344
345fn resolve_re_exports(modules: &mut HashMap<PathBuf, ModuleInfo>) {
350 let keys: Vec<PathBuf> = modules.keys().cloned().collect();
351 loop {
352 let mut changed = false;
353 for path in &keys {
354 let wildcard_paths = modules
357 .get(path)
358 .map(|m| m.wildcard_re_export_paths.clone())
359 .unwrap_or_default();
360 if wildcard_paths.is_empty() {
361 continue;
362 }
363 let mut additions: Vec<String> = Vec::new();
364 for src in &wildcard_paths {
365 let src_canonical = normalize_path(src);
366 if let Some(src_module) = modules.get(src).or_else(|| modules.get(&src_canonical)) {
367 additions.extend(src_module.exports.iter().cloned());
368 }
369 }
370 if let Some(module) = modules.get_mut(path) {
371 for name in additions {
372 if module.exports.insert(name) {
373 changed = true;
374 }
375 }
376 }
377 }
378 if !changed {
379 break;
380 }
381 }
382}
383
384impl ModuleGraph {
385 pub fn module_paths(&self) -> Vec<PathBuf> {
390 let mut paths: Vec<PathBuf> = self.modules.keys().cloned().collect();
391 paths.sort();
392 paths
393 }
394
395 pub fn contains_module(&self, path: &Path) -> bool {
398 self.modules.contains_key(path) || self.modules.contains_key(&normalize_path(path))
399 }
400
401 pub fn all_selective_import_names(&self) -> HashSet<&str> {
403 let mut names = HashSet::new();
404 for module in self.modules.values() {
405 for name in &module.selective_import_names {
406 names.insert(name.as_str());
407 }
408 }
409 names
410 }
411
412 pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
415 let target = normalize_path(target);
416 let mut out: Vec<PathBuf> = self
417 .modules
418 .iter()
419 .filter(|(_, info)| {
420 info.imports.iter().any(|import| {
421 import
422 .path
423 .as_ref()
424 .is_some_and(|p| normalize_path(p) == target)
425 })
426 })
427 .map(|(path, _)| path.clone())
428 .collect();
429 out.sort();
430 out
431 }
432
433 pub fn imports_for_module(&self, file: &Path) -> Vec<ModuleImport> {
435 let file = normalize_path(file);
436 let Some(module) = self.modules.get(&file) else {
437 return Vec::new();
438 };
439 let mut imports: Vec<ModuleImport> = module
440 .imports
441 .iter()
442 .map(|import| {
443 let mut selective_names = import
444 .selective_names
445 .as_ref()
446 .map(|names| names.iter().cloned().collect::<Vec<_>>());
447 if let Some(names) = selective_names.as_mut() {
448 names.sort();
449 }
450 ModuleImport {
451 raw_path: import.raw_path.clone(),
452 resolved_path: import.path.as_ref().map(|path| normalize_path(path)),
453 selective_names,
454 }
455 })
456 .collect();
457 imports.sort_by(|left, right| {
458 left.raw_path
459 .cmp(&right.raw_path)
460 .then_with(|| left.selective_names.cmp(&right.selective_names))
461 .then_with(|| left.resolved_path.cmp(&right.resolved_path))
462 });
463 imports
464 }
465
466 pub fn exports_for_module(&self, file: &Path) -> Vec<String> {
468 let file = normalize_path(file);
469 let Some(module) = self.modules.get(&file) else {
470 return Vec::new();
471 };
472 let mut exports: Vec<String> = module.exports.iter().cloned().collect();
473 exports.sort();
474 exports
475 }
476
477 pub fn wildcard_exports_for(&self, file: &Path) -> WildcardResolution {
482 let file = normalize_path(file);
483 let Some(module) = self.modules.get(&file) else {
484 return WildcardResolution::Unknown;
485 };
486 if module.has_unresolved_wildcard_import {
487 return WildcardResolution::Unknown;
488 }
489
490 let mut names = HashSet::new();
491 for import in module
492 .imports
493 .iter()
494 .filter(|import| import.selective_names.is_none())
495 {
496 let Some(import_path) = &import.path else {
497 return WildcardResolution::Unknown;
498 };
499 let imported = self.modules.get(import_path).or_else(|| {
500 let normalized = normalize_path(import_path);
501 self.modules.get(&normalized)
502 });
503 let Some(imported) = imported else {
504 return WildcardResolution::Unknown;
505 };
506 names.extend(imported.exports.iter().cloned());
507 }
508 WildcardResolution::Resolved(names)
509 }
510
511 #[must_use]
532 pub fn import_compile_failures(&self, file: &Path) -> Vec<ImportCompileFailure> {
533 let file = normalize_path(file);
534 let Some(module) = self.modules.get(&file) else {
535 return Vec::new();
536 };
537 let mut failures = Vec::new();
538 for import in &module.imports {
539 let Some(import_path) = &import.path else {
540 continue;
541 };
542 let Some(target) = self
543 .modules
544 .get(import_path)
545 .or_else(|| self.modules.get(&normalize_path(import_path)))
546 else {
547 continue;
548 };
549 if let Some(error) = &target.load_error {
550 failures.push(ImportCompileFailure {
551 import_raw_path: import.raw_path.clone(),
552 import_span: import.import_span,
553 module_path: normalize_path(import_path),
554 error: error.clone(),
555 });
556 }
557 }
558 failures
559 }
560
561 pub fn imported_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
562 let file = normalize_path(file);
563 let module = self.modules.get(&file)?;
564 if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
565 return None;
566 }
567
568 let mut names = HashSet::new();
569 for import in &module.imports {
570 let import_path = import.path.as_ref()?;
571 let imported = self
572 .modules
573 .get(import_path)
574 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
575 if imported.load_error.is_some() {
581 return None;
582 }
583 match &import.selective_names {
584 None => {
585 names.extend(imported.exports.iter().cloned());
586 }
587 Some(selective) => {
588 for name in selective {
597 if imported.declarations.contains_key(name)
598 || imported.exports.contains(name)
599 {
600 names.insert(name.clone());
601 }
602 }
603 }
604 }
605 }
606 Some(names)
607 }
608
609 pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
613 let file = normalize_path(file);
614 let module = self.modules.get(&file)?;
615 if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
616 return None;
617 }
618
619 let mut decls = Vec::new();
620 for import in &module.imports {
621 let import_path = import.path.as_ref()?;
622 let imported = self
623 .modules
624 .get(import_path)
625 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
626 if imported.load_error.is_some() {
632 return None;
633 }
634 let names_to_collect: Vec<String> = match &import.selective_names {
635 None => imported.exports.iter().cloned().collect(),
636 Some(selective) => selective.iter().cloned().collect(),
637 };
638 for name in &names_to_collect {
639 let mut visited = HashSet::new();
640 if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
641 decls.push(decl);
642 }
643 }
644 for ty_decl in &imported.type_declarations {
654 if type_decl_name(ty_decl).is_some() {
655 decls.push(ty_decl.clone());
656 }
657 }
658 }
659 Some(decls)
660 }
661
662 pub fn imported_callable_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
666 let file = normalize_path(file);
667 let module = self.modules.get(&file)?;
668 if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
669 return None;
670 }
671
672 let mut decls = Vec::new();
673 for import in &module.imports {
674 let import_path = import.path.as_ref()?;
675 let imported = self
676 .modules
677 .get(import_path)
678 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
679 if imported.load_error.is_some() {
685 return None;
686 }
687 let selective_import = import.selective_names.is_some();
688 let names_to_collect: Vec<String> = match &import.selective_names {
689 None => imported.exports.iter().cloned().collect(),
690 Some(selective) => selective.iter().cloned().collect(),
691 };
692 for name in &names_to_collect {
693 if selective_import || imported.own_exports.contains(name) {
694 if let Some(decl) = imported
695 .callable_declarations
696 .iter()
697 .find(|decl| callable_decl_name(decl) == Some(name.as_str()))
698 {
699 decls.push(decl.clone());
700 continue;
701 }
702 }
703 let mut visited = HashSet::new();
704 if let Some(decl) =
705 self.find_exported_callable_decl(import_path, name, &mut visited)
706 {
707 decls.push(decl);
708 }
709 }
710 }
711 Some(decls)
712 }
713
714 fn find_exported_type_decl(
717 &self,
718 path: &Path,
719 name: &str,
720 visited: &mut HashSet<PathBuf>,
721 ) -> Option<SNode> {
722 let canonical = normalize_path(path);
723 if !visited.insert(canonical.clone()) {
724 return None;
725 }
726 let module = self
727 .modules
728 .get(&canonical)
729 .or_else(|| self.modules.get(path))?;
730 for decl in &module.type_declarations {
731 if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
732 return Some(decl.clone());
733 }
734 }
735 if let Some(sources) = module.selective_re_exports.get(name) {
736 for source in sources {
737 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
738 return Some(decl);
739 }
740 }
741 }
742 for source in &module.wildcard_re_export_paths {
743 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
744 return Some(decl);
745 }
746 }
747 None
748 }
749
750 fn find_exported_callable_decl(
751 &self,
752 path: &Path,
753 name: &str,
754 visited: &mut HashSet<PathBuf>,
755 ) -> Option<SNode> {
756 let canonical = normalize_path(path);
757 if !visited.insert(canonical.clone()) {
758 return None;
759 }
760 let module = self
761 .modules
762 .get(&canonical)
763 .or_else(|| self.modules.get(path))?;
764 for decl in &module.callable_declarations {
765 if callable_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
766 return Some(decl.clone());
767 }
768 }
769 if let Some(sources) = module.selective_re_exports.get(name) {
770 for source in sources {
771 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
772 return Some(decl);
773 }
774 }
775 }
776 for source in &module.wildcard_re_export_paths {
777 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
778 return Some(decl);
779 }
780 }
781 None
782 }
783
784 pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
790 let mut visited = HashSet::new();
791 self.definition_of_inner(file, name, &mut visited)
792 }
793
794 pub fn declared_names_for_file(&self, file: &Path) -> Option<Vec<&str>> {
798 let module = self.modules.get(&normalize_path(file))?;
799 let mut names: Vec<&str> = module.declarations.keys().map(String::as_str).collect();
800 names.sort_unstable();
801 Some(names)
802 }
803
804 fn definition_of_inner(
805 &self,
806 file: &Path,
807 name: &str,
808 visited: &mut HashSet<PathBuf>,
809 ) -> Option<DefSite> {
810 let file = normalize_path(file);
811 if !visited.insert(file.clone()) {
812 return None;
813 }
814 let current = self.modules.get(&file)?;
815
816 if let Some(local) = current.declarations.get(name) {
817 return Some(local.clone());
818 }
819
820 if let Some(sources) = current.selective_re_exports.get(name) {
825 for source in sources {
826 if let Some(def) = self.definition_of_inner(source, name, visited) {
827 return Some(def);
828 }
829 }
830 }
831
832 for source in ¤t.wildcard_re_export_paths {
834 if let Some(def) = self.definition_of_inner(source, name, visited) {
835 return Some(def);
836 }
837 }
838
839 for import in ¤t.imports {
841 let Some(selective_names) = &import.selective_names else {
842 continue;
843 };
844 if !selective_names.contains(name) {
845 continue;
846 }
847 if let Some(path) = &import.path {
848 if let Some(def) = self.definition_of_inner(path, name, visited) {
849 return Some(def);
850 }
851 }
852 }
853
854 for import in ¤t.imports {
856 if import.selective_names.is_some() {
857 continue;
858 }
859 if let Some(path) = &import.path {
860 if let Some(def) = self.definition_of_inner(path, name, visited) {
861 return Some(def);
862 }
863 }
864 }
865
866 None
867 }
868
869 pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
873 let file = normalize_path(file);
874 let Some(module) = self.modules.get(&file) else {
875 return Vec::new();
876 };
877
878 let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
882
883 for (name, srcs) in &module.selective_re_exports {
884 sources
885 .entry(name.clone())
886 .or_default()
887 .extend(srcs.iter().cloned());
888 }
889 for src in &module.wildcard_re_export_paths {
890 let canonical = normalize_path(src);
891 let Some(src_module) = self
892 .modules
893 .get(&canonical)
894 .or_else(|| self.modules.get(src))
895 else {
896 continue;
897 };
898 for name in &src_module.exports {
899 sources
900 .entry(name.clone())
901 .or_default()
902 .push(canonical.clone());
903 }
904 }
905
906 for name in &module.own_exports {
910 if let Some(entry) = sources.get_mut(name) {
911 entry.push(file.clone());
912 }
913 }
914
915 let mut conflicts = Vec::new();
916 for (name, mut srcs) in sources {
917 srcs.sort();
918 srcs.dedup();
919 if srcs.len() > 1 {
920 conflicts.push(ReExportConflict {
921 name,
922 sources: srcs,
923 });
924 }
925 }
926 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
927 conflicts
928 }
929
930 pub fn selective_import_issues(&self, file: &Path) -> Vec<SelectiveImportIssue> {
936 let file = normalize_path(file);
937 let Some(module) = self.modules.get(&file) else {
938 return Vec::new();
939 };
940
941 let mut out = Vec::new();
942 for import in &module.imports {
943 let Some(selective) = &import.selective_names else {
944 continue;
945 };
946 let Some(import_path) = &import.path else {
947 continue;
948 };
949 let Some(target) = self
950 .modules
951 .get(import_path)
952 .or_else(|| self.modules.get(&normalize_path(import_path)))
953 else {
954 continue;
955 };
956 if target.load_error.is_some() {
957 continue;
958 }
959 for name in selective {
960 let kind = if target.exports.contains(name) {
961 continue;
962 } else if target.declarations.contains_key(name) {
963 SelectiveImportIssueKind::Private
964 } else {
965 SelectiveImportIssueKind::Missing
966 };
967 out.push(SelectiveImportIssue {
968 name: name.clone(),
969 module: import.raw_path.clone(),
970 span: import.import_span,
971 kind,
972 });
973 }
974 }
975 out.sort_by(|a, b| (&a.name, &a.module, a.kind).cmp(&(&b.name, &b.module, b.kind)));
976 out.dedup();
977 out
978 }
979}
980
981#[derive(Debug, Clone, PartialEq, Eq)]
984pub struct ReExportConflict {
985 pub name: String,
986 pub sources: Vec<PathBuf>,
987}
988
989#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
991pub enum SelectiveImportIssueKind {
992 Missing,
994 Private,
996}
997
998#[derive(Debug, Clone, PartialEq, Eq)]
1000pub struct SelectiveImportIssue {
1001 pub name: String,
1003 pub module: String,
1005 pub span: Span,
1007 pub kind: SelectiveImportIssueKind,
1009}
1010
1011impl SelectiveImportIssue {
1012 #[must_use]
1014 pub fn message(&self) -> String {
1015 match self.kind {
1016 SelectiveImportIssueKind::Missing => format!(
1017 "imported symbol `{}` does not exist in `{}`",
1018 self.name, self.module
1019 ),
1020 SelectiveImportIssueKind::Private => format!(
1021 "imported symbol `{}` is not exported by `{}` — it is defined there but not `pub`",
1022 self.name, self.module
1023 ),
1024 }
1025 }
1026
1027 #[must_use]
1029 pub fn help(&self) -> String {
1030 match self.kind {
1031 SelectiveImportIssueKind::Missing => format!(
1032 "update the import to a symbol exported by `{}`",
1033 self.module
1034 ),
1035 SelectiveImportIssueKind::Private => {
1036 format!(
1037 "mark `{}` as `pub` in `{}` to export it",
1038 self.name, self.module
1039 )
1040 }
1041 }
1042 }
1043}
1044
1045fn load_module(
1046 path: &Path,
1047 package_snapshots: &[PackageSnapshot],
1048 source_overrides: Option<&HashMap<PathBuf, String>>,
1049) -> (ModuleInfo, Option<ParsedModuleSource>) {
1050 let source = source_overrides
1051 .and_then(|overrides| overrides.get(&normalize_path(path)).cloned())
1052 .or_else(|| read_module_source(path));
1053 let Some(source) = source else {
1054 return (ModuleInfo::default(), None);
1055 };
1056 let mut lexer = harn_lexer::Lexer::new(&source);
1057 let tokens = match lexer.tokenize() {
1058 Ok(tokens) => tokens,
1059 Err(error) => {
1060 let module = ModuleInfo {
1061 load_error: Some(ModuleLoadError {
1062 message: error.to_string(),
1063 span: error.span(),
1064 }),
1065 ..ModuleInfo::default()
1066 };
1067 return (module, None);
1068 }
1069 };
1070 let mut parser = Parser::new(tokens);
1071 let program = match parser.parse() {
1072 Ok(program) => program,
1073 Err(error) => {
1074 let module = ModuleInfo {
1075 load_error: Some(ModuleLoadError {
1076 message: error.to_string(),
1077 span: error.span(),
1078 }),
1079 ..ModuleInfo::default()
1080 };
1081 return (module, None);
1082 }
1083 };
1084
1085 let mut module = ModuleInfo::default();
1086 for node in &program {
1087 collect_module_info(path, node, &mut module, package_snapshots);
1088 collect_type_declarations(node, &mut module.type_declarations);
1089 collect_callable_declarations(node, &mut module.callable_declarations);
1090 }
1091 if let Some(stdlib_module) = stdlib_module_from_path(path) {
1092 module.own_exports.extend(
1093 stdlib::builtin_reexports(stdlib_module)
1094 .iter()
1095 .map(|name| (*name).to_string()),
1096 );
1097 }
1098 module.exports.extend(module.own_exports.iter().cloned());
1102 module
1103 .exports
1104 .extend(module.selective_re_exports.keys().cloned());
1105 let parsed = ParsedModuleSource { source, program };
1106 (module, Some(parsed))
1107}
1108
1109fn stdlib_module_from_path(path: &Path) -> Option<&str> {
1112 let s = path.to_str()?;
1113 s.strip_prefix("<std>/")
1114}
1115
1116fn collect_module_info(
1117 file: &Path,
1118 snode: &SNode,
1119 module: &mut ModuleInfo,
1120 package_snapshots: &[PackageSnapshot],
1121) {
1122 match &snode.node {
1123 Node::FnDecl {
1124 name,
1125 params,
1126 is_pub,
1127 ..
1128 } => {
1129 if *is_pub {
1130 module.own_exports.insert(name.clone());
1131 }
1132 module.declarations.insert(
1133 name.clone(),
1134 decl_site(file, snode.span, name, DefKind::Function),
1135 );
1136 for param_name in params.iter().map(|param| param.name.clone()) {
1137 module.declarations.insert(
1138 param_name.clone(),
1139 decl_site(file, snode.span, ¶m_name, DefKind::Parameter),
1140 );
1141 }
1142 }
1143 Node::Pipeline { name, is_pub, .. } => {
1144 if *is_pub {
1145 module.own_exports.insert(name.clone());
1146 }
1147 module.declarations.insert(
1148 name.clone(),
1149 decl_site(file, snode.span, name, DefKind::Pipeline),
1150 );
1151 }
1152 Node::ToolDecl { name, is_pub, .. } => {
1153 if *is_pub {
1154 module.own_exports.insert(name.clone());
1155 }
1156 module.declarations.insert(
1157 name.clone(),
1158 decl_site(file, snode.span, name, DefKind::Tool),
1159 );
1160 }
1161 Node::SkillDecl { name, is_pub, .. } => {
1162 if *is_pub {
1163 module.own_exports.insert(name.clone());
1164 }
1165 module.declarations.insert(
1166 name.clone(),
1167 decl_site(file, snode.span, name, DefKind::Skill),
1168 );
1169 }
1170 Node::StructDecl { name, is_pub, .. } => {
1171 if *is_pub {
1172 module.own_exports.insert(name.clone());
1173 }
1174 module.declarations.insert(
1175 name.clone(),
1176 decl_site(file, snode.span, name, DefKind::Struct),
1177 );
1178 }
1179 Node::EnumDecl { name, is_pub, .. } => {
1180 if *is_pub {
1181 module.own_exports.insert(name.clone());
1182 }
1183 module.declarations.insert(
1184 name.clone(),
1185 decl_site(file, snode.span, name, DefKind::Enum),
1186 );
1187 }
1188 Node::InterfaceDecl { name, .. } => {
1189 module.own_exports.insert(name.clone());
1190 module.declarations.insert(
1191 name.clone(),
1192 decl_site(file, snode.span, name, DefKind::Interface),
1193 );
1194 }
1195 Node::TypeDecl { name, is_pub, .. } => {
1196 if *is_pub {
1197 module.own_exports.insert(name.clone());
1198 }
1199 module.declarations.insert(
1200 name.clone(),
1201 decl_site(file, snode.span, name, DefKind::Type),
1202 );
1203 }
1204 Node::LetBinding {
1205 pattern, is_pub, ..
1206 }
1207 | Node::ConstBinding {
1208 pattern, is_pub, ..
1209 } => {
1210 for name in pattern_names(pattern) {
1211 if *is_pub {
1215 module.own_exports.insert(name.clone());
1216 }
1217 module.declarations.insert(
1218 name.clone(),
1219 decl_site(file, snode.span, &name, DefKind::Variable),
1220 );
1221 }
1222 }
1223 Node::ImportDecl { path, is_pub } => {
1224 let import_path = resolve_import_path_with_snapshots(file, path, package_snapshots);
1225 if import_path.is_none() {
1226 module.has_unresolved_wildcard_import = true;
1227 }
1228 if *is_pub {
1229 if let Some(resolved) = &import_path {
1230 module
1231 .wildcard_re_export_paths
1232 .push(normalize_path(resolved));
1233 }
1234 }
1235 module.imports.push(ImportRef {
1236 raw_path: path.clone(),
1237 path: import_path,
1238 selective_names: None,
1239 import_span: snode.span,
1240 });
1241 }
1242 Node::SelectiveImport {
1243 names,
1244 path,
1245 is_pub,
1246 } => {
1247 let import_path = resolve_import_path_with_snapshots(file, path, package_snapshots);
1248 if import_path.is_none() {
1249 module.has_unresolved_selective_import = true;
1250 }
1251 if *is_pub {
1252 if let Some(resolved) = &import_path {
1253 let canonical = normalize_path(resolved);
1254 for name in names {
1255 module
1256 .selective_re_exports
1257 .entry(name.clone())
1258 .or_default()
1259 .push(canonical.clone());
1260 }
1261 }
1262 }
1263 let names: HashSet<String> = names.iter().cloned().collect();
1264 module.selective_import_names.extend(names.iter().cloned());
1265 module.imports.push(ImportRef {
1266 raw_path: path.clone(),
1267 path: import_path,
1268 selective_names: Some(names),
1269 import_span: snode.span,
1270 });
1271 }
1272 Node::AttributedDecl { inner, .. } => {
1273 collect_module_info(file, inner, module, package_snapshots);
1274 }
1275 _ => {}
1276 }
1277}
1278
1279fn collect_type_declarations(snode: &SNode, decls: &mut Vec<SNode>) {
1280 match &snode.node {
1281 Node::TypeDecl { .. }
1282 | Node::StructDecl { .. }
1283 | Node::EnumDecl { .. }
1284 | Node::InterfaceDecl { .. } => decls.push(snode.clone()),
1285 Node::AttributedDecl { inner, .. } => collect_type_declarations(inner, decls),
1286 _ => {}
1287 }
1288}
1289
1290fn collect_callable_declarations(snode: &SNode, decls: &mut Vec<SNode>) {
1291 match &snode.node {
1292 Node::FnDecl { .. } | Node::Pipeline { .. } | Node::ToolDecl { .. } => {
1293 decls.push(snode.clone());
1294 }
1295 Node::AttributedDecl { inner, .. } => collect_callable_declarations(inner, decls),
1296 _ => {}
1297 }
1298}
1299
1300fn type_decl_name(snode: &SNode) -> Option<&str> {
1301 match &snode.node {
1302 Node::TypeDecl { name, .. }
1303 | Node::StructDecl { name, .. }
1304 | Node::EnumDecl { name, .. }
1305 | Node::InterfaceDecl { name, .. } => Some(name.as_str()),
1306 _ => None,
1307 }
1308}
1309
1310fn callable_decl_name(snode: &SNode) -> Option<&str> {
1311 match &snode.node {
1312 Node::FnDecl { name, .. } | Node::Pipeline { name, .. } | Node::ToolDecl { name, .. } => {
1313 Some(name.as_str())
1314 }
1315 Node::AttributedDecl { inner, .. } => callable_decl_name(inner),
1316 _ => None,
1317 }
1318}
1319
1320fn decl_site(file: &Path, span: Span, name: &str, kind: DefKind) -> DefSite {
1321 DefSite {
1322 name: name.to_string(),
1323 file: file.to_path_buf(),
1324 kind,
1325 span,
1326 }
1327}
1328
1329fn pattern_names(pattern: &BindingPattern) -> Vec<String> {
1330 match pattern {
1331 BindingPattern::Identifier(name) => vec![name.clone()],
1332 BindingPattern::Dict(fields) => fields
1333 .iter()
1334 .filter_map(|field| field.alias.as_ref().or(Some(&field.key)).cloned())
1335 .collect(),
1336 BindingPattern::List(elements) => elements
1337 .iter()
1338 .map(|element| element.name.clone())
1339 .collect(),
1340 BindingPattern::Pair(a, b) => vec![a.clone(), b.clone()],
1341 }
1342}
1343
1344fn normalize_path(path: &Path) -> PathBuf {
1345 canonical_path(path)
1346}
1347
1348pub fn canonical_path(path: &Path) -> PathBuf {
1361 use std::sync::OnceLock;
1362 if stdlib_module_from_path(path).is_some() {
1363 return path.to_path_buf();
1364 }
1365 static MEMO: OnceLock<std::sync::Mutex<HashMap<PathBuf, PathBuf>>> = OnceLock::new();
1366 let memo = MEMO.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
1367 if let Some(hit) = memo
1368 .lock()
1369 .expect("canonical path memo lock poisoned")
1370 .get(path)
1371 .cloned()
1372 {
1373 return hit;
1374 }
1375 match path.canonicalize() {
1376 Ok(canonical) => {
1377 memo.lock()
1378 .expect("canonical path memo lock poisoned")
1379 .insert(path.to_path_buf(), canonical.clone());
1380 canonical
1381 }
1382 Err(_) => path.to_path_buf(),
1383 }
1384}
1385
1386#[cfg(test)]
1387#[path = "tests.rs"]
1388mod tests;