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 export_definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
801 let mut visited = HashSet::new();
802 self.export_definition_of_inner(file, name, &mut visited)
803 }
804
805 pub fn declared_names_for_file(&self, file: &Path) -> Option<Vec<&str>> {
809 let module = self.modules.get(&normalize_path(file))?;
810 let mut names: Vec<&str> = module.declarations.keys().map(String::as_str).collect();
811 names.sort_unstable();
812 Some(names)
813 }
814
815 fn definition_of_inner(
816 &self,
817 file: &Path,
818 name: &str,
819 visited: &mut HashSet<PathBuf>,
820 ) -> Option<DefSite> {
821 let file = normalize_path(file);
822 if !visited.insert(file.clone()) {
823 return None;
824 }
825 let current = self.modules.get(&file)?;
826
827 if let Some(local) = current.declarations.get(name) {
828 return Some(local.clone());
829 }
830
831 if let Some(sources) = current.selective_re_exports.get(name) {
836 for source in sources {
837 if let Some(def) = self.definition_of_inner(source, name, visited) {
838 return Some(def);
839 }
840 }
841 }
842
843 for source in ¤t.wildcard_re_export_paths {
845 if let Some(def) = self.definition_of_inner(source, name, visited) {
846 return Some(def);
847 }
848 }
849
850 for import in ¤t.imports {
852 let Some(selective_names) = &import.selective_names else {
853 continue;
854 };
855 if !selective_names.contains(name) {
856 continue;
857 }
858 if let Some(path) = &import.path {
859 if let Some(def) = self.definition_of_inner(path, name, visited) {
860 return Some(def);
861 }
862 }
863 }
864
865 for import in ¤t.imports {
867 if import.selective_names.is_some() {
868 continue;
869 }
870 if let Some(path) = &import.path {
871 if let Some(def) = self.definition_of_inner(path, name, visited) {
872 return Some(def);
873 }
874 }
875 }
876
877 None
878 }
879
880 fn export_definition_of_inner(
881 &self,
882 file: &Path,
883 name: &str,
884 visited: &mut HashSet<PathBuf>,
885 ) -> Option<DefSite> {
886 let file = normalize_path(file);
887 if !visited.insert(file.clone()) {
888 return None;
889 }
890 let current = self.modules.get(&file)?;
891
892 if current.own_exports.contains(name) {
893 if let Some(local) = current.declarations.get(name) {
894 return Some(local.clone());
895 }
896 }
897 if let Some(sources) = current.selective_re_exports.get(name) {
898 for source in sources {
899 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
900 return Some(definition);
901 }
902 }
903 }
904 for source in ¤t.wildcard_re_export_paths {
905 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
906 return Some(definition);
907 }
908 }
909 None
910 }
911
912 pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
916 let file = normalize_path(file);
917 let Some(module) = self.modules.get(&file) else {
918 return Vec::new();
919 };
920
921 let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
925
926 for (name, srcs) in &module.selective_re_exports {
927 sources
928 .entry(name.clone())
929 .or_default()
930 .extend(srcs.iter().cloned());
931 }
932 for src in &module.wildcard_re_export_paths {
933 let canonical = normalize_path(src);
934 let Some(src_module) = self
935 .modules
936 .get(&canonical)
937 .or_else(|| self.modules.get(src))
938 else {
939 continue;
940 };
941 for name in &src_module.exports {
942 sources
943 .entry(name.clone())
944 .or_default()
945 .push(canonical.clone());
946 }
947 }
948
949 for name in &module.own_exports {
953 if let Some(entry) = sources.get_mut(name) {
954 entry.push(file.clone());
955 }
956 }
957
958 let mut conflicts = Vec::new();
959 for (name, mut srcs) in sources {
960 srcs.sort();
961 srcs.dedup();
962 if srcs.len() > 1 {
963 conflicts.push(ReExportConflict {
964 name,
965 sources: srcs,
966 });
967 }
968 }
969 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
970 conflicts
971 }
972
973 pub fn selective_import_issues(&self, file: &Path) -> Vec<SelectiveImportIssue> {
979 let file = normalize_path(file);
980 let Some(module) = self.modules.get(&file) else {
981 return Vec::new();
982 };
983
984 let mut out = Vec::new();
985 for import in &module.imports {
986 let Some(selective) = &import.selective_names else {
987 continue;
988 };
989 let Some(import_path) = &import.path else {
990 continue;
991 };
992 let Some(target) = self
993 .modules
994 .get(import_path)
995 .or_else(|| self.modules.get(&normalize_path(import_path)))
996 else {
997 continue;
998 };
999 if target.load_error.is_some() {
1000 continue;
1001 }
1002 for name in selective {
1003 let kind = if target.exports.contains(name) {
1004 continue;
1005 } else if target.declarations.contains_key(name) {
1006 SelectiveImportIssueKind::Private
1007 } else {
1008 SelectiveImportIssueKind::Missing
1009 };
1010 out.push(SelectiveImportIssue {
1011 name: name.clone(),
1012 module: import.raw_path.clone(),
1013 span: import.import_span,
1014 kind,
1015 });
1016 }
1017 }
1018 out.sort_by(|a, b| (&a.name, &a.module, a.kind).cmp(&(&b.name, &b.module, b.kind)));
1019 out.dedup();
1020 out
1021 }
1022}
1023
1024#[derive(Debug, Clone, PartialEq, Eq)]
1027pub struct ReExportConflict {
1028 pub name: String,
1029 pub sources: Vec<PathBuf>,
1030}
1031
1032#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1034pub enum SelectiveImportIssueKind {
1035 Missing,
1037 Private,
1039}
1040
1041#[derive(Debug, Clone, PartialEq, Eq)]
1043pub struct SelectiveImportIssue {
1044 pub name: String,
1046 pub module: String,
1048 pub span: Span,
1050 pub kind: SelectiveImportIssueKind,
1052}
1053
1054impl SelectiveImportIssue {
1055 #[must_use]
1057 pub fn message(&self) -> String {
1058 match self.kind {
1059 SelectiveImportIssueKind::Missing => format!(
1060 "imported symbol `{}` does not exist in `{}`",
1061 self.name, self.module
1062 ),
1063 SelectiveImportIssueKind::Private => format!(
1064 "imported symbol `{}` is not exported by `{}` — it is defined there but not `pub`",
1065 self.name, self.module
1066 ),
1067 }
1068 }
1069
1070 #[must_use]
1072 pub fn help(&self) -> String {
1073 match self.kind {
1074 SelectiveImportIssueKind::Missing => format!(
1075 "update the import to a symbol exported by `{}`",
1076 self.module
1077 ),
1078 SelectiveImportIssueKind::Private => {
1079 format!(
1080 "mark `{}` as `pub` in `{}` to export it",
1081 self.name, self.module
1082 )
1083 }
1084 }
1085 }
1086}
1087
1088fn load_module(
1089 path: &Path,
1090 package_snapshots: &[PackageSnapshot],
1091 source_overrides: Option<&HashMap<PathBuf, String>>,
1092) -> (ModuleInfo, Option<ParsedModuleSource>) {
1093 let source = source_overrides
1094 .and_then(|overrides| overrides.get(&normalize_path(path)).cloned())
1095 .or_else(|| read_module_source(path));
1096 let Some(source) = source else {
1097 return (ModuleInfo::default(), None);
1098 };
1099 let mut lexer = harn_lexer::Lexer::new(&source);
1100 let tokens = match lexer.tokenize() {
1101 Ok(tokens) => tokens,
1102 Err(error) => {
1103 let module = ModuleInfo {
1104 load_error: Some(ModuleLoadError {
1105 message: error.to_string(),
1106 span: error.span(),
1107 }),
1108 ..ModuleInfo::default()
1109 };
1110 return (module, None);
1111 }
1112 };
1113 let mut parser = Parser::new(tokens);
1114 let program = match parser.parse() {
1115 Ok(program) => program,
1116 Err(error) => {
1117 let module = ModuleInfo {
1118 load_error: Some(ModuleLoadError {
1119 message: error.to_string(),
1120 span: error.span(),
1121 }),
1122 ..ModuleInfo::default()
1123 };
1124 return (module, None);
1125 }
1126 };
1127
1128 let mut module = ModuleInfo::default();
1129 for node in &program {
1130 collect_module_info(path, node, &mut module, package_snapshots);
1131 collect_type_declarations(node, &mut module.type_declarations);
1132 collect_callable_declarations(node, &mut module.callable_declarations);
1133 }
1134 if let Some(stdlib_module) = stdlib_module_from_path(path) {
1135 module.own_exports.extend(
1136 stdlib::builtin_reexports(stdlib_module)
1137 .iter()
1138 .map(|name| (*name).to_string()),
1139 );
1140 }
1141 module.exports.extend(module.own_exports.iter().cloned());
1145 module
1146 .exports
1147 .extend(module.selective_re_exports.keys().cloned());
1148 let parsed = ParsedModuleSource { source, program };
1149 (module, Some(parsed))
1150}
1151
1152fn stdlib_module_from_path(path: &Path) -> Option<&str> {
1155 let s = path.to_str()?;
1156 s.strip_prefix("<std>/")
1157}
1158
1159fn collect_module_info(
1160 file: &Path,
1161 snode: &SNode,
1162 module: &mut ModuleInfo,
1163 package_snapshots: &[PackageSnapshot],
1164) {
1165 match &snode.node {
1166 Node::FnDecl {
1167 name,
1168 params,
1169 is_pub,
1170 ..
1171 } => {
1172 if *is_pub {
1173 module.own_exports.insert(name.clone());
1174 }
1175 module.declarations.insert(
1176 name.clone(),
1177 decl_site(file, snode.span, name, DefKind::Function),
1178 );
1179 for param_name in params.iter().map(|param| param.name.clone()) {
1180 module.declarations.insert(
1181 param_name.clone(),
1182 decl_site(file, snode.span, ¶m_name, DefKind::Parameter),
1183 );
1184 }
1185 }
1186 Node::Pipeline { name, is_pub, .. } => {
1187 if *is_pub {
1188 module.own_exports.insert(name.clone());
1189 }
1190 module.declarations.insert(
1191 name.clone(),
1192 decl_site(file, snode.span, name, DefKind::Pipeline),
1193 );
1194 }
1195 Node::ToolDecl { 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::Tool),
1202 );
1203 }
1204 Node::SkillDecl { name, is_pub, .. } => {
1205 if *is_pub {
1206 module.own_exports.insert(name.clone());
1207 }
1208 module.declarations.insert(
1209 name.clone(),
1210 decl_site(file, snode.span, name, DefKind::Skill),
1211 );
1212 }
1213 Node::StructDecl { name, is_pub, .. } => {
1214 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::Struct),
1220 );
1221 }
1222 Node::EnumDecl { name, is_pub, .. } => {
1223 if *is_pub {
1224 module.own_exports.insert(name.clone());
1225 }
1226 module.declarations.insert(
1227 name.clone(),
1228 decl_site(file, snode.span, name, DefKind::Enum),
1229 );
1230 }
1231 Node::InterfaceDecl { name, .. } => {
1232 module.own_exports.insert(name.clone());
1233 module.declarations.insert(
1234 name.clone(),
1235 decl_site(file, snode.span, name, DefKind::Interface),
1236 );
1237 }
1238 Node::TypeDecl { name, is_pub, .. } => {
1239 if *is_pub {
1240 module.own_exports.insert(name.clone());
1241 }
1242 module.declarations.insert(
1243 name.clone(),
1244 decl_site(file, snode.span, name, DefKind::Type),
1245 );
1246 }
1247 Node::LetBinding {
1248 pattern, is_pub, ..
1249 }
1250 | Node::ConstBinding {
1251 pattern, is_pub, ..
1252 } => {
1253 for name in pattern_names(pattern) {
1254 if *is_pub {
1258 module.own_exports.insert(name.clone());
1259 }
1260 module.declarations.insert(
1261 name.clone(),
1262 decl_site(file, snode.span, &name, DefKind::Variable),
1263 );
1264 }
1265 }
1266 Node::ImportDecl { path, is_pub } => {
1267 let import_path = resolve_import_path_with_snapshots(file, path, package_snapshots);
1268 if import_path.is_none() {
1269 module.has_unresolved_wildcard_import = true;
1270 }
1271 if *is_pub {
1272 if let Some(resolved) = &import_path {
1273 module
1274 .wildcard_re_export_paths
1275 .push(normalize_path(resolved));
1276 }
1277 }
1278 module.imports.push(ImportRef {
1279 raw_path: path.clone(),
1280 path: import_path,
1281 selective_names: None,
1282 import_span: snode.span,
1283 });
1284 }
1285 Node::SelectiveImport {
1286 names,
1287 path,
1288 is_pub,
1289 } => {
1290 let import_path = resolve_import_path_with_snapshots(file, path, package_snapshots);
1291 if import_path.is_none() {
1292 module.has_unresolved_selective_import = true;
1293 }
1294 if *is_pub {
1295 if let Some(resolved) = &import_path {
1296 let canonical = normalize_path(resolved);
1297 for name in names {
1298 module
1299 .selective_re_exports
1300 .entry(name.clone())
1301 .or_default()
1302 .push(canonical.clone());
1303 }
1304 }
1305 }
1306 let names: HashSet<String> = names.iter().cloned().collect();
1307 module.selective_import_names.extend(names.iter().cloned());
1308 module.imports.push(ImportRef {
1309 raw_path: path.clone(),
1310 path: import_path,
1311 selective_names: Some(names),
1312 import_span: snode.span,
1313 });
1314 }
1315 Node::AttributedDecl { inner, .. } => {
1316 collect_module_info(file, inner, module, package_snapshots);
1317 }
1318 _ => {}
1319 }
1320}
1321
1322fn collect_type_declarations(snode: &SNode, decls: &mut Vec<SNode>) {
1323 match &snode.node {
1324 Node::TypeDecl { .. }
1325 | Node::StructDecl { .. }
1326 | Node::EnumDecl { .. }
1327 | Node::InterfaceDecl { .. } => decls.push(snode.clone()),
1328 Node::AttributedDecl { inner, .. } => collect_type_declarations(inner, decls),
1329 _ => {}
1330 }
1331}
1332
1333fn collect_callable_declarations(snode: &SNode, decls: &mut Vec<SNode>) {
1334 match &snode.node {
1335 Node::FnDecl { .. } | Node::Pipeline { .. } | Node::ToolDecl { .. } => {
1336 decls.push(snode.clone());
1337 }
1338 Node::AttributedDecl { inner, .. } => collect_callable_declarations(inner, decls),
1339 _ => {}
1340 }
1341}
1342
1343fn type_decl_name(snode: &SNode) -> Option<&str> {
1344 match &snode.node {
1345 Node::TypeDecl { name, .. }
1346 | Node::StructDecl { name, .. }
1347 | Node::EnumDecl { name, .. }
1348 | Node::InterfaceDecl { name, .. } => Some(name.as_str()),
1349 _ => None,
1350 }
1351}
1352
1353fn callable_decl_name(snode: &SNode) -> Option<&str> {
1354 match &snode.node {
1355 Node::FnDecl { name, .. } | Node::Pipeline { name, .. } | Node::ToolDecl { name, .. } => {
1356 Some(name.as_str())
1357 }
1358 Node::AttributedDecl { inner, .. } => callable_decl_name(inner),
1359 _ => None,
1360 }
1361}
1362
1363fn decl_site(file: &Path, span: Span, name: &str, kind: DefKind) -> DefSite {
1364 DefSite {
1365 name: name.to_string(),
1366 file: file.to_path_buf(),
1367 kind,
1368 span,
1369 }
1370}
1371
1372fn pattern_names(pattern: &BindingPattern) -> Vec<String> {
1373 match pattern {
1374 BindingPattern::Identifier(name) => vec![name.clone()],
1375 BindingPattern::Dict(fields) => fields
1376 .iter()
1377 .filter_map(|field| field.alias.as_ref().or(Some(&field.key)).cloned())
1378 .collect(),
1379 BindingPattern::List(elements) => elements
1380 .iter()
1381 .map(|element| element.name.clone())
1382 .collect(),
1383 BindingPattern::Pair(a, b) => vec![a.clone(), b.clone()],
1384 }
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 stdlib_module_from_path(path).is_some() {
1406 return path.to_path_buf();
1407 }
1408 static MEMO: OnceLock<std::sync::Mutex<HashMap<PathBuf, PathBuf>>> = OnceLock::new();
1409 let memo = MEMO.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
1410 if let Some(hit) = memo
1411 .lock()
1412 .expect("canonical path memo lock poisoned")
1413 .get(path)
1414 .cloned()
1415 {
1416 return hit;
1417 }
1418 match path.canonicalize() {
1419 Ok(canonical) => {
1420 memo.lock()
1421 .expect("canonical path memo lock poisoned")
1422 .insert(path.to_path_buf(), canonical.clone());
1423 canonical
1424 }
1425 Err(_) => path.to_path_buf(),
1426 }
1427}
1428
1429#[cfg(test)]
1430#[path = "tests.rs"]
1431mod tests;