1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4use crate::package_imports::acquire_package_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;
12mod import_recording;
13pub mod manifest_walk;
14mod namespace_imports;
15pub mod package_execution;
16mod package_imports;
17pub mod package_snapshot;
18pub mod personas;
19pub mod project_config;
20mod stdlib;
21
22use declarations::pattern_names;
23pub use declarations::{public_declarations, DefKind, PublicDeclaration};
24pub use namespace_imports::NamespaceImportInfo;
25pub use package_imports::{
26 resolve_import_path, resolve_import_path_with_guard, resolve_import_path_with_snapshot,
27};
28
29#[derive(Debug, Clone)]
31pub struct DefSite {
32 pub name: String,
33 pub file: PathBuf,
34 pub kind: DefKind,
35 pub span: Span,
36}
37
38#[derive(Debug, Clone)]
40pub enum WildcardResolution {
41 Resolved(HashSet<String>),
43 Unknown,
45}
46
47#[derive(Debug, Default)]
49pub struct ModuleGraph {
50 modules: HashMap<PathBuf, ModuleInfo>,
51 _package_snapshots: Vec<PackageSnapshot>,
53}
54
55#[derive(Debug, Clone)]
56pub struct ParsedModuleSource {
57 pub source: String,
58 pub program: Vec<SNode>,
59}
60
61#[derive(Debug, Default)]
62pub struct ModuleGraphBuild {
63 pub graph: ModuleGraph,
64 pub parsed_sources: HashMap<PathBuf, ParsedModuleSource>,
65}
66
67#[derive(Debug, Default)]
68struct ModuleInfo {
69 declarations: HashMap<String, DefSite>,
72 exports: HashSet<String>,
77 own_exports: HashSet<String>,
80 selective_re_exports: HashMap<String, Vec<PathBuf>>,
87 wildcard_re_export_paths: Vec<PathBuf>,
91 namespace_re_exports: HashMap<String, PathBuf>,
98 selective_import_names: HashSet<String>,
100 imports: Vec<ImportRef>,
102 has_unresolved_wildcard_import: bool,
104 has_unresolved_selective_import: bool,
108 has_unresolved_namespace_import: bool,
110 type_declarations: Vec<SNode>,
113 callable_declarations: Vec<SNode>,
116 load_error: Option<ModuleLoadError>,
122}
123
124#[derive(Debug, Clone)]
130pub struct ModuleLoadError {
131 pub message: String,
133 pub span: Span,
135}
136
137#[derive(Debug, Clone)]
140pub struct ImportCompileFailure {
141 pub import_raw_path: String,
143 pub import_span: Span,
145 pub module_path: PathBuf,
147 pub error: ModuleLoadError,
149}
150
151#[derive(Debug, Clone)]
152struct ImportRef {
153 raw_path: String,
154 path: Option<PathBuf>,
155 selective_names: Option<HashSet<String>>,
156 namespace_alias: Option<String>,
159 import_span: Span,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct ModuleImport {
165 pub raw_path: String,
167 pub resolved_path: Option<PathBuf>,
169 pub selective_names: Option<Vec<String>>,
171 pub namespace_alias: Option<String>,
173}
174
175pub fn read_module_source(path: &Path) -> Option<String> {
181 if let Some(stdlib_module) = stdlib_module_from_path(path) {
182 return stdlib::get_stdlib_source(stdlib_module).map(ToString::to_string);
183 }
184 std::fs::read_to_string(path).ok()
185}
186
187pub fn build(files: &[PathBuf]) -> ModuleGraph {
193 build_inner(files, None, None).graph
194}
195
196pub fn build_with_source(file: &Path, source: &str) -> ModuleGraph {
202 let file = normalize_path(file);
203 let source_overrides = HashMap::from([(file.clone(), source.to_string())]);
204 build_inner(&[file], None, Some(&source_overrides)).graph
205}
206
207pub fn build_with_parsed_sources(files: &[PathBuf]) -> ModuleGraphBuild {
213 let parsed_source_targets = files.iter().map(|file| normalize_path(file)).collect();
214 build_inner(files, Some(&parsed_source_targets), None)
215}
216
217fn build_inner(
218 files: &[PathBuf],
219 parsed_source_targets: Option<&HashSet<PathBuf>>,
220 source_overrides: Option<&HashMap<PathBuf, String>>,
221) -> ModuleGraphBuild {
222 let package_snapshots = acquire_package_snapshots(files);
223 let mut modules: HashMap<PathBuf, ModuleInfo> = HashMap::new();
224 let mut parsed_sources: HashMap<PathBuf, ParsedModuleSource> = HashMap::new();
225 let mut seen: HashSet<PathBuf> = HashSet::new();
226 let mut wave: Vec<PathBuf> = Vec::new();
227 for file in files {
228 let canonical = normalize_path(file);
229 if seen.insert(canonical.clone()) {
230 wave.push(canonical);
231 }
232 }
233 while !wave.is_empty() {
241 let loaded = load_wave(&wave, &package_snapshots, source_overrides);
242 let mut next_wave: Vec<PathBuf> = Vec::new();
243 for (path, (module, parsed)) in wave.drain(..).zip(loaded) {
244 let retain_parsed_source =
245 parsed_source_targets.is_some_and(|targets| targets.contains(&path));
246 if retain_parsed_source {
247 if let Some(parsed) = parsed {
248 parsed_sources.insert(path.clone(), parsed);
249 }
250 }
251 for import in &module.imports {
268 if let Some(import_path) = &import.path {
269 let canonical = normalize_path(import_path);
270 if seen.insert(canonical.clone()) {
271 next_wave.push(canonical);
272 }
273 }
274 }
275 modules.insert(path, module);
276 }
277 wave = next_wave;
278 }
279 resolve_re_exports(&mut modules);
280 ModuleGraphBuild {
281 graph: ModuleGraph {
282 modules,
283 _package_snapshots: package_snapshots,
284 },
285 parsed_sources,
286 }
287}
288
289pub const MODULE_GRAPH_JOBS_ENV: &str = "HARN_MODULE_GRAPH_JOBS";
292
293fn load_wave(
296 paths: &[PathBuf],
297 package_snapshots: &[PackageSnapshot],
298 source_overrides: Option<&HashMap<PathBuf, String>>,
299) -> Vec<(ModuleInfo, Option<ParsedModuleSource>)> {
300 const MIN_PARALLEL_WAVE: usize = 8;
301 let configured = std::env::var(MODULE_GRAPH_JOBS_ENV)
302 .ok()
303 .and_then(|value| value.parse::<usize>().ok())
304 .filter(|&jobs| jobs > 0);
305 let workers = configured
306 .unwrap_or_else(|| {
307 std::thread::available_parallelism()
308 .map(std::num::NonZeroUsize::get)
309 .unwrap_or(1)
310 })
311 .min(paths.len());
312 if workers <= 1 || paths.len() < MIN_PARALLEL_WAVE {
313 return paths
314 .iter()
315 .map(|path| load_module(path, package_snapshots, source_overrides))
316 .collect();
317 }
318 let next = std::sync::atomic::AtomicUsize::new(0);
319 let mut produced: Vec<(usize, (ModuleInfo, Option<ParsedModuleSource>))> =
320 std::thread::scope(|scope| {
321 let handles: Vec<_> = (0..workers)
322 .map(|_| {
323 scope.spawn(|| {
324 let mut local = Vec::new();
325 loop {
326 let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
327 let Some(path) = paths.get(index) else {
328 break;
329 };
330 local.push((
331 index,
332 load_module(path, package_snapshots, source_overrides),
333 ));
334 }
335 local
336 })
337 })
338 .collect();
339 handles
340 .into_iter()
341 .flat_map(|handle| match handle.join() {
342 Ok(local) => local,
343 Err(panic) => std::panic::resume_unwind(panic),
344 })
345 .collect()
346 });
347 produced.sort_unstable_by_key(|(index, _)| *index);
348 produced.into_iter().map(|(_, loaded)| loaded).collect()
349}
350
351fn resolve_re_exports(modules: &mut HashMap<PathBuf, ModuleInfo>) {
356 let keys: Vec<PathBuf> = modules.keys().cloned().collect();
357 loop {
358 let mut changed = false;
359 for path in &keys {
360 let wildcard_paths = modules
363 .get(path)
364 .map(|m| m.wildcard_re_export_paths.clone())
365 .unwrap_or_default();
366 if wildcard_paths.is_empty() {
367 continue;
368 }
369 let mut additions: Vec<String> = Vec::new();
370 for src in &wildcard_paths {
371 let src_canonical = normalize_path(src);
372 if let Some(src_module) = modules.get(src).or_else(|| modules.get(&src_canonical)) {
373 additions.extend(src_module.exports.iter().cloned());
374 }
375 }
376 if let Some(module) = modules.get_mut(path) {
377 for name in additions {
378 if module.exports.insert(name) {
379 changed = true;
380 }
381 }
382 }
383 }
384 if !changed {
385 break;
386 }
387 }
388}
389
390impl ModuleGraph {
391 pub fn module_paths(&self) -> Vec<PathBuf> {
396 let mut paths: Vec<PathBuf> = self.modules.keys().cloned().collect();
397 paths.sort();
398 paths
399 }
400
401 pub fn contains_module(&self, path: &Path) -> bool {
404 self.modules.contains_key(path) || self.modules.contains_key(&normalize_path(path))
405 }
406
407 pub fn all_selective_import_names(&self) -> HashSet<&str> {
409 let mut names = HashSet::new();
410 for module in self.modules.values() {
411 for name in &module.selective_import_names {
412 names.insert(name.as_str());
413 }
414 }
415 names
416 }
417
418 pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
421 let target = normalize_path(target);
422 let mut out: Vec<PathBuf> = self
423 .modules
424 .iter()
425 .filter(|(_, info)| {
426 info.imports.iter().any(|import| {
427 import
428 .path
429 .as_ref()
430 .is_some_and(|p| normalize_path(p) == target)
431 })
432 })
433 .map(|(path, _)| path.clone())
434 .collect();
435 out.sort();
436 out
437 }
438
439 pub fn imports_for_module(&self, file: &Path) -> Vec<ModuleImport> {
441 let file = normalize_path(file);
442 let Some(module) = self.modules.get(&file) else {
443 return Vec::new();
444 };
445 let mut imports: Vec<ModuleImport> = module
446 .imports
447 .iter()
448 .map(|import| {
449 let mut selective_names = import
450 .selective_names
451 .as_ref()
452 .map(|names| names.iter().cloned().collect::<Vec<_>>());
453 if let Some(names) = selective_names.as_mut() {
454 names.sort();
455 }
456 ModuleImport {
457 raw_path: import.raw_path.clone(),
458 resolved_path: import.path.as_ref().map(|path| normalize_path(path)),
459 selective_names,
460 namespace_alias: import.namespace_alias.clone(),
461 }
462 })
463 .collect();
464 imports.sort_by(|left, right| {
465 left.raw_path
466 .cmp(&right.raw_path)
467 .then_with(|| left.selective_names.cmp(&right.selective_names))
468 .then_with(|| left.namespace_alias.cmp(&right.namespace_alias))
469 .then_with(|| left.resolved_path.cmp(&right.resolved_path))
470 });
471 imports
472 }
473
474 pub fn exports_for_module(&self, file: &Path) -> Vec<String> {
476 let file = normalize_path(file);
477 let Some(module) = self.modules.get(&file) else {
478 return Vec::new();
479 };
480 let mut exports: Vec<String> = module.exports.iter().cloned().collect();
481 exports.sort();
482 exports
483 }
484
485 pub fn wildcard_exports_for(&self, file: &Path) -> WildcardResolution {
490 let file = normalize_path(file);
491 let Some(module) = self.modules.get(&file) else {
492 return WildcardResolution::Unknown;
493 };
494 if module.has_unresolved_wildcard_import {
495 return WildcardResolution::Unknown;
496 }
497
498 let mut names = HashSet::new();
499 for import in module
500 .imports
501 .iter()
502 .filter(|import| import.selective_names.is_none())
503 {
504 let Some(import_path) = &import.path else {
505 return WildcardResolution::Unknown;
506 };
507 let imported = self.modules.get(import_path).or_else(|| {
508 let normalized = normalize_path(import_path);
509 self.modules.get(&normalized)
510 });
511 let Some(imported) = imported else {
512 return WildcardResolution::Unknown;
513 };
514 names.extend(imported.exports.iter().cloned());
515 }
516 WildcardResolution::Resolved(names)
517 }
518
519 #[must_use]
540 pub fn import_compile_failures(&self, file: &Path) -> Vec<ImportCompileFailure> {
541 let file = normalize_path(file);
542 let Some(module) = self.modules.get(&file) else {
543 return Vec::new();
544 };
545 let mut failures = Vec::new();
546 for import in &module.imports {
547 let Some(import_path) = &import.path else {
548 continue;
549 };
550 let Some(target) = self
551 .modules
552 .get(import_path)
553 .or_else(|| self.modules.get(&normalize_path(import_path)))
554 else {
555 continue;
556 };
557 if let Some(error) = &target.load_error {
558 failures.push(ImportCompileFailure {
559 import_raw_path: import.raw_path.clone(),
560 import_span: import.import_span,
561 module_path: normalize_path(import_path),
562 error: error.clone(),
563 });
564 }
565 }
566 failures
567 }
568
569 pub fn imported_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
570 let file = normalize_path(file);
571 let module = self.modules.get(&file)?;
572 if module.has_unresolved_wildcard_import
573 || module.has_unresolved_selective_import
574 || module.has_unresolved_namespace_import
575 {
576 return None;
577 }
578
579 let mut names = HashSet::new();
580 for import in &module.imports {
581 if let Some(alias) = &import.namespace_alias {
583 names.insert(alias.clone());
584 continue;
585 }
586 let import_path = import.path.as_ref()?;
587 let imported = self
588 .modules
589 .get(import_path)
590 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
591 if imported.load_error.is_some() {
597 return None;
598 }
599 match &import.selective_names {
600 None => {
601 names.extend(imported.exports.iter().cloned());
602 }
603 Some(selective) => {
604 for name in selective {
613 if imported.declarations.contains_key(name)
614 || imported.exports.contains(name)
615 {
616 names.insert(name.clone());
617 }
618 }
619 }
620 }
621 }
622 Some(names)
623 }
624
625 pub fn imported_names_by_kind_for_file(
630 &self,
631 file: &Path,
632 kind: DefKind,
633 ) -> Option<HashSet<String>> {
634 let file = normalize_path(file);
635 let module = self.modules.get(&file)?;
636 if module.has_unresolved_wildcard_import
637 || module.has_unresolved_selective_import
638 || module.has_unresolved_namespace_import
639 {
640 return None;
641 }
642
643 let mut names = HashSet::new();
644 for import in &module.imports {
645 if import.namespace_alias.is_some() {
647 continue;
648 }
649 let import_path = import.path.as_ref()?;
650 let imported_names: Vec<String> = match &import.selective_names {
651 Some(selective) => selective.iter().cloned().collect(),
652 None => self
653 .modules
654 .get(import_path)
655 .or_else(|| self.modules.get(&normalize_path(import_path)))?
656 .exports
657 .iter()
658 .cloned()
659 .collect(),
660 };
661 for name in imported_names {
662 if self.exported_kind(import_path, &name) == Some(kind) {
663 names.insert(name);
664 }
665 }
666 }
667 Some(names)
668 }
669
670 pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
674 let file = normalize_path(file);
675 let module = self.modules.get(&file)?;
676 if module.has_unresolved_wildcard_import
677 || module.has_unresolved_selective_import
678 || module.has_unresolved_namespace_import
679 {
680 return None;
681 }
682
683 let mut decls = Vec::new();
684 for import in &module.imports {
685 if import.namespace_alias.is_some() {
687 continue;
688 }
689 let import_path = import.path.as_ref()?;
690 let imported = self
691 .modules
692 .get(import_path)
693 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
694 if imported.load_error.is_some() {
700 return None;
701 }
702 let names_to_collect: Vec<String> = match &import.selective_names {
703 None => imported.exports.iter().cloned().collect(),
704 Some(selective) => selective.iter().cloned().collect(),
705 };
706 for name in &names_to_collect {
707 let mut visited = HashSet::new();
708 if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
709 decls.push(decl);
710 }
711 }
712 for ty_decl in &imported.type_declarations {
722 if type_decl_name(ty_decl).is_some() {
723 decls.push(ty_decl.clone());
724 }
725 }
726 }
727 Some(decls)
728 }
729
730 pub fn imported_callable_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
734 let file = normalize_path(file);
735 let module = self.modules.get(&file)?;
736 if module.has_unresolved_wildcard_import
737 || module.has_unresolved_selective_import
738 || module.has_unresolved_namespace_import
739 {
740 return None;
741 }
742
743 let mut decls = Vec::new();
744 for import in &module.imports {
745 if import.namespace_alias.is_some() {
747 continue;
748 }
749 let import_path = import.path.as_ref()?;
750 let imported = self
751 .modules
752 .get(import_path)
753 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
754 if imported.load_error.is_some() {
760 return None;
761 }
762 let selective_import = import.selective_names.is_some();
763 let names_to_collect: Vec<String> = match &import.selective_names {
764 None => imported.exports.iter().cloned().collect(),
765 Some(selective) => selective.iter().cloned().collect(),
766 };
767 for name in &names_to_collect {
768 if selective_import || imported.own_exports.contains(name) {
769 if let Some(decl) = imported
770 .callable_declarations
771 .iter()
772 .find(|decl| callable_decl_name(decl) == Some(name.as_str()))
773 {
774 decls.push(decl.clone());
775 continue;
776 }
777 }
778 let mut visited = HashSet::new();
779 if let Some(decl) =
780 self.find_exported_callable_decl(import_path, name, &mut visited)
781 {
782 decls.push(decl);
783 }
784 }
785 }
786 Some(decls)
787 }
788
789 fn find_exported_type_decl(
792 &self,
793 path: &Path,
794 name: &str,
795 visited: &mut HashSet<PathBuf>,
796 ) -> Option<SNode> {
797 let canonical = normalize_path(path);
798 if !visited.insert(canonical.clone()) {
799 return None;
800 }
801 let module = self
802 .modules
803 .get(&canonical)
804 .or_else(|| self.modules.get(path))?;
805 for decl in &module.type_declarations {
806 if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
807 return Some(decl.clone());
808 }
809 }
810 if let Some(sources) = module.selective_re_exports.get(name) {
811 for source in sources {
812 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
813 return Some(decl);
814 }
815 }
816 }
817 for source in &module.wildcard_re_export_paths {
818 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
819 return Some(decl);
820 }
821 }
822 None
823 }
824
825 fn find_exported_callable_decl(
826 &self,
827 path: &Path,
828 name: &str,
829 visited: &mut HashSet<PathBuf>,
830 ) -> Option<SNode> {
831 let canonical = normalize_path(path);
832 if !visited.insert(canonical.clone()) {
833 return None;
834 }
835 let module = self
836 .modules
837 .get(&canonical)
838 .or_else(|| self.modules.get(path))?;
839 for decl in &module.callable_declarations {
840 if callable_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
841 return Some(decl.clone());
842 }
843 }
844 if let Some(sources) = module.selective_re_exports.get(name) {
845 for source in sources {
846 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
847 return Some(decl);
848 }
849 }
850 }
851 for source in &module.wildcard_re_export_paths {
852 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
853 return Some(decl);
854 }
855 }
856 None
857 }
858
859 pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
865 let mut visited = HashSet::new();
866 self.definition_of_inner(file, name, &mut visited)
867 }
868
869 pub fn export_definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
876 let mut visited = HashSet::new();
877 self.export_definition_of_inner(file, name, &mut visited)
878 }
879
880 pub fn declared_names_for_file(&self, file: &Path) -> Option<Vec<&str>> {
884 let module = self.modules.get(&normalize_path(file))?;
885 let mut names: Vec<&str> = module.declarations.keys().map(String::as_str).collect();
886 names.sort_unstable();
887 Some(names)
888 }
889
890 fn definition_of_inner(
891 &self,
892 file: &Path,
893 name: &str,
894 visited: &mut HashSet<PathBuf>,
895 ) -> Option<DefSite> {
896 let file = normalize_path(file);
897 if !visited.insert(file.clone()) {
898 return None;
899 }
900 let current = self.modules.get(&file)?;
901
902 if let Some(local) = current.declarations.get(name) {
903 return Some(local.clone());
904 }
905
906 if let Some(sources) = current.selective_re_exports.get(name) {
911 for source in sources {
912 if let Some(def) = self.definition_of_inner(source, name, visited) {
913 return Some(def);
914 }
915 }
916 }
917
918 for source in ¤t.wildcard_re_export_paths {
920 if let Some(def) = self.definition_of_inner(source, name, visited) {
921 return Some(def);
922 }
923 }
924
925 for import in ¤t.imports {
927 let Some(selective_names) = &import.selective_names else {
928 continue;
929 };
930 if !selective_names.contains(name) {
931 continue;
932 }
933 if let Some(path) = &import.path {
934 if let Some(def) = self.definition_of_inner(path, name, visited) {
935 return Some(def);
936 }
937 }
938 }
939
940 for import in ¤t.imports {
942 if import.selective_names.is_some() || import.namespace_alias.is_some() {
943 continue;
944 }
945 if let Some(path) = &import.path {
946 if let Some(def) = self.definition_of_inner(path, name, visited) {
947 return Some(def);
948 }
949 }
950 }
951
952 None
953 }
954
955 fn export_definition_of_inner(
956 &self,
957 file: &Path,
958 name: &str,
959 visited: &mut HashSet<PathBuf>,
960 ) -> Option<DefSite> {
961 let file = normalize_path(file);
962 if !visited.insert(file.clone()) {
963 return None;
964 }
965 let current = self.modules.get(&file)?;
966
967 if current.own_exports.contains(name) {
968 if let Some(local) = current.declarations.get(name) {
969 return Some(local.clone());
970 }
971 }
972 if let Some(sources) = current.selective_re_exports.get(name) {
973 for source in sources {
974 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
975 return Some(definition);
976 }
977 }
978 }
979 for source in ¤t.wildcard_re_export_paths {
980 if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
981 return Some(definition);
982 }
983 }
984 None
985 }
986
987 pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
991 let file = normalize_path(file);
992 let Some(module) = self.modules.get(&file) else {
993 return Vec::new();
994 };
995
996 let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
1000
1001 for (name, srcs) in &module.selective_re_exports {
1002 sources
1003 .entry(name.clone())
1004 .or_default()
1005 .extend(srcs.iter().cloned());
1006 }
1007 for src in &module.wildcard_re_export_paths {
1008 let canonical = normalize_path(src);
1009 let Some(src_module) = self
1010 .modules
1011 .get(&canonical)
1012 .or_else(|| self.modules.get(src))
1013 else {
1014 continue;
1015 };
1016 for name in &src_module.exports {
1017 sources
1018 .entry(name.clone())
1019 .or_default()
1020 .push(canonical.clone());
1021 }
1022 }
1023
1024 for name in &module.own_exports {
1028 if let Some(entry) = sources.get_mut(name) {
1029 entry.push(file.clone());
1030 }
1031 }
1032
1033 let mut conflicts = Vec::new();
1034 for (name, mut srcs) in sources {
1035 srcs.sort();
1036 srcs.dedup();
1037 if srcs.len() > 1 {
1038 conflicts.push(ReExportConflict {
1039 name,
1040 sources: srcs,
1041 });
1042 }
1043 }
1044 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
1045 conflicts
1046 }
1047
1048 pub fn selective_import_issues(&self, file: &Path) -> Vec<SelectiveImportIssue> {
1054 let file = normalize_path(file);
1055 let Some(module) = self.modules.get(&file) else {
1056 return Vec::new();
1057 };
1058
1059 let mut out = Vec::new();
1060 for import in &module.imports {
1061 let Some(selective) = &import.selective_names else {
1062 continue;
1063 };
1064 let Some(import_path) = &import.path else {
1065 continue;
1066 };
1067 let Some(target) = self
1068 .modules
1069 .get(import_path)
1070 .or_else(|| self.modules.get(&normalize_path(import_path)))
1071 else {
1072 continue;
1073 };
1074 if target.load_error.is_some() {
1075 continue;
1076 }
1077 for name in selective {
1078 let kind = if target.exports.contains(name) {
1079 continue;
1080 } else if target.declarations.contains_key(name) {
1081 SelectiveImportIssueKind::Private
1082 } else {
1083 SelectiveImportIssueKind::Missing
1084 };
1085 out.push(SelectiveImportIssue {
1086 name: name.clone(),
1087 module: import.raw_path.clone(),
1088 span: import.import_span,
1089 kind,
1090 });
1091 }
1092 }
1093 out.sort_by(|a, b| (&a.name, &a.module, a.kind).cmp(&(&b.name, &b.module, b.kind)));
1094 out.dedup();
1095 out
1096 }
1097
1098 pub fn exported_kind(&self, file: &Path, name: &str) -> Option<DefKind> {
1102 self.exported_kind_inner(file, name, &mut HashSet::new())
1103 }
1104
1105 fn exported_kind_inner(
1106 &self,
1107 file: &Path,
1108 name: &str,
1109 visited: &mut HashSet<PathBuf>,
1110 ) -> Option<DefKind> {
1111 let file = normalize_path(file);
1112 if !visited.insert(file.clone()) {
1113 return None;
1114 }
1115 let result = self.modules.get(&file).and_then(|module| {
1116 if module.own_exports.contains(name) {
1117 return module
1118 .declarations
1119 .get(name)
1120 .map(|definition| definition.kind)
1121 .or_else(|| {
1122 stdlib_module_from_path(&file).and_then(|stdlib_module| {
1123 stdlib::builtin_reexports(stdlib_module)
1124 .contains(&name)
1125 .then_some(DefKind::Function)
1126 })
1127 });
1128 }
1129 if let Some(sources) = module.selective_re_exports.get(name) {
1130 for source in sources {
1131 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1132 return Some(kind);
1133 }
1134 }
1135 }
1136 for source in &module.wildcard_re_export_paths {
1137 if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1138 return Some(kind);
1139 }
1140 }
1141 None
1142 });
1143 visited.remove(&file);
1144 result
1145 }
1146}
1147
1148#[derive(Debug, Clone, PartialEq, Eq)]
1151pub struct ReExportConflict {
1152 pub name: String,
1153 pub sources: Vec<PathBuf>,
1154}
1155
1156#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1158pub enum SelectiveImportIssueKind {
1159 Missing,
1161 Private,
1163}
1164
1165#[derive(Debug, Clone, PartialEq, Eq)]
1167pub struct SelectiveImportIssue {
1168 pub name: String,
1170 pub module: String,
1172 pub span: Span,
1174 pub kind: SelectiveImportIssueKind,
1176}
1177
1178impl SelectiveImportIssue {
1179 #[must_use]
1181 pub fn message(&self) -> String {
1182 match self.kind {
1183 SelectiveImportIssueKind::Missing => format!(
1184 "imported symbol `{}` does not exist in `{}`",
1185 self.name, self.module
1186 ),
1187 SelectiveImportIssueKind::Private => format!(
1188 "imported symbol `{}` is not exported by `{}` — it is defined there but not `pub`",
1189 self.name, self.module
1190 ),
1191 }
1192 }
1193
1194 #[must_use]
1196 pub fn help(&self) -> String {
1197 match self.kind {
1198 SelectiveImportIssueKind::Missing => format!(
1199 "update the import to a symbol exported by `{}`",
1200 self.module
1201 ),
1202 SelectiveImportIssueKind::Private => {
1203 format!(
1204 "mark `{}` as `pub` in `{}` to export it",
1205 self.name, self.module
1206 )
1207 }
1208 }
1209 }
1210}
1211
1212fn load_module(
1213 path: &Path,
1214 package_snapshots: &[PackageSnapshot],
1215 source_overrides: Option<&HashMap<PathBuf, String>>,
1216) -> (ModuleInfo, Option<ParsedModuleSource>) {
1217 let source = source_overrides
1218 .and_then(|overrides| overrides.get(&normalize_path(path)).cloned())
1219 .or_else(|| read_module_source(path));
1220 let Some(source) = source else {
1221 return (ModuleInfo::default(), None);
1222 };
1223 let mut lexer = harn_lexer::Lexer::new(&source);
1224 let tokens = match lexer.tokenize() {
1225 Ok(tokens) => tokens,
1226 Err(error) => {
1227 let module = ModuleInfo {
1228 load_error: Some(ModuleLoadError {
1229 message: error.to_string(),
1230 span: error.span(),
1231 }),
1232 ..ModuleInfo::default()
1233 };
1234 return (module, None);
1235 }
1236 };
1237 let mut parser = Parser::new(tokens);
1238 let program = match parser.parse() {
1239 Ok(program) => program,
1240 Err(error) => {
1241 let module = ModuleInfo {
1242 load_error: Some(ModuleLoadError {
1243 message: error.to_string(),
1244 span: error.span(),
1245 }),
1246 ..ModuleInfo::default()
1247 };
1248 return (module, None);
1249 }
1250 };
1251
1252 let mut module = ModuleInfo::default();
1253 for node in &program {
1254 collect_module_info(path, node, &mut module, package_snapshots);
1255 collect_type_declarations(node, &mut module.type_declarations);
1256 collect_callable_declarations(node, &mut module.callable_declarations);
1257 }
1258 if let Some(stdlib_module) = stdlib_module_from_path(path) {
1259 module.own_exports.extend(
1260 stdlib::builtin_reexports(stdlib_module)
1261 .iter()
1262 .map(|name| (*name).to_string()),
1263 );
1264 }
1265 module.exports.extend(module.own_exports.iter().cloned());
1269 module
1270 .exports
1271 .extend(module.selective_re_exports.keys().cloned());
1272 let parsed = ParsedModuleSource { source, program };
1273 (module, Some(parsed))
1274}
1275
1276fn stdlib_module_from_path(path: &Path) -> Option<&str> {
1279 let s = path.to_str()?;
1280 s.strip_prefix("<std>/")
1281}
1282
1283fn collect_module_info(
1284 file: &Path,
1285 snode: &SNode,
1286 module: &mut ModuleInfo,
1287 package_snapshots: &[PackageSnapshot],
1288) {
1289 if let Node::AttributedDecl { inner, .. } = &snode.node {
1290 collect_module_info(file, inner, module, package_snapshots);
1291 return;
1292 }
1293
1294 for public in public_declarations(snode) {
1295 module.own_exports.insert(public.name);
1296 }
1297
1298 match &snode.node {
1299 Node::FnDecl { name, params, .. } => {
1300 module.declarations.insert(
1301 name.clone(),
1302 decl_site(file, snode.span, name, DefKind::Function),
1303 );
1304 for param_name in params.iter().map(|param| param.name.clone()) {
1305 module.declarations.insert(
1306 param_name.clone(),
1307 decl_site(file, snode.span, ¶m_name, DefKind::Parameter),
1308 );
1309 }
1310 }
1311 Node::Pipeline { name, .. } => {
1312 module.declarations.insert(
1313 name.clone(),
1314 decl_site(file, snode.span, name, DefKind::Pipeline),
1315 );
1316 }
1317 Node::ToolDecl { name, .. } => {
1318 module.declarations.insert(
1319 name.clone(),
1320 decl_site(file, snode.span, name, DefKind::Tool),
1321 );
1322 }
1323 Node::SkillDecl { name, .. } => {
1324 module.declarations.insert(
1325 name.clone(),
1326 decl_site(file, snode.span, name, DefKind::Skill),
1327 );
1328 }
1329 Node::EvalPackDecl { binding_name, .. } => {
1330 module.declarations.insert(
1331 binding_name.clone(),
1332 decl_site(file, snode.span, binding_name, DefKind::EvalPack),
1333 );
1334 }
1335 Node::StructDecl { name, .. } => {
1336 module.declarations.insert(
1337 name.clone(),
1338 decl_site(file, snode.span, name, DefKind::Struct),
1339 );
1340 }
1341 Node::EnumDecl { name, .. } => {
1342 module.declarations.insert(
1343 name.clone(),
1344 decl_site(file, snode.span, name, DefKind::Enum),
1345 );
1346 }
1347 Node::InterfaceDecl { name, .. } => {
1348 module.declarations.insert(
1349 name.clone(),
1350 decl_site(file, snode.span, name, DefKind::Interface),
1351 );
1352 }
1353 Node::TypeDecl { name, .. } => {
1354 module.declarations.insert(
1355 name.clone(),
1356 decl_site(file, snode.span, name, DefKind::Type),
1357 );
1358 }
1359 Node::LetBinding { pattern, .. } | Node::ConstBinding { pattern, .. } => {
1360 for name in pattern_names(pattern) {
1361 module.declarations.insert(
1362 name.clone(),
1363 decl_site(file, snode.span, &name, DefKind::Variable),
1364 );
1365 }
1366 }
1367 _ if import_recording::record_import_node(module, file, snode, package_snapshots) => {}
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;