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::{Node, Parser, SNode};
8
9pub mod asset_paths;
10mod declarations;
11pub mod fingerprint;
12pub mod manifest_walk;
13pub mod package_execution;
14mod package_imports;
15pub mod package_snapshot;
16pub mod personas;
17pub mod project_config;
18mod stdlib;
19
20use declarations::pattern_names;
21pub use declarations::{public_declarations, DefKind, PublicDeclaration};
22pub use package_imports::{
23 resolve_import_path, resolve_import_path_with_guard, resolve_import_path_with_snapshot,
24};
25
26#[derive(Debug, Clone)]
28pub struct DefSite {
29 pub name: String,
30 pub file: PathBuf,
31 pub kind: DefKind,
32 pub span: Span,
33}
34
35#[derive(Debug, Clone)]
37pub enum WildcardResolution {
38 Resolved(HashSet<String>),
40 Unknown,
42}
43
44#[derive(Debug, Default)]
46pub struct ModuleGraph {
47 modules: HashMap<PathBuf, ModuleInfo>,
48 _package_snapshots: Vec<PackageSnapshot>,
50}
51
52#[derive(Debug, Clone)]
53pub struct ParsedModuleSource {
54 pub source: String,
55 pub program: Vec<SNode>,
56}
57
58#[derive(Debug, Default)]
59pub struct ModuleGraphBuild {
60 pub graph: ModuleGraph,
61 pub parsed_sources: HashMap<PathBuf, ParsedModuleSource>,
62}
63
64#[derive(Debug, Default)]
65struct ModuleInfo {
66 declarations: HashMap<String, DefSite>,
69 exports: HashSet<String>,
74 own_exports: HashSet<String>,
77 selective_re_exports: HashMap<String, Vec<PathBuf>>,
84 wildcard_re_export_paths: Vec<PathBuf>,
88 selective_import_names: HashSet<String>,
90 imports: Vec<ImportRef>,
92 has_unresolved_wildcard_import: bool,
94 has_unresolved_selective_import: bool,
98 type_declarations: Vec<SNode>,
101 callable_declarations: Vec<SNode>,
104 load_error: Option<ModuleLoadError>,
110}
111
112#[derive(Debug, Clone)]
118pub struct ModuleLoadError {
119 pub message: String,
121 pub span: Span,
123}
124
125#[derive(Debug, Clone)]
128pub struct ImportCompileFailure {
129 pub import_raw_path: String,
131 pub import_span: Span,
133 pub module_path: PathBuf,
135 pub error: ModuleLoadError,
137}
138
139#[derive(Debug, Clone)]
140struct ImportRef {
141 raw_path: String,
142 path: Option<PathBuf>,
143 selective_names: Option<HashSet<String>>,
144 import_span: Span,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct ModuleImport {
150 pub raw_path: String,
152 pub resolved_path: Option<PathBuf>,
154 pub selective_names: Option<Vec<String>>,
156}
157
158pub fn read_module_source(path: &Path) -> Option<String> {
164 if let Some(stdlib_module) = stdlib_module_from_path(path) {
165 return stdlib::get_stdlib_source(stdlib_module).map(ToString::to_string);
166 }
167 std::fs::read_to_string(path).ok()
168}
169
170pub fn build(files: &[PathBuf]) -> ModuleGraph {
176 build_inner(files, None, None).graph
177}
178
179pub fn build_with_source(file: &Path, source: &str) -> ModuleGraph {
185 let file = normalize_path(file);
186 let source_overrides = HashMap::from([(file.clone(), source.to_string())]);
187 build_inner(&[file], None, Some(&source_overrides)).graph
188}
189
190pub fn build_with_parsed_sources(files: &[PathBuf]) -> ModuleGraphBuild {
196 let parsed_source_targets = files.iter().map(|file| normalize_path(file)).collect();
197 build_inner(files, Some(&parsed_source_targets), None)
198}
199
200fn build_inner(
201 files: &[PathBuf],
202 parsed_source_targets: Option<&HashSet<PathBuf>>,
203 source_overrides: Option<&HashMap<PathBuf, String>>,
204) -> ModuleGraphBuild {
205 let package_snapshots = acquire_package_snapshots(files);
206 let mut modules: HashMap<PathBuf, ModuleInfo> = HashMap::new();
207 let mut parsed_sources: HashMap<PathBuf, ParsedModuleSource> = HashMap::new();
208 let mut seen: HashSet<PathBuf> = HashSet::new();
209 let mut wave: Vec<PathBuf> = Vec::new();
210 for file in files {
211 let canonical = normalize_path(file);
212 if seen.insert(canonical.clone()) {
213 wave.push(canonical);
214 }
215 }
216 while !wave.is_empty() {
224 let loaded = load_wave(&wave, &package_snapshots, source_overrides);
225 let mut next_wave: Vec<PathBuf> = Vec::new();
226 for (path, (module, parsed)) in wave.drain(..).zip(loaded) {
227 let retain_parsed_source =
228 parsed_source_targets.is_some_and(|targets| targets.contains(&path));
229 if retain_parsed_source {
230 if let Some(parsed) = parsed {
231 parsed_sources.insert(path.clone(), parsed);
232 }
233 }
234 for import in &module.imports {
251 if let Some(import_path) = &import.path {
252 let canonical = normalize_path(import_path);
253 if seen.insert(canonical.clone()) {
254 next_wave.push(canonical);
255 }
256 }
257 }
258 modules.insert(path, module);
259 }
260 wave = next_wave;
261 }
262 resolve_re_exports(&mut modules);
263 ModuleGraphBuild {
264 graph: ModuleGraph {
265 modules,
266 _package_snapshots: package_snapshots,
267 },
268 parsed_sources,
269 }
270}
271
272pub const MODULE_GRAPH_JOBS_ENV: &str = "HARN_MODULE_GRAPH_JOBS";
275
276fn load_wave(
279 paths: &[PathBuf],
280 package_snapshots: &[PackageSnapshot],
281 source_overrides: Option<&HashMap<PathBuf, String>>,
282) -> Vec<(ModuleInfo, Option<ParsedModuleSource>)> {
283 const MIN_PARALLEL_WAVE: usize = 8;
284 let configured = std::env::var(MODULE_GRAPH_JOBS_ENV)
285 .ok()
286 .and_then(|value| value.parse::<usize>().ok())
287 .filter(|&jobs| jobs > 0);
288 let workers = configured
289 .unwrap_or_else(|| {
290 std::thread::available_parallelism()
291 .map(std::num::NonZeroUsize::get)
292 .unwrap_or(1)
293 })
294 .min(paths.len());
295 if workers <= 1 || paths.len() < MIN_PARALLEL_WAVE {
296 return paths
297 .iter()
298 .map(|path| load_module(path, package_snapshots, source_overrides))
299 .collect();
300 }
301 let next = std::sync::atomic::AtomicUsize::new(0);
302 let mut produced: Vec<(usize, (ModuleInfo, Option<ParsedModuleSource>))> =
303 std::thread::scope(|scope| {
304 let handles: Vec<_> = (0..workers)
305 .map(|_| {
306 scope.spawn(|| {
307 let mut local = Vec::new();
308 loop {
309 let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
310 let Some(path) = paths.get(index) else {
311 break;
312 };
313 local.push((
314 index,
315 load_module(path, package_snapshots, source_overrides),
316 ));
317 }
318 local
319 })
320 })
321 .collect();
322 handles
323 .into_iter()
324 .flat_map(|handle| match handle.join() {
325 Ok(local) => local,
326 Err(panic) => std::panic::resume_unwind(panic),
327 })
328 .collect()
329 });
330 produced.sort_unstable_by_key(|(index, _)| *index);
331 produced.into_iter().map(|(_, loaded)| loaded).collect()
332}
333
334fn resolve_re_exports(modules: &mut HashMap<PathBuf, ModuleInfo>) {
339 let keys: Vec<PathBuf> = modules.keys().cloned().collect();
340 loop {
341 let mut changed = false;
342 for path in &keys {
343 let wildcard_paths = modules
346 .get(path)
347 .map(|m| m.wildcard_re_export_paths.clone())
348 .unwrap_or_default();
349 if wildcard_paths.is_empty() {
350 continue;
351 }
352 let mut additions: Vec<String> = Vec::new();
353 for src in &wildcard_paths {
354 let src_canonical = normalize_path(src);
355 if let Some(src_module) = modules.get(src).or_else(|| modules.get(&src_canonical)) {
356 additions.extend(src_module.exports.iter().cloned());
357 }
358 }
359 if let Some(module) = modules.get_mut(path) {
360 for name in additions {
361 if module.exports.insert(name) {
362 changed = true;
363 }
364 }
365 }
366 }
367 if !changed {
368 break;
369 }
370 }
371}
372
373impl ModuleGraph {
374 pub fn module_paths(&self) -> Vec<PathBuf> {
379 let mut paths: Vec<PathBuf> = self.modules.keys().cloned().collect();
380 paths.sort();
381 paths
382 }
383
384 pub fn contains_module(&self, path: &Path) -> bool {
387 self.modules.contains_key(path) || self.modules.contains_key(&normalize_path(path))
388 }
389
390 pub fn all_selective_import_names(&self) -> HashSet<&str> {
392 let mut names = HashSet::new();
393 for module in self.modules.values() {
394 for name in &module.selective_import_names {
395 names.insert(name.as_str());
396 }
397 }
398 names
399 }
400
401 pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
404 let target = normalize_path(target);
405 let mut out: Vec<PathBuf> = self
406 .modules
407 .iter()
408 .filter(|(_, info)| {
409 info.imports.iter().any(|import| {
410 import
411 .path
412 .as_ref()
413 .is_some_and(|p| normalize_path(p) == target)
414 })
415 })
416 .map(|(path, _)| path.clone())
417 .collect();
418 out.sort();
419 out
420 }
421
422 pub fn imports_for_module(&self, file: &Path) -> Vec<ModuleImport> {
424 let file = normalize_path(file);
425 let Some(module) = self.modules.get(&file) else {
426 return Vec::new();
427 };
428 let mut imports: Vec<ModuleImport> = module
429 .imports
430 .iter()
431 .map(|import| {
432 let mut selective_names = import
433 .selective_names
434 .as_ref()
435 .map(|names| names.iter().cloned().collect::<Vec<_>>());
436 if let Some(names) = selective_names.as_mut() {
437 names.sort();
438 }
439 ModuleImport {
440 raw_path: import.raw_path.clone(),
441 resolved_path: import.path.as_ref().map(|path| normalize_path(path)),
442 selective_names,
443 }
444 })
445 .collect();
446 imports.sort_by(|left, right| {
447 left.raw_path
448 .cmp(&right.raw_path)
449 .then_with(|| left.selective_names.cmp(&right.selective_names))
450 .then_with(|| left.resolved_path.cmp(&right.resolved_path))
451 });
452 imports
453 }
454
455 pub fn exports_for_module(&self, file: &Path) -> Vec<String> {
457 let file = normalize_path(file);
458 let Some(module) = self.modules.get(&file) else {
459 return Vec::new();
460 };
461 let mut exports: Vec<String> = module.exports.iter().cloned().collect();
462 exports.sort();
463 exports
464 }
465
466 pub fn wildcard_exports_for(&self, file: &Path) -> WildcardResolution {
471 let file = normalize_path(file);
472 let Some(module) = self.modules.get(&file) else {
473 return WildcardResolution::Unknown;
474 };
475 if module.has_unresolved_wildcard_import {
476 return WildcardResolution::Unknown;
477 }
478
479 let mut names = HashSet::new();
480 for import in module
481 .imports
482 .iter()
483 .filter(|import| import.selective_names.is_none())
484 {
485 let Some(import_path) = &import.path else {
486 return WildcardResolution::Unknown;
487 };
488 let imported = self.modules.get(import_path).or_else(|| {
489 let normalized = normalize_path(import_path);
490 self.modules.get(&normalized)
491 });
492 let Some(imported) = imported else {
493 return WildcardResolution::Unknown;
494 };
495 names.extend(imported.exports.iter().cloned());
496 }
497 WildcardResolution::Resolved(names)
498 }
499
500 #[must_use]
521 pub fn import_compile_failures(&self, file: &Path) -> Vec<ImportCompileFailure> {
522 let file = normalize_path(file);
523 let Some(module) = self.modules.get(&file) else {
524 return Vec::new();
525 };
526 let mut failures = Vec::new();
527 for import in &module.imports {
528 let Some(import_path) = &import.path else {
529 continue;
530 };
531 let Some(target) = self
532 .modules
533 .get(import_path)
534 .or_else(|| self.modules.get(&normalize_path(import_path)))
535 else {
536 continue;
537 };
538 if let Some(error) = &target.load_error {
539 failures.push(ImportCompileFailure {
540 import_raw_path: import.raw_path.clone(),
541 import_span: import.import_span,
542 module_path: normalize_path(import_path),
543 error: error.clone(),
544 });
545 }
546 }
547 failures
548 }
549
550 pub fn imported_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
551 let file = normalize_path(file);
552 let module = self.modules.get(&file)?;
553 if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
554 return None;
555 }
556
557 let mut names = HashSet::new();
558 for import in &module.imports {
559 let import_path = import.path.as_ref()?;
560 let imported = self
561 .modules
562 .get(import_path)
563 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
564 if imported.load_error.is_some() {
570 return None;
571 }
572 match &import.selective_names {
573 None => {
574 names.extend(imported.exports.iter().cloned());
575 }
576 Some(selective) => {
577 for name in selective {
586 if imported.declarations.contains_key(name)
587 || imported.exports.contains(name)
588 {
589 names.insert(name.clone());
590 }
591 }
592 }
593 }
594 }
595 Some(names)
596 }
597
598 pub fn imported_names_by_kind_for_file(
603 &self,
604 file: &Path,
605 kind: DefKind,
606 ) -> Option<HashSet<String>> {
607 let file = normalize_path(file);
608 let module = self.modules.get(&file)?;
609 if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
610 return None;
611 }
612
613 let mut names = HashSet::new();
614 for import in &module.imports {
615 let import_path = import.path.as_ref()?;
616 let imported_names: Vec<String> = match &import.selective_names {
617 Some(selective) => selective.iter().cloned().collect(),
618 None => self
619 .modules
620 .get(import_path)
621 .or_else(|| self.modules.get(&normalize_path(import_path)))?
622 .exports
623 .iter()
624 .cloned()
625 .collect(),
626 };
627 for name in imported_names {
628 if self.exported_kind(import_path, &name) == Some(kind) {
629 names.insert(name);
630 }
631 }
632 }
633 Some(names)
634 }
635
636 pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
640 let file = normalize_path(file);
641 let module = self.modules.get(&file)?;
642 if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
643 return None;
644 }
645
646 let mut decls = Vec::new();
647 for import in &module.imports {
648 let import_path = import.path.as_ref()?;
649 let imported = self
650 .modules
651 .get(import_path)
652 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
653 if imported.load_error.is_some() {
659 return None;
660 }
661 let names_to_collect: Vec<String> = match &import.selective_names {
662 None => imported.exports.iter().cloned().collect(),
663 Some(selective) => selective.iter().cloned().collect(),
664 };
665 for name in &names_to_collect {
666 let mut visited = HashSet::new();
667 if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
668 decls.push(decl);
669 }
670 }
671 for ty_decl in &imported.type_declarations {
681 if type_decl_name(ty_decl).is_some() {
682 decls.push(ty_decl.clone());
683 }
684 }
685 }
686 Some(decls)
687 }
688
689 pub fn imported_callable_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
693 let file = normalize_path(file);
694 let module = self.modules.get(&file)?;
695 if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
696 return None;
697 }
698
699 let mut decls = Vec::new();
700 for import in &module.imports {
701 let import_path = import.path.as_ref()?;
702 let imported = self
703 .modules
704 .get(import_path)
705 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
706 if imported.load_error.is_some() {
712 return None;
713 }
714 let selective_import = import.selective_names.is_some();
715 let names_to_collect: Vec<String> = match &import.selective_names {
716 None => imported.exports.iter().cloned().collect(),
717 Some(selective) => selective.iter().cloned().collect(),
718 };
719 for name in &names_to_collect {
720 if selective_import || imported.own_exports.contains(name) {
721 if let Some(decl) = imported
722 .callable_declarations
723 .iter()
724 .find(|decl| callable_decl_name(decl) == Some(name.as_str()))
725 {
726 decls.push(decl.clone());
727 continue;
728 }
729 }
730 let mut visited = HashSet::new();
731 if let Some(decl) =
732 self.find_exported_callable_decl(import_path, name, &mut visited)
733 {
734 decls.push(decl);
735 }
736 }
737 }
738 Some(decls)
739 }
740
741 fn find_exported_type_decl(
744 &self,
745 path: &Path,
746 name: &str,
747 visited: &mut HashSet<PathBuf>,
748 ) -> Option<SNode> {
749 let canonical = normalize_path(path);
750 if !visited.insert(canonical.clone()) {
751 return None;
752 }
753 let module = self
754 .modules
755 .get(&canonical)
756 .or_else(|| self.modules.get(path))?;
757 for decl in &module.type_declarations {
758 if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
759 return Some(decl.clone());
760 }
761 }
762 if let Some(sources) = module.selective_re_exports.get(name) {
763 for source in sources {
764 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
765 return Some(decl);
766 }
767 }
768 }
769 for source in &module.wildcard_re_export_paths {
770 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
771 return Some(decl);
772 }
773 }
774 None
775 }
776
777 fn find_exported_callable_decl(
778 &self,
779 path: &Path,
780 name: &str,
781 visited: &mut HashSet<PathBuf>,
782 ) -> Option<SNode> {
783 let canonical = normalize_path(path);
784 if !visited.insert(canonical.clone()) {
785 return None;
786 }
787 let module = self
788 .modules
789 .get(&canonical)
790 .or_else(|| self.modules.get(path))?;
791 for decl in &module.callable_declarations {
792 if callable_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
793 return Some(decl.clone());
794 }
795 }
796 if let Some(sources) = module.selective_re_exports.get(name) {
797 for source in sources {
798 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
799 return Some(decl);
800 }
801 }
802 }
803 for source in &module.wildcard_re_export_paths {
804 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
805 return Some(decl);
806 }
807 }
808 None
809 }
810
811 pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
817 let mut visited = HashSet::new();
818 self.definition_of_inner(file, name, &mut visited)
819 }
820
821 pub fn export_definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
828 let mut visited = HashSet::new();
829 self.export_definition_of_inner(file, name, &mut visited)
830 }
831
832 pub fn declared_names_for_file(&self, file: &Path) -> Option<Vec<&str>> {
836 let module = self.modules.get(&normalize_path(file))?;
837 let mut names: Vec<&str> = module.declarations.keys().map(String::as_str).collect();
838 names.sort_unstable();
839 Some(names)
840 }
841
842 fn definition_of_inner(
843 &self,
844 file: &Path,
845 name: &str,
846 visited: &mut HashSet<PathBuf>,
847 ) -> Option<DefSite> {
848 let file = normalize_path(file);
849 if !visited.insert(file.clone()) {
850 return None;
851 }
852 let current = self.modules.get(&file)?;
853
854 if let Some(local) = current.declarations.get(name) {
855 return Some(local.clone());
856 }
857
858 if let Some(sources) = current.selective_re_exports.get(name) {
863 for source in sources {
864 if let Some(def) = self.definition_of_inner(source, name, visited) {
865 return Some(def);
866 }
867 }
868 }
869
870 for source in ¤t.wildcard_re_export_paths {
872 if let Some(def) = self.definition_of_inner(source, name, visited) {
873 return Some(def);
874 }
875 }
876
877 for import in ¤t.imports {
879 let Some(selective_names) = &import.selective_names else {
880 continue;
881 };
882 if !selective_names.contains(name) {
883 continue;
884 }
885 if let Some(path) = &import.path {
886 if let Some(def) = self.definition_of_inner(path, name, visited) {
887 return Some(def);
888 }
889 }
890 }
891
892 for import in ¤t.imports {
894 if import.selective_names.is_some() {
895 continue;
896 }
897 if let Some(path) = &import.path {
898 if let Some(def) = self.definition_of_inner(path, name, visited) {
899 return Some(def);
900 }
901 }
902 }
903
904 None
905 }
906
907 fn export_definition_of_inner(
908 &self,
909 file: &Path,
910 name: &str,
911 visited: &mut HashSet<PathBuf>,
912 ) -> Option<DefSite> {
913 let file = normalize_path(file);
914 if !visited.insert(file.clone()) {
915 return None;
916 }
917 let current = self.modules.get(&file)?;
918
919 if current.own_exports.contains(name) {
920 if let Some(local) = current.declarations.get(name) {
921 return Some(local.clone());
922 }
923 }
924 if let Some(sources) = current.selective_re_exports.get(name) {
925 for source in sources {
926 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
927 return Some(definition);
928 }
929 }
930 }
931 for source in ¤t.wildcard_re_export_paths {
932 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
933 return Some(definition);
934 }
935 }
936 None
937 }
938
939 pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
943 let file = normalize_path(file);
944 let Some(module) = self.modules.get(&file) else {
945 return Vec::new();
946 };
947
948 let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
952
953 for (name, srcs) in &module.selective_re_exports {
954 sources
955 .entry(name.clone())
956 .or_default()
957 .extend(srcs.iter().cloned());
958 }
959 for src in &module.wildcard_re_export_paths {
960 let canonical = normalize_path(src);
961 let Some(src_module) = self
962 .modules
963 .get(&canonical)
964 .or_else(|| self.modules.get(src))
965 else {
966 continue;
967 };
968 for name in &src_module.exports {
969 sources
970 .entry(name.clone())
971 .or_default()
972 .push(canonical.clone());
973 }
974 }
975
976 for name in &module.own_exports {
980 if let Some(entry) = sources.get_mut(name) {
981 entry.push(file.clone());
982 }
983 }
984
985 let mut conflicts = Vec::new();
986 for (name, mut srcs) in sources {
987 srcs.sort();
988 srcs.dedup();
989 if srcs.len() > 1 {
990 conflicts.push(ReExportConflict {
991 name,
992 sources: srcs,
993 });
994 }
995 }
996 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
997 conflicts
998 }
999
1000 pub fn selective_import_issues(&self, file: &Path) -> Vec<SelectiveImportIssue> {
1006 let file = normalize_path(file);
1007 let Some(module) = self.modules.get(&file) else {
1008 return Vec::new();
1009 };
1010
1011 let mut out = Vec::new();
1012 for import in &module.imports {
1013 let Some(selective) = &import.selective_names else {
1014 continue;
1015 };
1016 let Some(import_path) = &import.path else {
1017 continue;
1018 };
1019 let Some(target) = self
1020 .modules
1021 .get(import_path)
1022 .or_else(|| self.modules.get(&normalize_path(import_path)))
1023 else {
1024 continue;
1025 };
1026 if target.load_error.is_some() {
1027 continue;
1028 }
1029 for name in selective {
1030 let kind = if target.exports.contains(name) {
1031 continue;
1032 } else if target.declarations.contains_key(name) {
1033 SelectiveImportIssueKind::Private
1034 } else {
1035 SelectiveImportIssueKind::Missing
1036 };
1037 out.push(SelectiveImportIssue {
1038 name: name.clone(),
1039 module: import.raw_path.clone(),
1040 span: import.import_span,
1041 kind,
1042 });
1043 }
1044 }
1045 out.sort_by(|a, b| (&a.name, &a.module, a.kind).cmp(&(&b.name, &b.module, b.kind)));
1046 out.dedup();
1047 out
1048 }
1049
1050 pub fn exported_kind(&self, file: &Path, name: &str) -> Option<DefKind> {
1054 self.exported_kind_inner(file, name, &mut HashSet::new())
1055 }
1056
1057 fn exported_kind_inner(
1058 &self,
1059 file: &Path,
1060 name: &str,
1061 visited: &mut HashSet<PathBuf>,
1062 ) -> Option<DefKind> {
1063 let file = normalize_path(file);
1064 if !visited.insert(file.clone()) {
1065 return None;
1066 }
1067 let result = self.modules.get(&file).and_then(|module| {
1068 if module.own_exports.contains(name) {
1069 return module
1070 .declarations
1071 .get(name)
1072 .map(|definition| definition.kind)
1073 .or_else(|| {
1074 stdlib_module_from_path(&file).and_then(|stdlib_module| {
1075 stdlib::builtin_reexports(stdlib_module)
1076 .contains(&name)
1077 .then_some(DefKind::Function)
1078 })
1079 });
1080 }
1081 if let Some(sources) = module.selective_re_exports.get(name) {
1082 for source in sources {
1083 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1084 return Some(kind);
1085 }
1086 }
1087 }
1088 for source in &module.wildcard_re_export_paths {
1089 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1090 return Some(kind);
1091 }
1092 }
1093 None
1094 });
1095 visited.remove(&file);
1096 result
1097 }
1098}
1099
1100#[derive(Debug, Clone, PartialEq, Eq)]
1103pub struct ReExportConflict {
1104 pub name: String,
1105 pub sources: Vec<PathBuf>,
1106}
1107
1108#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1110pub enum SelectiveImportIssueKind {
1111 Missing,
1113 Private,
1115}
1116
1117#[derive(Debug, Clone, PartialEq, Eq)]
1119pub struct SelectiveImportIssue {
1120 pub name: String,
1122 pub module: String,
1124 pub span: Span,
1126 pub kind: SelectiveImportIssueKind,
1128}
1129
1130impl SelectiveImportIssue {
1131 #[must_use]
1133 pub fn message(&self) -> String {
1134 match self.kind {
1135 SelectiveImportIssueKind::Missing => format!(
1136 "imported symbol `{}` does not exist in `{}`",
1137 self.name, self.module
1138 ),
1139 SelectiveImportIssueKind::Private => format!(
1140 "imported symbol `{}` is not exported by `{}` — it is defined there but not `pub`",
1141 self.name, self.module
1142 ),
1143 }
1144 }
1145
1146 #[must_use]
1148 pub fn help(&self) -> String {
1149 match self.kind {
1150 SelectiveImportIssueKind::Missing => format!(
1151 "update the import to a symbol exported by `{}`",
1152 self.module
1153 ),
1154 SelectiveImportIssueKind::Private => {
1155 format!(
1156 "mark `{}` as `pub` in `{}` to export it",
1157 self.name, self.module
1158 )
1159 }
1160 }
1161 }
1162}
1163
1164fn load_module(
1165 path: &Path,
1166 package_snapshots: &[PackageSnapshot],
1167 source_overrides: Option<&HashMap<PathBuf, String>>,
1168) -> (ModuleInfo, Option<ParsedModuleSource>) {
1169 let source = source_overrides
1170 .and_then(|overrides| overrides.get(&normalize_path(path)).cloned())
1171 .or_else(|| read_module_source(path));
1172 let Some(source) = source else {
1173 return (ModuleInfo::default(), None);
1174 };
1175 let mut lexer = harn_lexer::Lexer::new(&source);
1176 let tokens = match lexer.tokenize() {
1177 Ok(tokens) => tokens,
1178 Err(error) => {
1179 let module = ModuleInfo {
1180 load_error: Some(ModuleLoadError {
1181 message: error.to_string(),
1182 span: error.span(),
1183 }),
1184 ..ModuleInfo::default()
1185 };
1186 return (module, None);
1187 }
1188 };
1189 let mut parser = Parser::new(tokens);
1190 let program = match parser.parse() {
1191 Ok(program) => program,
1192 Err(error) => {
1193 let module = ModuleInfo {
1194 load_error: Some(ModuleLoadError {
1195 message: error.to_string(),
1196 span: error.span(),
1197 }),
1198 ..ModuleInfo::default()
1199 };
1200 return (module, None);
1201 }
1202 };
1203
1204 let mut module = ModuleInfo::default();
1205 for node in &program {
1206 collect_module_info(path, node, &mut module, package_snapshots);
1207 collect_type_declarations(node, &mut module.type_declarations);
1208 collect_callable_declarations(node, &mut module.callable_declarations);
1209 }
1210 if let Some(stdlib_module) = stdlib_module_from_path(path) {
1211 module.own_exports.extend(
1212 stdlib::builtin_reexports(stdlib_module)
1213 .iter()
1214 .map(|name| (*name).to_string()),
1215 );
1216 }
1217 module.exports.extend(module.own_exports.iter().cloned());
1221 module
1222 .exports
1223 .extend(module.selective_re_exports.keys().cloned());
1224 let parsed = ParsedModuleSource { source, program };
1225 (module, Some(parsed))
1226}
1227
1228fn stdlib_module_from_path(path: &Path) -> Option<&str> {
1231 let s = path.to_str()?;
1232 s.strip_prefix("<std>/")
1233}
1234
1235fn collect_module_info(
1236 file: &Path,
1237 snode: &SNode,
1238 module: &mut ModuleInfo,
1239 package_snapshots: &[PackageSnapshot],
1240) {
1241 if let Node::AttributedDecl { inner, .. } = &snode.node {
1242 collect_module_info(file, inner, module, package_snapshots);
1243 return;
1244 }
1245
1246 for public in public_declarations(snode) {
1247 module.own_exports.insert(public.name);
1248 }
1249
1250 match &snode.node {
1251 Node::FnDecl { name, params, .. } => {
1252 module.declarations.insert(
1253 name.clone(),
1254 decl_site(file, snode.span, name, DefKind::Function),
1255 );
1256 for param_name in params.iter().map(|param| param.name.clone()) {
1257 module.declarations.insert(
1258 param_name.clone(),
1259 decl_site(file, snode.span, ¶m_name, DefKind::Parameter),
1260 );
1261 }
1262 }
1263 Node::Pipeline { name, .. } => {
1264 module.declarations.insert(
1265 name.clone(),
1266 decl_site(file, snode.span, name, DefKind::Pipeline),
1267 );
1268 }
1269 Node::ToolDecl { name, .. } => {
1270 module.declarations.insert(
1271 name.clone(),
1272 decl_site(file, snode.span, name, DefKind::Tool),
1273 );
1274 }
1275 Node::SkillDecl { name, .. } => {
1276 module.declarations.insert(
1277 name.clone(),
1278 decl_site(file, snode.span, name, DefKind::Skill),
1279 );
1280 }
1281 Node::EvalPackDecl { binding_name, .. } => {
1282 module.declarations.insert(
1283 binding_name.clone(),
1284 decl_site(file, snode.span, binding_name, DefKind::EvalPack),
1285 );
1286 }
1287 Node::StructDecl { name, .. } => {
1288 module.declarations.insert(
1289 name.clone(),
1290 decl_site(file, snode.span, name, DefKind::Struct),
1291 );
1292 }
1293 Node::EnumDecl { name, .. } => {
1294 module.declarations.insert(
1295 name.clone(),
1296 decl_site(file, snode.span, name, DefKind::Enum),
1297 );
1298 }
1299 Node::InterfaceDecl { name, .. } => {
1300 module.declarations.insert(
1301 name.clone(),
1302 decl_site(file, snode.span, name, DefKind::Interface),
1303 );
1304 }
1305 Node::TypeDecl { name, .. } => {
1306 module.declarations.insert(
1307 name.clone(),
1308 decl_site(file, snode.span, name, DefKind::Type),
1309 );
1310 }
1311 Node::LetBinding { pattern, .. } | Node::ConstBinding { pattern, .. } => {
1312 for name in pattern_names(pattern) {
1313 module.declarations.insert(
1314 name.clone(),
1315 decl_site(file, snode.span, &name, DefKind::Variable),
1316 );
1317 }
1318 }
1319 Node::ImportDecl { path, is_pub } => {
1320 let import_path = resolve_import_path_with_snapshots(file, path, package_snapshots);
1321 if import_path.is_none() {
1322 module.has_unresolved_wildcard_import = true;
1323 }
1324 if *is_pub {
1325 if let Some(resolved) = &import_path {
1326 module
1327 .wildcard_re_export_paths
1328 .push(normalize_path(resolved));
1329 }
1330 }
1331 module.imports.push(ImportRef {
1332 raw_path: path.clone(),
1333 path: import_path,
1334 selective_names: None,
1335 import_span: snode.span,
1336 });
1337 }
1338 Node::SelectiveImport {
1339 names,
1340 path,
1341 is_pub,
1342 } => {
1343 let import_path = resolve_import_path_with_snapshots(file, path, package_snapshots);
1344 if import_path.is_none() {
1345 module.has_unresolved_selective_import = true;
1346 }
1347 if *is_pub {
1348 if let Some(resolved) = &import_path {
1349 let canonical = normalize_path(resolved);
1350 for name in names {
1351 module
1352 .selective_re_exports
1353 .entry(name.clone())
1354 .or_default()
1355 .push(canonical.clone());
1356 }
1357 }
1358 }
1359 let names: HashSet<String> = names.iter().cloned().collect();
1360 module.selective_import_names.extend(names.iter().cloned());
1361 module.imports.push(ImportRef {
1362 raw_path: path.clone(),
1363 path: import_path,
1364 selective_names: Some(names),
1365 import_span: snode.span,
1366 });
1367 }
1368 _ => {}
1369 }
1370}
1371
1372fn collect_type_declarations(snode: &SNode, decls: &mut Vec<SNode>) {
1373 match &snode.node {
1374 Node::TypeDecl { .. }
1375 | Node::StructDecl { .. }
1376 | Node::EnumDecl { .. }
1377 | Node::InterfaceDecl { .. } => decls.push(snode.clone()),
1378 Node::AttributedDecl { inner, .. } => collect_type_declarations(inner, decls),
1379 _ => {}
1380 }
1381}
1382
1383fn collect_callable_declarations(snode: &SNode, decls: &mut Vec<SNode>) {
1384 match &snode.node {
1385 Node::FnDecl { .. } | Node::Pipeline { .. } | Node::ToolDecl { .. } => {
1386 decls.push(snode.clone());
1387 }
1388 Node::AttributedDecl { inner, .. } => collect_callable_declarations(inner, decls),
1389 _ => {}
1390 }
1391}
1392
1393fn type_decl_name(snode: &SNode) -> Option<&str> {
1394 match &snode.node {
1395 Node::TypeDecl { name, .. }
1396 | Node::StructDecl { name, .. }
1397 | Node::EnumDecl { name, .. }
1398 | Node::InterfaceDecl { name, .. } => Some(name.as_str()),
1399 _ => None,
1400 }
1401}
1402
1403fn callable_decl_name(snode: &SNode) -> Option<&str> {
1404 match &snode.node {
1405 Node::FnDecl { name, .. } | Node::Pipeline { name, .. } | Node::ToolDecl { name, .. } => {
1406 Some(name.as_str())
1407 }
1408 Node::AttributedDecl { inner, .. } => callable_decl_name(inner),
1409 _ => None,
1410 }
1411}
1412
1413fn decl_site(file: &Path, span: Span, name: &str, kind: DefKind) -> DefSite {
1414 DefSite {
1415 name: name.to_string(),
1416 file: file.to_path_buf(),
1417 kind,
1418 span,
1419 }
1420}
1421
1422fn normalize_path(path: &Path) -> PathBuf {
1423 canonical_path(path)
1424}
1425
1426pub fn canonical_path(path: &Path) -> PathBuf {
1439 use std::sync::OnceLock;
1440 if stdlib_module_from_path(path).is_some() {
1441 return path.to_path_buf();
1442 }
1443 static MEMO: OnceLock<std::sync::Mutex<HashMap<PathBuf, PathBuf>>> = OnceLock::new();
1444 let memo = MEMO.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
1445 if let Some(hit) = memo
1446 .lock()
1447 .expect("canonical path memo lock poisoned")
1448 .get(path)
1449 .cloned()
1450 {
1451 return hit;
1452 }
1453 match path.canonicalize() {
1454 Ok(canonical) => {
1455 memo.lock()
1456 .expect("canonical path memo lock poisoned")
1457 .insert(path.to_path_buf(), canonical.clone());
1458 canonical
1459 }
1460 Err(_) => path.to_path_buf(),
1461 }
1462}
1463
1464#[cfg(test)]
1465#[path = "tests.rs"]
1466mod tests;