1use std::collections::{HashMap, HashSet, VecDeque};
2use std::path::{Component, Path, PathBuf};
3
4use harn_lexer::Span;
5use harn_parser::{BindingPattern, Node, Parser, SNode};
6use serde::Deserialize;
7
8pub mod asset_paths;
9pub mod fingerprint;
10pub mod personas;
11mod stdlib;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum DefKind {
16 Function,
17 Pipeline,
18 Tool,
19 Skill,
20 Struct,
21 Enum,
22 Interface,
23 Type,
24 Variable,
25 Parameter,
26}
27
28#[derive(Debug, Clone)]
30pub struct DefSite {
31 pub name: String,
32 pub file: PathBuf,
33 pub kind: DefKind,
34 pub span: Span,
35}
36
37#[derive(Debug, Clone)]
39pub enum WildcardResolution {
40 Resolved(HashSet<String>),
42 Unknown,
44}
45
46#[derive(Debug, Default)]
48pub struct ModuleGraph {
49 modules: HashMap<PathBuf, ModuleInfo>,
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 fn_names: Vec<String>,
102 has_pub_fn: bool,
104 type_declarations: Vec<SNode>,
107 callable_declarations: Vec<SNode>,
110}
111
112#[derive(Debug, Clone)]
113struct ImportRef {
114 raw_path: String,
115 path: Option<PathBuf>,
116 selective_names: Option<HashSet<String>>,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct ModuleImport {
122 pub raw_path: String,
124 pub resolved_path: Option<PathBuf>,
126 pub selective_names: Option<Vec<String>>,
128}
129
130#[derive(Debug, Default, Deserialize)]
131struct PackageManifest {
132 #[serde(default)]
133 exports: HashMap<String, String>,
134}
135
136pub fn read_module_source(path: &Path) -> Option<String> {
142 if let Some(stdlib_module) = stdlib_module_from_path(path) {
143 return stdlib::get_stdlib_source(stdlib_module).map(ToString::to_string);
144 }
145 std::fs::read_to_string(path).ok()
146}
147
148pub fn build(files: &[PathBuf]) -> ModuleGraph {
154 build_inner(files, None).graph
155}
156
157pub fn build_with_parsed_sources(files: &[PathBuf]) -> ModuleGraphBuild {
163 let parsed_source_targets = files.iter().map(|file| normalize_path(file)).collect();
164 build_inner(files, Some(&parsed_source_targets))
165}
166
167fn build_inner(
168 files: &[PathBuf],
169 parsed_source_targets: Option<&HashSet<PathBuf>>,
170) -> ModuleGraphBuild {
171 let mut modules: HashMap<PathBuf, ModuleInfo> = HashMap::new();
172 let mut parsed_sources: HashMap<PathBuf, ParsedModuleSource> = HashMap::new();
173 let mut seen: HashSet<PathBuf> = HashSet::new();
174 let mut queue: VecDeque<PathBuf> = VecDeque::new();
175 for file in files {
176 let canonical = normalize_path(file);
177 if seen.insert(canonical.clone()) {
178 queue.push_back(canonical);
179 }
180 }
181 while let Some(path) = queue.pop_front() {
182 if modules.contains_key(&path) {
183 continue;
184 }
185 let retain_parsed_source =
186 parsed_source_targets.is_some_and(|targets| targets.contains(&path));
187 let (module, parsed) = load_module(&path);
188 if retain_parsed_source {
189 if let Some(parsed) = parsed {
190 parsed_sources.insert(path.clone(), parsed);
191 }
192 }
193 for import in &module.imports {
210 if let Some(import_path) = &import.path {
211 let canonical = normalize_path(import_path);
212 if seen.insert(canonical.clone()) {
213 queue.push_back(canonical);
214 }
215 }
216 }
217 modules.insert(path, module);
218 }
219 resolve_re_exports(&mut modules);
220 ModuleGraphBuild {
221 graph: ModuleGraph { modules },
222 parsed_sources,
223 }
224}
225
226fn resolve_re_exports(modules: &mut HashMap<PathBuf, ModuleInfo>) {
231 let keys: Vec<PathBuf> = modules.keys().cloned().collect();
232 loop {
233 let mut changed = false;
234 for path in &keys {
235 let wildcard_paths = modules
238 .get(path)
239 .map(|m| m.wildcard_re_export_paths.clone())
240 .unwrap_or_default();
241 if wildcard_paths.is_empty() {
242 continue;
243 }
244 let mut additions: Vec<String> = Vec::new();
245 for src in &wildcard_paths {
246 let src_canonical = normalize_path(src);
247 if let Some(src_module) = modules.get(src).or_else(|| modules.get(&src_canonical)) {
248 additions.extend(src_module.exports.iter().cloned());
249 }
250 }
251 if let Some(module) = modules.get_mut(path) {
252 for name in additions {
253 if module.exports.insert(name) {
254 changed = true;
255 }
256 }
257 }
258 }
259 if !changed {
260 break;
261 }
262 }
263}
264
265pub fn resolve_import_path(current_file: &Path, import_path: &str) -> Option<PathBuf> {
278 if let Some(module) = import_path
279 .strip_prefix("std/")
280 .or_else(|| (import_path == "observability").then_some("observability"))
281 {
282 if stdlib::get_stdlib_source(module).is_some() {
283 return Some(stdlib::stdlib_virtual_path(module));
284 }
285 return None;
286 }
287
288 let base = current_file.parent().unwrap_or(Path::new("."));
289 let mut file_path = base.join(import_path);
290 if !file_path.exists() && file_path.extension().is_none() {
291 file_path.set_extension("harn");
292 }
293 if file_path.exists() {
294 return Some(file_path);
295 }
296
297 if let Some(path) = resolve_package_import(base, import_path) {
298 return Some(path);
299 }
300
301 None
302}
303
304fn resolve_package_import(base: &Path, import_path: &str) -> Option<PathBuf> {
305 for anchor in base.ancestors() {
306 let packages_root = anchor.join(".harn/packages");
307 if !packages_root.is_dir() {
308 if anchor.join(".git").exists() {
309 break;
310 }
311 continue;
312 }
313 if let Some(path) = resolve_from_packages_root(&packages_root, import_path) {
314 return Some(path);
315 }
316 if anchor.join(".git").exists() {
317 break;
318 }
319 }
320 None
321}
322
323fn resolve_from_packages_root(packages_root: &Path, import_path: &str) -> Option<PathBuf> {
324 let safe_import_path = safe_package_relative_path(import_path)?;
325 let package_name = package_name_from_relative_path(&safe_import_path)?;
326 let package_root = packages_root.join(package_name);
327
328 let pkg_path = packages_root.join(&safe_import_path);
329 if let Some(path) = finalize_package_target(&package_root, &pkg_path) {
330 return Some(path);
331 }
332
333 let export_name = export_name_from_relative_path(&safe_import_path)?;
334 let manifest_path = packages_root.join(package_name).join("harn.toml");
335 let manifest = read_package_manifest(&manifest_path)?;
336 let rel_path = manifest.exports.get(export_name)?;
337 let safe_export_path = safe_package_relative_path(rel_path)?;
338 finalize_package_target(&package_root, &package_root.join(safe_export_path))
339}
340
341fn read_package_manifest(path: &Path) -> Option<PackageManifest> {
342 let content = std::fs::read_to_string(path).ok()?;
343 toml::from_str::<PackageManifest>(&content).ok()
344}
345
346fn safe_package_relative_path(raw: &str) -> Option<PathBuf> {
347 if raw.is_empty() || raw.contains('\\') {
348 return None;
349 }
350 let mut out = PathBuf::new();
351 let mut saw_component = false;
352 for component in Path::new(raw).components() {
353 match component {
354 Component::Normal(part) => {
355 saw_component = true;
356 out.push(part);
357 }
358 Component::CurDir => {}
359 Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
360 }
361 }
362 saw_component.then_some(out)
363}
364
365fn package_name_from_relative_path(path: &Path) -> Option<&str> {
366 match path.components().next()? {
367 Component::Normal(name) => name.to_str(),
368 _ => None,
369 }
370}
371
372fn export_name_from_relative_path(path: &Path) -> Option<&str> {
373 let mut components = path.components();
374 components.next()?;
375 let rest = components.as_path();
376 if rest.as_os_str().is_empty() {
377 None
378 } else {
379 rest.to_str()
380 }
381}
382
383fn path_is_within(root: &Path, path: &Path) -> bool {
384 let Ok(root) = root.canonicalize() else {
385 return false;
386 };
387 let Ok(path) = path.canonicalize() else {
388 return false;
389 };
390 path == root || path.starts_with(root)
391}
392
393fn target_within_package_root(package_root: &Path, path: PathBuf) -> Option<PathBuf> {
394 path_is_within(package_root, &path).then_some(path)
395}
396
397fn finalize_package_target(package_root: &Path, path: &Path) -> Option<PathBuf> {
398 if path.is_dir() {
399 let lib = path.join("lib.harn");
400 if lib.exists() {
401 return target_within_package_root(package_root, lib);
402 }
403 return target_within_package_root(package_root, path.to_path_buf());
404 }
405 if path.exists() {
406 return target_within_package_root(package_root, path.to_path_buf());
407 }
408 if path.extension().is_none() {
409 let mut with_ext = path.to_path_buf();
410 with_ext.set_extension("harn");
411 if with_ext.exists() {
412 return target_within_package_root(package_root, with_ext);
413 }
414 }
415 None
416}
417
418impl ModuleGraph {
419 pub fn module_paths(&self) -> Vec<PathBuf> {
424 let mut paths: Vec<PathBuf> = self.modules.keys().cloned().collect();
425 paths.sort();
426 paths
427 }
428
429 pub fn contains_module(&self, path: &Path) -> bool {
432 self.modules.contains_key(path) || self.modules.contains_key(&normalize_path(path))
433 }
434
435 pub fn all_selective_import_names(&self) -> HashSet<&str> {
437 let mut names = HashSet::new();
438 for module in self.modules.values() {
439 for name in &module.selective_import_names {
440 names.insert(name.as_str());
441 }
442 }
443 names
444 }
445
446 pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
449 let target = normalize_path(target);
450 let mut out: Vec<PathBuf> = self
451 .modules
452 .iter()
453 .filter(|(_, info)| {
454 info.imports.iter().any(|import| {
455 import
456 .path
457 .as_ref()
458 .is_some_and(|p| normalize_path(p) == target)
459 })
460 })
461 .map(|(path, _)| path.clone())
462 .collect();
463 out.sort();
464 out
465 }
466
467 pub fn imports_for_module(&self, file: &Path) -> Vec<ModuleImport> {
469 let file = normalize_path(file);
470 let Some(module) = self.modules.get(&file) else {
471 return Vec::new();
472 };
473 let mut imports: Vec<ModuleImport> = module
474 .imports
475 .iter()
476 .map(|import| {
477 let mut selective_names = import
478 .selective_names
479 .as_ref()
480 .map(|names| names.iter().cloned().collect::<Vec<_>>());
481 if let Some(names) = selective_names.as_mut() {
482 names.sort();
483 }
484 ModuleImport {
485 raw_path: import.raw_path.clone(),
486 resolved_path: import.path.as_ref().map(|path| normalize_path(path)),
487 selective_names,
488 }
489 })
490 .collect();
491 imports.sort_by(|left, right| {
492 left.raw_path
493 .cmp(&right.raw_path)
494 .then_with(|| left.selective_names.cmp(&right.selective_names))
495 .then_with(|| left.resolved_path.cmp(&right.resolved_path))
496 });
497 imports
498 }
499
500 pub fn exports_for_module(&self, file: &Path) -> Vec<String> {
502 let file = normalize_path(file);
503 let Some(module) = self.modules.get(&file) else {
504 return Vec::new();
505 };
506 let mut exports: Vec<String> = module.exports.iter().cloned().collect();
507 exports.sort();
508 exports
509 }
510
511 pub fn wildcard_exports_for(&self, file: &Path) -> WildcardResolution {
516 let file = normalize_path(file);
517 let Some(module) = self.modules.get(&file) else {
518 return WildcardResolution::Unknown;
519 };
520 if module.has_unresolved_wildcard_import {
521 return WildcardResolution::Unknown;
522 }
523
524 let mut names = HashSet::new();
525 for import in module
526 .imports
527 .iter()
528 .filter(|import| import.selective_names.is_none())
529 {
530 let Some(import_path) = &import.path else {
531 return WildcardResolution::Unknown;
532 };
533 let imported = self.modules.get(import_path).or_else(|| {
534 let normalized = normalize_path(import_path);
535 self.modules.get(&normalized)
536 });
537 let Some(imported) = imported else {
538 return WildcardResolution::Unknown;
539 };
540 names.extend(imported.exports.iter().cloned());
541 }
542 WildcardResolution::Resolved(names)
543 }
544
545 pub fn imported_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
561 let file = normalize_path(file);
562 let module = self.modules.get(&file)?;
563 if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
564 return None;
565 }
566
567 let mut names = HashSet::new();
568 for import in &module.imports {
569 let import_path = import.path.as_ref()?;
570 let imported = self
571 .modules
572 .get(import_path)
573 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
574 match &import.selective_names {
575 None => {
576 names.extend(imported.exports.iter().cloned());
577 }
578 Some(selective) => {
579 for name in selective {
588 if imported.declarations.contains_key(name)
589 || imported.exports.contains(name)
590 {
591 names.insert(name.clone());
592 }
593 }
594 }
595 }
596 }
597 Some(names)
598 }
599
600 pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
604 let file = normalize_path(file);
605 let module = self.modules.get(&file)?;
606 if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
607 return None;
608 }
609
610 let mut decls = Vec::new();
611 for import in &module.imports {
612 let import_path = import.path.as_ref()?;
613 let imported = self
614 .modules
615 .get(import_path)
616 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
617 let names_to_collect: Vec<String> = match &import.selective_names {
618 None => imported.exports.iter().cloned().collect(),
619 Some(selective) => {
620 let mut names: Vec<String> = selective.iter().cloned().collect();
629 for ty_decl in &imported.type_declarations {
630 if let Some(name) = type_decl_name(ty_decl) {
631 if imported.own_exports.contains(name)
632 && !names.iter().any(|n| n == name)
633 {
634 names.push(name.to_string());
635 }
636 }
637 }
638 names
639 }
640 };
641 for name in &names_to_collect {
642 let mut visited = HashSet::new();
643 if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
644 decls.push(decl);
645 }
646 }
647 }
648 Some(decls)
649 }
650
651 pub fn imported_callable_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
655 let file = normalize_path(file);
656 let module = self.modules.get(&file)?;
657 if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
658 return None;
659 }
660
661 let mut decls = Vec::new();
662 for import in &module.imports {
663 let import_path = import.path.as_ref()?;
664 let imported = self
665 .modules
666 .get(import_path)
667 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
668 let selective_import = import.selective_names.is_some();
669 let names_to_collect: Vec<String> = match &import.selective_names {
670 None => imported.exports.iter().cloned().collect(),
671 Some(selective) => selective.iter().cloned().collect(),
672 };
673 for name in &names_to_collect {
674 if selective_import || imported.own_exports.contains(name) {
675 if let Some(decl) = imported
676 .callable_declarations
677 .iter()
678 .find(|decl| callable_decl_name(decl) == Some(name.as_str()))
679 {
680 decls.push(decl.clone());
681 continue;
682 }
683 }
684 let mut visited = HashSet::new();
685 if let Some(decl) =
686 self.find_exported_callable_decl(import_path, name, &mut visited)
687 {
688 decls.push(decl);
689 }
690 }
691 }
692 Some(decls)
693 }
694
695 fn find_exported_type_decl(
698 &self,
699 path: &Path,
700 name: &str,
701 visited: &mut HashSet<PathBuf>,
702 ) -> Option<SNode> {
703 let canonical = normalize_path(path);
704 if !visited.insert(canonical.clone()) {
705 return None;
706 }
707 let module = self
708 .modules
709 .get(&canonical)
710 .or_else(|| self.modules.get(path))?;
711 for decl in &module.type_declarations {
712 if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
713 return Some(decl.clone());
714 }
715 }
716 if let Some(sources) = module.selective_re_exports.get(name) {
717 for source in sources {
718 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
719 return Some(decl);
720 }
721 }
722 }
723 for source in &module.wildcard_re_export_paths {
724 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
725 return Some(decl);
726 }
727 }
728 None
729 }
730
731 fn find_exported_callable_decl(
732 &self,
733 path: &Path,
734 name: &str,
735 visited: &mut HashSet<PathBuf>,
736 ) -> Option<SNode> {
737 let canonical = normalize_path(path);
738 if !visited.insert(canonical.clone()) {
739 return None;
740 }
741 let module = self
742 .modules
743 .get(&canonical)
744 .or_else(|| self.modules.get(path))?;
745 for decl in &module.callable_declarations {
746 if callable_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
747 return Some(decl.clone());
748 }
749 }
750 if let Some(sources) = module.selective_re_exports.get(name) {
751 for source in sources {
752 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
753 return Some(decl);
754 }
755 }
756 }
757 for source in &module.wildcard_re_export_paths {
758 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
759 return Some(decl);
760 }
761 }
762 None
763 }
764
765 pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
771 let mut visited = HashSet::new();
772 self.definition_of_inner(file, name, &mut visited)
773 }
774
775 fn definition_of_inner(
776 &self,
777 file: &Path,
778 name: &str,
779 visited: &mut HashSet<PathBuf>,
780 ) -> Option<DefSite> {
781 let file = normalize_path(file);
782 if !visited.insert(file.clone()) {
783 return None;
784 }
785 let current = self.modules.get(&file)?;
786
787 if let Some(local) = current.declarations.get(name) {
788 return Some(local.clone());
789 }
790
791 if let Some(sources) = current.selective_re_exports.get(name) {
796 for source in sources {
797 if let Some(def) = self.definition_of_inner(source, name, visited) {
798 return Some(def);
799 }
800 }
801 }
802
803 for source in ¤t.wildcard_re_export_paths {
805 if let Some(def) = self.definition_of_inner(source, name, visited) {
806 return Some(def);
807 }
808 }
809
810 for import in ¤t.imports {
812 let Some(selective_names) = &import.selective_names else {
813 continue;
814 };
815 if !selective_names.contains(name) {
816 continue;
817 }
818 if let Some(path) = &import.path {
819 if let Some(def) = self.definition_of_inner(path, name, visited) {
820 return Some(def);
821 }
822 }
823 }
824
825 for import in ¤t.imports {
827 if import.selective_names.is_some() {
828 continue;
829 }
830 if let Some(path) = &import.path {
831 if let Some(def) = self.definition_of_inner(path, name, visited) {
832 return Some(def);
833 }
834 }
835 }
836
837 None
838 }
839
840 pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
844 let file = normalize_path(file);
845 let Some(module) = self.modules.get(&file) else {
846 return Vec::new();
847 };
848
849 let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
853
854 for (name, srcs) in &module.selective_re_exports {
855 sources
856 .entry(name.clone())
857 .or_default()
858 .extend(srcs.iter().cloned());
859 }
860 for src in &module.wildcard_re_export_paths {
861 let canonical = normalize_path(src);
862 let Some(src_module) = self
863 .modules
864 .get(&canonical)
865 .or_else(|| self.modules.get(src))
866 else {
867 continue;
868 };
869 for name in &src_module.exports {
870 sources
871 .entry(name.clone())
872 .or_default()
873 .push(canonical.clone());
874 }
875 }
876
877 for name in &module.own_exports {
881 if let Some(entry) = sources.get_mut(name) {
882 entry.push(file.clone());
883 }
884 }
885
886 let mut conflicts = Vec::new();
887 for (name, mut srcs) in sources {
888 srcs.sort();
889 srcs.dedup();
890 if srcs.len() > 1 {
891 conflicts.push(ReExportConflict {
892 name,
893 sources: srcs,
894 });
895 }
896 }
897 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
898 conflicts
899 }
900
901 pub fn non_exported_selective_imports(&self, file: &Path) -> Vec<NonExportedImport> {
913 let file = normalize_path(file);
914 let Some(module) = self.modules.get(&file) else {
915 return Vec::new();
916 };
917
918 let mut out = Vec::new();
919 for import in &module.imports {
920 let Some(selective) = &import.selective_names else {
921 continue;
922 };
923 let Some(import_path) = &import.path else {
924 continue;
925 };
926 let Some(target) = self
927 .modules
928 .get(import_path)
929 .or_else(|| self.modules.get(&normalize_path(import_path)))
930 else {
931 continue;
932 };
933 if !target.has_pub_fn {
936 continue;
937 }
938 for name in selective {
939 if target.declarations.contains_key(name) && !target.exports.contains(name) {
943 out.push(NonExportedImport {
944 name: name.clone(),
945 module: import.raw_path.clone(),
946 });
947 }
948 }
949 }
950 out.sort_by(|a, b| (&a.name, &a.module).cmp(&(&b.name, &b.module)));
951 out.dedup();
952 out
953 }
954}
955
956#[derive(Debug, Clone, PartialEq, Eq)]
959pub struct ReExportConflict {
960 pub name: String,
961 pub sources: Vec<PathBuf>,
962}
963
964#[derive(Debug, Clone, PartialEq, Eq)]
967pub struct NonExportedImport {
968 pub name: String,
970 pub module: String,
972}
973
974fn load_module(path: &Path) -> (ModuleInfo, Option<ParsedModuleSource>) {
975 let Some(source) = read_module_source(path) else {
976 return (ModuleInfo::default(), None);
977 };
978 let mut lexer = harn_lexer::Lexer::new(&source);
979 let tokens = match lexer.tokenize() {
980 Ok(tokens) => tokens,
981 Err(_) => return (ModuleInfo::default(), None),
982 };
983 let mut parser = Parser::new(tokens);
984 let program = match parser.parse() {
985 Ok(program) => program,
986 Err(_) => return (ModuleInfo::default(), None),
987 };
988
989 let mut module = ModuleInfo::default();
990 for node in &program {
991 collect_module_info(path, node, &mut module);
992 collect_type_declarations(node, &mut module.type_declarations);
993 collect_callable_declarations(node, &mut module.callable_declarations);
994 }
995 if !module.has_pub_fn {
998 for name in &module.fn_names {
999 module.own_exports.insert(name.clone());
1000 }
1001 }
1002 module.exports.extend(module.own_exports.iter().cloned());
1006 module
1007 .exports
1008 .extend(module.selective_re_exports.keys().cloned());
1009 let parsed = ParsedModuleSource { source, program };
1010 (module, Some(parsed))
1011}
1012
1013fn stdlib_module_from_path(path: &Path) -> Option<&str> {
1016 let s = path.to_str()?;
1017 s.strip_prefix("<std>/")
1018}
1019
1020fn collect_module_info(file: &Path, snode: &SNode, module: &mut ModuleInfo) {
1021 match &snode.node {
1022 Node::FnDecl {
1023 name,
1024 params,
1025 is_pub,
1026 ..
1027 } => {
1028 if *is_pub {
1029 module.own_exports.insert(name.clone());
1030 module.has_pub_fn = true;
1031 }
1032 module.fn_names.push(name.clone());
1033 module.declarations.insert(
1034 name.clone(),
1035 decl_site(file, snode.span, name, DefKind::Function),
1036 );
1037 for param_name in params.iter().map(|param| param.name.clone()) {
1038 module.declarations.insert(
1039 param_name.clone(),
1040 decl_site(file, snode.span, ¶m_name, DefKind::Parameter),
1041 );
1042 }
1043 }
1044 Node::Pipeline { name, is_pub, .. } => {
1045 if *is_pub {
1046 module.own_exports.insert(name.clone());
1047 }
1048 module.declarations.insert(
1049 name.clone(),
1050 decl_site(file, snode.span, name, DefKind::Pipeline),
1051 );
1052 }
1053 Node::ToolDecl { name, is_pub, .. } => {
1054 if *is_pub {
1055 module.own_exports.insert(name.clone());
1056 }
1057 module.declarations.insert(
1058 name.clone(),
1059 decl_site(file, snode.span, name, DefKind::Tool),
1060 );
1061 }
1062 Node::SkillDecl { name, is_pub, .. } => {
1063 if *is_pub {
1064 module.own_exports.insert(name.clone());
1065 }
1066 module.declarations.insert(
1067 name.clone(),
1068 decl_site(file, snode.span, name, DefKind::Skill),
1069 );
1070 }
1071 Node::StructDecl { name, is_pub, .. } => {
1072 if *is_pub {
1073 module.own_exports.insert(name.clone());
1074 }
1075 module.declarations.insert(
1076 name.clone(),
1077 decl_site(file, snode.span, name, DefKind::Struct),
1078 );
1079 }
1080 Node::EnumDecl { name, is_pub, .. } => {
1081 if *is_pub {
1082 module.own_exports.insert(name.clone());
1083 }
1084 module.declarations.insert(
1085 name.clone(),
1086 decl_site(file, snode.span, name, DefKind::Enum),
1087 );
1088 }
1089 Node::InterfaceDecl { name, .. } => {
1090 module.own_exports.insert(name.clone());
1091 module.declarations.insert(
1092 name.clone(),
1093 decl_site(file, snode.span, name, DefKind::Interface),
1094 );
1095 }
1096 Node::TypeDecl { name, .. } => {
1097 module.own_exports.insert(name.clone());
1098 module.declarations.insert(
1099 name.clone(),
1100 decl_site(file, snode.span, name, DefKind::Type),
1101 );
1102 }
1103 Node::LetBinding { pattern, .. } | Node::VarBinding { pattern, .. } => {
1104 for name in pattern_names(pattern) {
1105 module.declarations.insert(
1106 name.clone(),
1107 decl_site(file, snode.span, &name, DefKind::Variable),
1108 );
1109 }
1110 }
1111 Node::ImportDecl { path, is_pub } => {
1112 let import_path = resolve_import_path(file, path);
1113 if import_path.is_none() {
1114 module.has_unresolved_wildcard_import = true;
1115 }
1116 if *is_pub {
1117 if let Some(resolved) = &import_path {
1118 module
1119 .wildcard_re_export_paths
1120 .push(normalize_path(resolved));
1121 }
1122 }
1123 module.imports.push(ImportRef {
1124 raw_path: path.clone(),
1125 path: import_path,
1126 selective_names: None,
1127 });
1128 }
1129 Node::SelectiveImport {
1130 names,
1131 path,
1132 is_pub,
1133 } => {
1134 let import_path = resolve_import_path(file, path);
1135 if import_path.is_none() {
1136 module.has_unresolved_selective_import = true;
1137 }
1138 if *is_pub {
1139 if let Some(resolved) = &import_path {
1140 let canonical = normalize_path(resolved);
1141 for name in names {
1142 module
1143 .selective_re_exports
1144 .entry(name.clone())
1145 .or_default()
1146 .push(canonical.clone());
1147 }
1148 }
1149 }
1150 let names: HashSet<String> = names.iter().cloned().collect();
1151 module.selective_import_names.extend(names.iter().cloned());
1152 module.imports.push(ImportRef {
1153 raw_path: path.clone(),
1154 path: import_path,
1155 selective_names: Some(names),
1156 });
1157 }
1158 Node::AttributedDecl { inner, .. } => {
1159 collect_module_info(file, inner, module);
1160 }
1161 _ => {}
1162 }
1163}
1164
1165fn collect_type_declarations(snode: &SNode, decls: &mut Vec<SNode>) {
1166 match &snode.node {
1167 Node::TypeDecl { .. }
1168 | Node::StructDecl { .. }
1169 | Node::EnumDecl { .. }
1170 | Node::InterfaceDecl { .. } => decls.push(snode.clone()),
1171 Node::AttributedDecl { inner, .. } => collect_type_declarations(inner, decls),
1172 _ => {}
1173 }
1174}
1175
1176fn collect_callable_declarations(snode: &SNode, decls: &mut Vec<SNode>) {
1177 match &snode.node {
1178 Node::FnDecl { .. } | Node::Pipeline { .. } | Node::ToolDecl { .. } => {
1179 decls.push(snode.clone());
1180 }
1181 Node::AttributedDecl { inner, .. } => collect_callable_declarations(inner, decls),
1182 _ => {}
1183 }
1184}
1185
1186fn type_decl_name(snode: &SNode) -> Option<&str> {
1187 match &snode.node {
1188 Node::TypeDecl { name, .. }
1189 | Node::StructDecl { name, .. }
1190 | Node::EnumDecl { name, .. }
1191 | Node::InterfaceDecl { name, .. } => Some(name.as_str()),
1192 _ => None,
1193 }
1194}
1195
1196fn callable_decl_name(snode: &SNode) -> Option<&str> {
1197 match &snode.node {
1198 Node::FnDecl { name, .. } | Node::Pipeline { name, .. } | Node::ToolDecl { name, .. } => {
1199 Some(name.as_str())
1200 }
1201 Node::AttributedDecl { inner, .. } => callable_decl_name(inner),
1202 _ => None,
1203 }
1204}
1205
1206fn decl_site(file: &Path, span: Span, name: &str, kind: DefKind) -> DefSite {
1207 DefSite {
1208 name: name.to_string(),
1209 file: file.to_path_buf(),
1210 kind,
1211 span,
1212 }
1213}
1214
1215fn pattern_names(pattern: &BindingPattern) -> Vec<String> {
1216 match pattern {
1217 BindingPattern::Identifier(name) => vec![name.clone()],
1218 BindingPattern::Dict(fields) => fields
1219 .iter()
1220 .filter_map(|field| field.alias.as_ref().or(Some(&field.key)).cloned())
1221 .collect(),
1222 BindingPattern::List(elements) => elements
1223 .iter()
1224 .map(|element| element.name.clone())
1225 .collect(),
1226 BindingPattern::Pair(a, b) => vec![a.clone(), b.clone()],
1227 }
1228}
1229
1230fn normalize_path(path: &Path) -> PathBuf {
1231 if stdlib_module_from_path(path).is_some() {
1232 return path.to_path_buf();
1233 }
1234 path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
1235}
1236
1237#[cfg(test)]
1238mod tests {
1239 use super::*;
1240 use std::fs;
1241
1242 fn write_file(dir: &Path, name: &str, contents: &str) -> PathBuf {
1243 let path = dir.join(name);
1244 fs::write(&path, contents).unwrap();
1245 path
1246 }
1247
1248 #[test]
1249 fn importers_of_finds_direct_dependents() {
1250 let tmp = tempfile::tempdir().unwrap();
1251 let root = tmp.path();
1252 let leaf = write_file(root, "leaf.harn", "pub fn leaf() { 1 }\n");
1253 write_file(root, "a.harn", "import \"./leaf\"\nleaf()\n");
1254 write_file(root, "b.harn", "import { leaf } from \"./leaf\"\nleaf()\n");
1255 let entry = write_file(root, "entry.harn", "import \"./a\"\nimport \"./b\"\n");
1256
1257 let graph = build(std::slice::from_ref(&entry));
1258 let importers = graph.importers_of(&leaf);
1259 let names: Vec<String> = importers
1260 .iter()
1261 .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
1262 .collect();
1263 assert!(names.contains(&"a.harn".to_string()));
1264 assert!(names.contains(&"b.harn".to_string()));
1265 assert!(!names.contains(&"entry.harn".to_string()));
1266 }
1267
1268 #[test]
1269 fn recursive_build_loads_transitively_imported_modules() {
1270 let tmp = tempfile::tempdir().unwrap();
1271 let root = tmp.path();
1272 write_file(root, "leaf.harn", "pub fn leaf_fn() { 1 }\n");
1273 write_file(
1274 root,
1275 "mid.harn",
1276 "import \"./leaf\"\npub fn mid_fn() { leaf_fn() }\n",
1277 );
1278 let entry = write_file(root, "entry.harn", "import \"./mid\"\nmid_fn()\n");
1279
1280 let graph = build(std::slice::from_ref(&entry));
1281 let imported = graph
1282 .imported_names_for_file(&entry)
1283 .expect("entry imports should resolve");
1284 assert!(imported.contains("mid_fn"));
1286 assert!(!imported.contains("leaf_fn"));
1287
1288 let leaf_path = root.join("leaf.harn");
1291 assert!(graph.definition_of(&leaf_path, "leaf_fn").is_some());
1292 }
1293
1294 #[test]
1295 fn imported_names_returns_none_when_import_unresolved() {
1296 let tmp = tempfile::tempdir().unwrap();
1297 let root = tmp.path();
1298 let entry = write_file(root, "entry.harn", "import \"./does_not_exist\"\n");
1299
1300 let graph = build(std::slice::from_ref(&entry));
1301 assert!(graph.imported_names_for_file(&entry).is_none());
1302 }
1303
1304 #[test]
1305 fn selective_imports_contribute_only_requested_names() {
1306 let tmp = tempfile::tempdir().unwrap();
1307 let root = tmp.path();
1308 write_file(root, "util.harn", "pub fn a() { 1 }\npub fn b() { 2 }\n");
1309 let entry = write_file(root, "entry.harn", "import { a } from \"./util\"\n");
1310
1311 let graph = build(std::slice::from_ref(&entry));
1312 let imported = graph
1313 .imported_names_for_file(&entry)
1314 .expect("entry imports should resolve");
1315 assert!(imported.contains("a"));
1316 assert!(!imported.contains("b"));
1317 }
1318
1319 #[test]
1320 fn non_exported_selective_import_is_flagged_when_module_has_pub() {
1321 let tmp = tempfile::tempdir().unwrap();
1322 let root = tmp.path();
1323 write_file(root, "lib.harn", "pub fn api() { 1 }\nfn helper() { 2 }\n");
1324 let entry = write_file(root, "entry.harn", "import { helper } from \"./lib\"\n");
1325
1326 let graph = build(std::slice::from_ref(&entry));
1327 let offenders = graph.non_exported_selective_imports(&entry);
1328 assert_eq!(offenders.len(), 1);
1329 assert_eq!(offenders[0].name, "helper");
1330 assert_eq!(offenders[0].module, "./lib");
1331
1332 let entry_ok = write_file(root, "entry_ok.harn", "import { api } from \"./lib\"\n");
1334 let graph_ok = build(std::slice::from_ref(&entry_ok));
1335 assert!(graph_ok
1336 .non_exported_selective_imports(&entry_ok)
1337 .is_empty());
1338 }
1339
1340 #[test]
1341 fn non_exported_selective_import_allows_everything_when_no_pub() {
1342 let tmp = tempfile::tempdir().unwrap();
1343 let root = tmp.path();
1344 write_file(root, "util.harn", "fn a() { 1 }\nfn b() { 2 }\n");
1347 let entry = write_file(root, "entry.harn", "import { a } from \"./util\"\n");
1348
1349 let graph = build(std::slice::from_ref(&entry));
1350 assert!(graph.non_exported_selective_imports(&entry).is_empty());
1351 }
1352
1353 #[test]
1354 fn stdlib_imports_resolve_to_embedded_sources() {
1355 let tmp = tempfile::tempdir().unwrap();
1356 let root = tmp.path();
1357 let entry = write_file(root, "entry.harn", "import \"std/math\"\nclamp(5, 0, 10)\n");
1358
1359 let graph = build(std::slice::from_ref(&entry));
1360 let imported = graph
1361 .imported_names_for_file(&entry)
1362 .expect("std/math should resolve");
1363 assert!(imported.contains("clamp"));
1365 }
1366
1367 #[test]
1368 fn stdlib_internal_imports_resolve_without_leaking_to_callers() {
1369 let tmp = tempfile::tempdir().unwrap();
1370 let root = tmp.path();
1371 let entry = write_file(
1372 root,
1373 "entry.harn",
1374 "import { process_run } from \"std/runtime\"\nprocess_run([\"echo\", \"ok\"])\n",
1375 );
1376
1377 let graph = build(std::slice::from_ref(&entry));
1378 let entry_imports = graph
1379 .imported_names_for_file(&entry)
1380 .expect("std/runtime should resolve");
1381 assert!(entry_imports.contains("process_run"));
1382 assert!(
1383 !entry_imports.contains("filter_nil"),
1384 "private std/runtime dependency leaked to caller"
1385 );
1386
1387 let runtime_path = stdlib::stdlib_virtual_path("runtime");
1388 let runtime_imports = graph
1389 .imported_names_for_file(&runtime_path)
1390 .expect("std/runtime internal imports should resolve");
1391 assert!(runtime_imports.contains("filter_nil"));
1392 }
1393
1394 #[test]
1395 fn runtime_stdlib_import_surface_resolves_to_embedded_sources() {
1396 let tmp = tempfile::tempdir().unwrap();
1397 let entry_path = write_file(tmp.path(), "entry.harn", "");
1398
1399 for source in harn_stdlib::STDLIB_SOURCES {
1400 let import_path = format!("std/{}", source.module);
1401 assert!(
1402 resolve_import_path(&entry_path, &import_path).is_some(),
1403 "{import_path} should resolve in the module graph"
1404 );
1405 }
1406 }
1407
1408 #[test]
1409 fn stdlib_imports_expose_type_declarations() {
1410 let tmp = tempfile::tempdir().unwrap();
1411 let root = tmp.path();
1412 let entry = write_file(
1413 root,
1414 "entry.harn",
1415 "import \"std/triggers\"\nlet provider = \"github\"\n",
1416 );
1417
1418 let graph = build(std::slice::from_ref(&entry));
1419 let decls = graph
1420 .imported_type_declarations_for_file(&entry)
1421 .expect("std/triggers type declarations should resolve");
1422 let names: HashSet<String> = decls
1423 .iter()
1424 .filter_map(type_decl_name)
1425 .map(ToString::to_string)
1426 .collect();
1427 assert!(names.contains("TriggerEvent"));
1428 assert!(names.contains("ProviderPayload"));
1429 assert!(names.contains("SignatureStatus"));
1430 }
1431
1432 #[test]
1433 fn stdlib_imports_expose_callable_declarations() {
1434 let tmp = tempfile::tempdir().unwrap();
1435 let root = tmp.path();
1436 let entry = write_file(
1437 root,
1438 "entry.harn",
1439 "import { select_from } from \"std/tui\"\nlet item = \"alpha\"\n",
1440 );
1441
1442 let graph = build(std::slice::from_ref(&entry));
1443 let decls = graph
1444 .imported_callable_declarations_for_file(&entry)
1445 .expect("std/tui callable declarations should resolve");
1446 let names: HashSet<String> = decls
1447 .iter()
1448 .filter_map(callable_decl_name)
1449 .map(ToString::to_string)
1450 .collect();
1451 assert!(names.contains("select_from"));
1452 }
1453
1454 #[test]
1455 fn package_export_map_resolves_declared_module() {
1456 let tmp = tempfile::tempdir().unwrap();
1457 let root = tmp.path();
1458 let packages = root.join(".harn/packages/acme/runtime");
1459 fs::create_dir_all(&packages).unwrap();
1460 fs::write(
1461 root.join(".harn/packages/acme/harn.toml"),
1462 "[exports]\ncapabilities = \"runtime/capabilities.harn\"\n",
1463 )
1464 .unwrap();
1465 fs::write(
1466 packages.join("capabilities.harn"),
1467 "pub fn exported_capability() { 1 }\n",
1468 )
1469 .unwrap();
1470 let entry = write_file(
1471 root,
1472 "entry.harn",
1473 "import \"acme/capabilities\"\nexported_capability()\n",
1474 );
1475
1476 let graph = build(std::slice::from_ref(&entry));
1477 let imported = graph
1478 .imported_names_for_file(&entry)
1479 .expect("package export should resolve");
1480 assert!(imported.contains("exported_capability"));
1481 }
1482
1483 #[test]
1484 fn package_direct_import_cannot_escape_packages_root() {
1485 let tmp = tempfile::tempdir().unwrap();
1486 let root = tmp.path();
1487 fs::create_dir_all(root.join(".harn/packages/acme")).unwrap();
1488 fs::write(root.join("secret.harn"), "pub fn leaked() { 1 }\n").unwrap();
1489 let entry = write_file(root, "entry.harn", "");
1490
1491 let resolved = resolve_import_path(&entry, "acme/../../secret");
1492 assert!(resolved.is_none(), "package import escaped package root");
1493 }
1494
1495 #[test]
1496 fn package_export_map_cannot_escape_package_root() {
1497 let tmp = tempfile::tempdir().unwrap();
1498 let root = tmp.path();
1499 fs::create_dir_all(root.join(".harn/packages/acme")).unwrap();
1500 fs::write(root.join("secret.harn"), "pub fn leaked() { 1 }\n").unwrap();
1501 fs::write(
1502 root.join(".harn/packages/acme/harn.toml"),
1503 "[exports]\nleak = \"../../secret.harn\"\n",
1504 )
1505 .unwrap();
1506 let entry = write_file(root, "entry.harn", "");
1507
1508 let resolved = resolve_import_path(&entry, "acme/leak");
1509 assert!(resolved.is_none(), "package export escaped package root");
1510 }
1511
1512 #[test]
1513 fn package_export_map_allows_symlinked_path_dependencies() {
1514 let tmp = tempfile::tempdir().unwrap();
1515 let root = tmp.path();
1516 let source = root.join("source-package");
1517 fs::create_dir_all(source.join("runtime")).unwrap();
1518 fs::write(
1519 source.join("harn.toml"),
1520 "[exports]\ncapabilities = \"runtime/capabilities.harn\"\n",
1521 )
1522 .unwrap();
1523 fs::write(
1524 source.join("runtime/capabilities.harn"),
1525 "pub fn exported_capability() { 1 }\n",
1526 )
1527 .unwrap();
1528 fs::create_dir_all(root.join(".harn/packages")).unwrap();
1529 #[cfg(unix)]
1530 std::os::unix::fs::symlink(&source, root.join(".harn/packages/acme")).unwrap();
1531 #[cfg(windows)]
1532 std::os::windows::fs::symlink_dir(&source, root.join(".harn/packages/acme")).unwrap();
1533 let entry = write_file(root, "entry.harn", "");
1534
1535 let resolved = resolve_import_path(&entry, "acme/capabilities")
1536 .expect("symlinked package export should resolve");
1537 assert!(resolved.ends_with("runtime/capabilities.harn"));
1538 }
1539
1540 #[test]
1541 fn package_imports_resolve_from_nested_package_module() {
1542 let tmp = tempfile::tempdir().unwrap();
1543 let root = tmp.path();
1544 fs::create_dir_all(root.join(".git")).unwrap();
1545 fs::create_dir_all(root.join(".harn/packages/acme")).unwrap();
1546 fs::create_dir_all(root.join(".harn/packages/shared")).unwrap();
1547 fs::write(
1548 root.join(".harn/packages/shared/lib.harn"),
1549 "pub fn shared_helper() { 1 }\n",
1550 )
1551 .unwrap();
1552 fs::write(
1553 root.join(".harn/packages/acme/lib.harn"),
1554 "import \"shared\"\npub fn use_shared() { shared_helper() }\n",
1555 )
1556 .unwrap();
1557 let entry = write_file(root, "entry.harn", "import \"acme\"\nuse_shared()\n");
1558
1559 let graph = build(std::slice::from_ref(&entry));
1560 let imported = graph
1561 .imported_names_for_file(&entry)
1562 .expect("nested package import should resolve");
1563 assert!(imported.contains("use_shared"));
1564 let acme_path = root.join(".harn/packages/acme/lib.harn");
1565 let acme_imports = graph
1566 .imported_names_for_file(&acme_path)
1567 .expect("package module imports should resolve");
1568 assert!(acme_imports.contains("shared_helper"));
1569 }
1570
1571 #[test]
1572 fn unknown_stdlib_import_is_unresolved() {
1573 let tmp = tempfile::tempdir().unwrap();
1574 let root = tmp.path();
1575 let entry = write_file(root, "entry.harn", "import \"std/does_not_exist\"\n");
1576
1577 let graph = build(std::slice::from_ref(&entry));
1578 assert!(
1579 graph.imported_names_for_file(&entry).is_none(),
1580 "unknown std module should fail resolution and disable strict check"
1581 );
1582 }
1583
1584 #[test]
1585 fn import_cycles_do_not_loop_forever() {
1586 let tmp = tempfile::tempdir().unwrap();
1587 let root = tmp.path();
1588 write_file(root, "a.harn", "import \"./b\"\npub fn a_fn() { 1 }\n");
1589 write_file(root, "b.harn", "import \"./a\"\npub fn b_fn() { 1 }\n");
1590 let entry = root.join("a.harn");
1591
1592 let graph = build(std::slice::from_ref(&entry));
1594 let imported = graph
1595 .imported_names_for_file(&entry)
1596 .expect("cyclic imports still resolve to known exports");
1597 assert!(imported.contains("b_fn"));
1598 }
1599
1600 #[test]
1601 fn pub_import_selective_re_exports_named_symbols() {
1602 let tmp = tempfile::tempdir().unwrap();
1603 let root = tmp.path();
1604 write_file(
1605 root,
1606 "src.harn",
1607 "pub fn alpha() { 1 }\npub fn beta() { 2 }\n",
1608 );
1609 write_file(root, "facade.harn", "pub import { alpha } from \"./src\"\n");
1610 let entry = write_file(root, "entry.harn", "import \"./facade\"\nalpha()\n");
1611
1612 let graph = build(std::slice::from_ref(&entry));
1613 let imported = graph
1614 .imported_names_for_file(&entry)
1615 .expect("entry should resolve");
1616 assert!(imported.contains("alpha"), "selective re-export missing");
1617 assert!(
1618 !imported.contains("beta"),
1619 "non-listed name leaked through facade"
1620 );
1621
1622 let facade_path = root.join("facade.harn");
1623 let def = graph
1624 .definition_of(&facade_path, "alpha")
1625 .expect("definition_of should chase re-export");
1626 assert!(def.file.ends_with("src.harn"));
1627 }
1628
1629 #[test]
1630 fn pub_import_wildcard_re_exports_full_surface() {
1631 let tmp = tempfile::tempdir().unwrap();
1632 let root = tmp.path();
1633 write_file(
1634 root,
1635 "src.harn",
1636 "pub fn alpha() { 1 }\npub fn beta() { 2 }\n",
1637 );
1638 write_file(root, "facade.harn", "pub import \"./src\"\n");
1639 let entry = write_file(root, "entry.harn", "import \"./facade\"\nalpha()\n");
1640
1641 let graph = build(std::slice::from_ref(&entry));
1642 let imported = graph
1643 .imported_names_for_file(&entry)
1644 .expect("entry should resolve");
1645 assert!(imported.contains("alpha"));
1646 assert!(imported.contains("beta"));
1647 }
1648
1649 #[test]
1650 fn pub_import_chain_resolves_definition_to_origin() {
1651 let tmp = tempfile::tempdir().unwrap();
1652 let root = tmp.path();
1653 write_file(root, "inner.harn", "pub fn deep() { 1 }\n");
1654 write_file(
1655 root,
1656 "middle.harn",
1657 "pub import { deep } from \"./inner\"\n",
1658 );
1659 write_file(
1660 root,
1661 "outer.harn",
1662 "pub import { deep } from \"./middle\"\n",
1663 );
1664 let entry = write_file(
1665 root,
1666 "entry.harn",
1667 "import { deep } from \"./outer\"\ndeep()\n",
1668 );
1669
1670 let graph = build(std::slice::from_ref(&entry));
1671 let def = graph
1672 .definition_of(&entry, "deep")
1673 .expect("definition_of should follow chain");
1674 assert!(def.file.ends_with("inner.harn"));
1675
1676 let imported = graph
1677 .imported_names_for_file(&entry)
1678 .expect("entry should resolve");
1679 assert!(imported.contains("deep"));
1680 }
1681
1682 #[test]
1683 fn duplicate_pub_import_reports_re_export_conflict() {
1684 let tmp = tempfile::tempdir().unwrap();
1685 let root = tmp.path();
1686 write_file(root, "a.harn", "pub fn shared() { 1 }\n");
1687 write_file(root, "b.harn", "pub fn shared() { 2 }\n");
1688 let facade = write_file(
1689 root,
1690 "facade.harn",
1691 "pub import { shared } from \"./a\"\npub import { shared } from \"./b\"\n",
1692 );
1693
1694 let graph = build(std::slice::from_ref(&facade));
1695 let conflicts = graph.re_export_conflicts(&facade);
1696 assert_eq!(
1697 conflicts.len(),
1698 1,
1699 "expected exactly one re-export conflict, got {conflicts:?}"
1700 );
1701 assert_eq!(conflicts[0].name, "shared");
1702 assert_eq!(conflicts[0].sources.len(), 2);
1703 }
1704
1705 #[test]
1706 fn cross_directory_cycle_does_not_explode_module_count() {
1707 let tmp = tempfile::tempdir().unwrap();
1715 let root = tmp.path();
1716 let context = root.join("context");
1717 let runtime = root.join("runtime");
1718 fs::create_dir_all(&context).unwrap();
1719 fs::create_dir_all(&runtime).unwrap();
1720 write_file(
1721 &context,
1722 "a.harn",
1723 "import \"../runtime/b\"\npub fn a_fn() { 1 }\n",
1724 );
1725 write_file(
1726 &runtime,
1727 "b.harn",
1728 "import \"../context/a\"\npub fn b_fn() { 1 }\n",
1729 );
1730 let entry = context.join("a.harn");
1731
1732 let graph = build(std::slice::from_ref(&entry));
1733 assert_eq!(
1736 graph.modules.len(),
1737 2,
1738 "cross-directory cycle loaded {} modules, expected 2",
1739 graph.modules.len()
1740 );
1741 let imported = graph
1742 .imported_names_for_file(&entry)
1743 .expect("cyclic imports still resolve to known exports");
1744 assert!(imported.contains("b_fn"));
1745 }
1746}