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 personas;
10mod stdlib;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum DefKind {
15 Function,
16 Pipeline,
17 Tool,
18 Skill,
19 Struct,
20 Enum,
21 Interface,
22 Type,
23 Variable,
24 Parameter,
25}
26
27#[derive(Debug, Clone)]
29pub struct DefSite {
30 pub name: String,
31 pub file: PathBuf,
32 pub kind: DefKind,
33 pub span: Span,
34}
35
36#[derive(Debug, Clone)]
38pub enum WildcardResolution {
39 Resolved(HashSet<String>),
41 Unknown,
43}
44
45#[derive(Debug, Default)]
47pub struct ModuleGraph {
48 modules: HashMap<PathBuf, ModuleInfo>,
49}
50
51#[derive(Debug, Default)]
52struct ModuleInfo {
53 declarations: HashMap<String, DefSite>,
56 exports: HashSet<String>,
61 own_exports: HashSet<String>,
64 selective_re_exports: HashMap<String, Vec<PathBuf>>,
71 wildcard_re_export_paths: Vec<PathBuf>,
75 selective_import_names: HashSet<String>,
77 imports: Vec<ImportRef>,
79 has_unresolved_wildcard_import: bool,
81 has_unresolved_selective_import: bool,
85 fn_names: Vec<String>,
89 has_pub_fn: bool,
91 type_declarations: Vec<SNode>,
94}
95
96#[derive(Debug, Clone)]
97struct ImportRef {
98 path: Option<PathBuf>,
99 selective_names: Option<HashSet<String>>,
100}
101
102#[derive(Debug, Default, Deserialize)]
103struct PackageManifest {
104 #[serde(default)]
105 exports: HashMap<String, String>,
106}
107
108pub fn build(files: &[PathBuf]) -> ModuleGraph {
114 let mut modules: HashMap<PathBuf, ModuleInfo> = HashMap::new();
115 let mut seen: HashSet<PathBuf> = HashSet::new();
116 let mut queue: VecDeque<PathBuf> = VecDeque::new();
117 for file in files {
118 let canonical = normalize_path(file);
119 if seen.insert(canonical.clone()) {
120 queue.push_back(canonical);
121 }
122 }
123 while let Some(path) = queue.pop_front() {
124 if modules.contains_key(&path) {
125 continue;
126 }
127 let module = load_module(&path);
128 for import in &module.imports {
145 if let Some(import_path) = &import.path {
146 let canonical = normalize_path(import_path);
147 if seen.insert(canonical.clone()) {
148 queue.push_back(canonical);
149 }
150 }
151 }
152 modules.insert(path, module);
153 }
154 resolve_re_exports(&mut modules);
155 ModuleGraph { modules }
156}
157
158fn resolve_re_exports(modules: &mut HashMap<PathBuf, ModuleInfo>) {
163 let keys: Vec<PathBuf> = modules.keys().cloned().collect();
164 loop {
165 let mut changed = false;
166 for path in &keys {
167 let wildcard_paths = modules
170 .get(path)
171 .map(|m| m.wildcard_re_export_paths.clone())
172 .unwrap_or_default();
173 if wildcard_paths.is_empty() {
174 continue;
175 }
176 let mut additions: Vec<String> = Vec::new();
177 for src in &wildcard_paths {
178 let src_canonical = normalize_path(src);
179 if let Some(src_module) = modules.get(src).or_else(|| modules.get(&src_canonical)) {
180 additions.extend(src_module.exports.iter().cloned());
181 }
182 }
183 if let Some(module) = modules.get_mut(path) {
184 for name in additions {
185 if module.exports.insert(name) {
186 changed = true;
187 }
188 }
189 }
190 }
191 if !changed {
192 break;
193 }
194 }
195}
196
197pub fn resolve_import_path(current_file: &Path, import_path: &str) -> Option<PathBuf> {
210 if let Some(module) = import_path.strip_prefix("std/") {
211 if stdlib::get_stdlib_source(module).is_some() {
212 return Some(stdlib::stdlib_virtual_path(module));
213 }
214 return None;
215 }
216
217 let base = current_file.parent().unwrap_or(Path::new("."));
218 let mut file_path = base.join(import_path);
219 if !file_path.exists() && file_path.extension().is_none() {
220 file_path.set_extension("harn");
221 }
222 if file_path.exists() {
223 return Some(file_path);
224 }
225
226 if let Some(path) = resolve_package_import(base, import_path) {
227 return Some(path);
228 }
229
230 None
231}
232
233fn resolve_package_import(base: &Path, import_path: &str) -> Option<PathBuf> {
234 for anchor in base.ancestors() {
235 let packages_root = anchor.join(".harn/packages");
236 if !packages_root.is_dir() {
237 if anchor.join(".git").exists() {
238 break;
239 }
240 continue;
241 }
242 if let Some(path) = resolve_from_packages_root(&packages_root, import_path) {
243 return Some(path);
244 }
245 if anchor.join(".git").exists() {
246 break;
247 }
248 }
249 None
250}
251
252fn resolve_from_packages_root(packages_root: &Path, import_path: &str) -> Option<PathBuf> {
253 let safe_import_path = safe_package_relative_path(import_path)?;
254 let package_name = package_name_from_relative_path(&safe_import_path)?;
255 let package_root = packages_root.join(package_name);
256
257 let pkg_path = packages_root.join(&safe_import_path);
258 if let Some(path) = finalize_package_target(&package_root, &pkg_path) {
259 return Some(path);
260 }
261
262 let export_name = export_name_from_relative_path(&safe_import_path)?;
263 let manifest_path = packages_root.join(package_name).join("harn.toml");
264 let manifest = read_package_manifest(&manifest_path)?;
265 let rel_path = manifest.exports.get(export_name)?;
266 let safe_export_path = safe_package_relative_path(rel_path)?;
267 finalize_package_target(&package_root, &package_root.join(safe_export_path))
268}
269
270fn read_package_manifest(path: &Path) -> Option<PackageManifest> {
271 let content = std::fs::read_to_string(path).ok()?;
272 toml::from_str::<PackageManifest>(&content).ok()
273}
274
275fn safe_package_relative_path(raw: &str) -> Option<PathBuf> {
276 if raw.is_empty() || raw.contains('\\') {
277 return None;
278 }
279 let mut out = PathBuf::new();
280 let mut saw_component = false;
281 for component in Path::new(raw).components() {
282 match component {
283 Component::Normal(part) => {
284 saw_component = true;
285 out.push(part);
286 }
287 Component::CurDir => {}
288 Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
289 }
290 }
291 saw_component.then_some(out)
292}
293
294fn package_name_from_relative_path(path: &Path) -> Option<&str> {
295 match path.components().next()? {
296 Component::Normal(name) => name.to_str(),
297 _ => None,
298 }
299}
300
301fn export_name_from_relative_path(path: &Path) -> Option<&str> {
302 let mut components = path.components();
303 components.next()?;
304 let rest = components.as_path();
305 if rest.as_os_str().is_empty() {
306 None
307 } else {
308 rest.to_str()
309 }
310}
311
312fn path_is_within(root: &Path, path: &Path) -> bool {
313 let Ok(root) = root.canonicalize() else {
314 return false;
315 };
316 let Ok(path) = path.canonicalize() else {
317 return false;
318 };
319 path == root || path.starts_with(root)
320}
321
322fn target_within_package_root(package_root: &Path, path: PathBuf) -> Option<PathBuf> {
323 path_is_within(package_root, &path).then_some(path)
324}
325
326fn finalize_package_target(package_root: &Path, path: &Path) -> Option<PathBuf> {
327 if path.is_dir() {
328 let lib = path.join("lib.harn");
329 if lib.exists() {
330 return target_within_package_root(package_root, lib);
331 }
332 return target_within_package_root(package_root, path.to_path_buf());
333 }
334 if path.exists() {
335 return target_within_package_root(package_root, path.to_path_buf());
336 }
337 if path.extension().is_none() {
338 let mut with_ext = path.to_path_buf();
339 with_ext.set_extension("harn");
340 if with_ext.exists() {
341 return target_within_package_root(package_root, with_ext);
342 }
343 }
344 None
345}
346
347impl ModuleGraph {
348 pub fn all_selective_import_names(&self) -> HashSet<&str> {
350 let mut names = HashSet::new();
351 for module in self.modules.values() {
352 for name in &module.selective_import_names {
353 names.insert(name.as_str());
354 }
355 }
356 names
357 }
358
359 pub fn wildcard_exports_for(&self, file: &Path) -> WildcardResolution {
364 let file = normalize_path(file);
365 let Some(module) = self.modules.get(&file) else {
366 return WildcardResolution::Unknown;
367 };
368 if module.has_unresolved_wildcard_import {
369 return WildcardResolution::Unknown;
370 }
371
372 let mut names = HashSet::new();
373 for import in module
374 .imports
375 .iter()
376 .filter(|import| import.selective_names.is_none())
377 {
378 let Some(import_path) = &import.path else {
379 return WildcardResolution::Unknown;
380 };
381 let imported = self.modules.get(import_path).or_else(|| {
382 let normalized = normalize_path(import_path);
383 self.modules.get(&normalized)
384 });
385 let Some(imported) = imported else {
386 return WildcardResolution::Unknown;
387 };
388 names.extend(imported.exports.iter().cloned());
389 }
390 WildcardResolution::Resolved(names)
391 }
392
393 pub fn imported_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
408 let file = normalize_path(file);
409 let module = self.modules.get(&file)?;
410 if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
411 return None;
412 }
413
414 let mut names = HashSet::new();
415 for import in &module.imports {
416 let import_path = import.path.as_ref()?;
417 let imported = self
418 .modules
419 .get(import_path)
420 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
421 match &import.selective_names {
422 None => {
423 names.extend(imported.exports.iter().cloned());
424 }
425 Some(selective) => {
426 for name in selective {
427 if imported.declarations.contains_key(name)
428 || imported.exports.contains(name)
429 {
430 names.insert(name.clone());
431 }
432 }
433 }
434 }
435 }
436 Some(names)
437 }
438
439 pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
443 let file = normalize_path(file);
444 let module = self.modules.get(&file)?;
445 if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
446 return None;
447 }
448
449 let mut decls = Vec::new();
450 for import in &module.imports {
451 let import_path = import.path.as_ref()?;
452 let imported = self
453 .modules
454 .get(import_path)
455 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
456 let names_to_collect: Vec<String> = match &import.selective_names {
457 None => imported.exports.iter().cloned().collect(),
458 Some(selective) => selective.iter().cloned().collect(),
459 };
460 for name in &names_to_collect {
461 let mut visited = HashSet::new();
462 if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
463 decls.push(decl);
464 }
465 }
466 }
467 Some(decls)
468 }
469
470 fn find_exported_type_decl(
473 &self,
474 path: &Path,
475 name: &str,
476 visited: &mut HashSet<PathBuf>,
477 ) -> Option<SNode> {
478 let canonical = normalize_path(path);
479 if !visited.insert(canonical.clone()) {
480 return None;
481 }
482 let module = self
483 .modules
484 .get(&canonical)
485 .or_else(|| self.modules.get(path))?;
486 for decl in &module.type_declarations {
487 if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
488 return Some(decl.clone());
489 }
490 }
491 if let Some(sources) = module.selective_re_exports.get(name) {
492 for source in sources {
493 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
494 return Some(decl);
495 }
496 }
497 }
498 for source in &module.wildcard_re_export_paths {
499 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
500 return Some(decl);
501 }
502 }
503 None
504 }
505
506 pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
512 let mut visited = HashSet::new();
513 self.definition_of_inner(file, name, &mut visited)
514 }
515
516 fn definition_of_inner(
517 &self,
518 file: &Path,
519 name: &str,
520 visited: &mut HashSet<PathBuf>,
521 ) -> Option<DefSite> {
522 let file = normalize_path(file);
523 if !visited.insert(file.clone()) {
524 return None;
525 }
526 let current = self.modules.get(&file)?;
527
528 if let Some(local) = current.declarations.get(name) {
529 return Some(local.clone());
530 }
531
532 if let Some(sources) = current.selective_re_exports.get(name) {
537 for source in sources {
538 if let Some(def) = self.definition_of_inner(source, name, visited) {
539 return Some(def);
540 }
541 }
542 }
543
544 for source in ¤t.wildcard_re_export_paths {
546 if let Some(def) = self.definition_of_inner(source, name, visited) {
547 return Some(def);
548 }
549 }
550
551 for import in ¤t.imports {
553 let Some(selective_names) = &import.selective_names else {
554 continue;
555 };
556 if !selective_names.contains(name) {
557 continue;
558 }
559 if let Some(path) = &import.path {
560 if let Some(def) = self.definition_of_inner(path, name, visited) {
561 return Some(def);
562 }
563 }
564 }
565
566 for import in ¤t.imports {
568 if import.selective_names.is_some() {
569 continue;
570 }
571 if let Some(path) = &import.path {
572 if let Some(def) = self.definition_of_inner(path, name, visited) {
573 return Some(def);
574 }
575 }
576 }
577
578 None
579 }
580
581 pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
585 let file = normalize_path(file);
586 let Some(module) = self.modules.get(&file) else {
587 return Vec::new();
588 };
589
590 let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
594
595 for (name, srcs) in &module.selective_re_exports {
596 sources
597 .entry(name.clone())
598 .or_default()
599 .extend(srcs.iter().cloned());
600 }
601 for src in &module.wildcard_re_export_paths {
602 let canonical = normalize_path(src);
603 let Some(src_module) = self
604 .modules
605 .get(&canonical)
606 .or_else(|| self.modules.get(src))
607 else {
608 continue;
609 };
610 for name in &src_module.exports {
611 sources
612 .entry(name.clone())
613 .or_default()
614 .push(canonical.clone());
615 }
616 }
617
618 for name in &module.own_exports {
622 if let Some(entry) = sources.get_mut(name) {
623 entry.push(file.clone());
624 }
625 }
626
627 let mut conflicts = Vec::new();
628 for (name, mut srcs) in sources {
629 srcs.sort();
630 srcs.dedup();
631 if srcs.len() > 1 {
632 conflicts.push(ReExportConflict {
633 name,
634 sources: srcs,
635 });
636 }
637 }
638 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
639 conflicts
640 }
641}
642
643#[derive(Debug, Clone, PartialEq, Eq)]
646pub struct ReExportConflict {
647 pub name: String,
648 pub sources: Vec<PathBuf>,
649}
650
651fn load_module(path: &Path) -> ModuleInfo {
652 let source = if let Some(stdlib_module) = stdlib_module_from_path(path) {
655 match stdlib::get_stdlib_source(stdlib_module) {
656 Some(src) => src.to_string(),
657 None => return ModuleInfo::default(),
658 }
659 } else {
660 match std::fs::read_to_string(path) {
661 Ok(src) => src,
662 Err(_) => return ModuleInfo::default(),
663 }
664 };
665 let mut lexer = harn_lexer::Lexer::new(&source);
666 let tokens = match lexer.tokenize() {
667 Ok(tokens) => tokens,
668 Err(_) => return ModuleInfo::default(),
669 };
670 let mut parser = Parser::new(tokens);
671 let program = match parser.parse() {
672 Ok(program) => program,
673 Err(_) => return ModuleInfo::default(),
674 };
675
676 let mut module = ModuleInfo::default();
677 for node in &program {
678 collect_module_info(path, node, &mut module);
679 collect_type_declarations(node, &mut module.type_declarations);
680 }
681 if !module.has_pub_fn {
684 for name in &module.fn_names {
685 module.own_exports.insert(name.clone());
686 }
687 }
688 module.exports.extend(module.own_exports.iter().cloned());
692 module
693 .exports
694 .extend(module.selective_re_exports.keys().cloned());
695 module
696}
697
698fn stdlib_module_from_path(path: &Path) -> Option<&str> {
701 let s = path.to_str()?;
702 s.strip_prefix("<std>/")
703}
704
705fn collect_module_info(file: &Path, snode: &SNode, module: &mut ModuleInfo) {
706 match &snode.node {
707 Node::FnDecl {
708 name,
709 params,
710 is_pub,
711 ..
712 } => {
713 if *is_pub {
714 module.own_exports.insert(name.clone());
715 module.has_pub_fn = true;
716 }
717 module.fn_names.push(name.clone());
718 module.declarations.insert(
719 name.clone(),
720 decl_site(file, snode.span, name, DefKind::Function),
721 );
722 for param_name in params.iter().map(|param| param.name.clone()) {
723 module.declarations.insert(
724 param_name.clone(),
725 decl_site(file, snode.span, ¶m_name, DefKind::Parameter),
726 );
727 }
728 }
729 Node::Pipeline { name, is_pub, .. } => {
730 if *is_pub {
731 module.own_exports.insert(name.clone());
732 }
733 module.declarations.insert(
734 name.clone(),
735 decl_site(file, snode.span, name, DefKind::Pipeline),
736 );
737 }
738 Node::ToolDecl { name, is_pub, .. } => {
739 if *is_pub {
740 module.own_exports.insert(name.clone());
741 }
742 module.declarations.insert(
743 name.clone(),
744 decl_site(file, snode.span, name, DefKind::Tool),
745 );
746 }
747 Node::SkillDecl { name, is_pub, .. } => {
748 if *is_pub {
749 module.own_exports.insert(name.clone());
750 }
751 module.declarations.insert(
752 name.clone(),
753 decl_site(file, snode.span, name, DefKind::Skill),
754 );
755 }
756 Node::StructDecl { name, is_pub, .. } => {
757 if *is_pub {
758 module.own_exports.insert(name.clone());
759 }
760 module.declarations.insert(
761 name.clone(),
762 decl_site(file, snode.span, name, DefKind::Struct),
763 );
764 }
765 Node::EnumDecl { name, is_pub, .. } => {
766 if *is_pub {
767 module.own_exports.insert(name.clone());
768 }
769 module.declarations.insert(
770 name.clone(),
771 decl_site(file, snode.span, name, DefKind::Enum),
772 );
773 }
774 Node::InterfaceDecl { name, .. } => {
775 module.own_exports.insert(name.clone());
776 module.declarations.insert(
777 name.clone(),
778 decl_site(file, snode.span, name, DefKind::Interface),
779 );
780 }
781 Node::TypeDecl { name, .. } => {
782 module.own_exports.insert(name.clone());
783 module.declarations.insert(
784 name.clone(),
785 decl_site(file, snode.span, name, DefKind::Type),
786 );
787 }
788 Node::LetBinding { pattern, .. } | Node::VarBinding { pattern, .. } => {
789 for name in pattern_names(pattern) {
790 module.declarations.insert(
791 name.clone(),
792 decl_site(file, snode.span, &name, DefKind::Variable),
793 );
794 }
795 }
796 Node::ImportDecl { path, is_pub } => {
797 let import_path = resolve_import_path(file, path);
798 if import_path.is_none() {
799 module.has_unresolved_wildcard_import = true;
800 }
801 if *is_pub {
802 if let Some(resolved) = &import_path {
803 module
804 .wildcard_re_export_paths
805 .push(normalize_path(resolved));
806 }
807 }
808 module.imports.push(ImportRef {
809 path: import_path,
810 selective_names: None,
811 });
812 }
813 Node::SelectiveImport {
814 names,
815 path,
816 is_pub,
817 } => {
818 let import_path = resolve_import_path(file, path);
819 if import_path.is_none() {
820 module.has_unresolved_selective_import = true;
821 }
822 if *is_pub {
823 if let Some(resolved) = &import_path {
824 let canonical = normalize_path(resolved);
825 for name in names {
826 module
827 .selective_re_exports
828 .entry(name.clone())
829 .or_default()
830 .push(canonical.clone());
831 }
832 }
833 }
834 let names: HashSet<String> = names.iter().cloned().collect();
835 module.selective_import_names.extend(names.iter().cloned());
836 module.imports.push(ImportRef {
837 path: import_path,
838 selective_names: Some(names),
839 });
840 }
841 Node::AttributedDecl { inner, .. } => {
842 collect_module_info(file, inner, module);
843 }
844 _ => {}
845 }
846}
847
848fn collect_type_declarations(snode: &SNode, decls: &mut Vec<SNode>) {
849 match &snode.node {
850 Node::TypeDecl { .. }
851 | Node::StructDecl { .. }
852 | Node::EnumDecl { .. }
853 | Node::InterfaceDecl { .. } => decls.push(snode.clone()),
854 Node::AttributedDecl { inner, .. } => collect_type_declarations(inner, decls),
855 _ => {}
856 }
857}
858
859fn type_decl_name(snode: &SNode) -> Option<&str> {
860 match &snode.node {
861 Node::TypeDecl { name, .. }
862 | Node::StructDecl { name, .. }
863 | Node::EnumDecl { name, .. }
864 | Node::InterfaceDecl { name, .. } => Some(name.as_str()),
865 _ => None,
866 }
867}
868
869fn decl_site(file: &Path, span: Span, name: &str, kind: DefKind) -> DefSite {
870 DefSite {
871 name: name.to_string(),
872 file: file.to_path_buf(),
873 kind,
874 span,
875 }
876}
877
878fn pattern_names(pattern: &BindingPattern) -> Vec<String> {
879 match pattern {
880 BindingPattern::Identifier(name) => vec![name.clone()],
881 BindingPattern::Dict(fields) => fields
882 .iter()
883 .filter_map(|field| field.alias.as_ref().or(Some(&field.key)).cloned())
884 .collect(),
885 BindingPattern::List(elements) => elements
886 .iter()
887 .map(|element| element.name.clone())
888 .collect(),
889 BindingPattern::Pair(a, b) => vec![a.clone(), b.clone()],
890 }
891}
892
893fn normalize_path(path: &Path) -> PathBuf {
894 if stdlib_module_from_path(path).is_some() {
895 return path.to_path_buf();
896 }
897 path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
898}
899
900#[cfg(test)]
901mod tests {
902 use super::*;
903 use std::fs;
904
905 fn write_file(dir: &Path, name: &str, contents: &str) -> PathBuf {
906 let path = dir.join(name);
907 fs::write(&path, contents).unwrap();
908 path
909 }
910
911 #[test]
912 fn recursive_build_loads_transitively_imported_modules() {
913 let tmp = tempfile::tempdir().unwrap();
914 let root = tmp.path();
915 write_file(root, "leaf.harn", "pub fn leaf_fn() { 1 }\n");
916 write_file(
917 root,
918 "mid.harn",
919 "import \"./leaf\"\npub fn mid_fn() { leaf_fn() }\n",
920 );
921 let entry = write_file(root, "entry.harn", "import \"./mid\"\nmid_fn()\n");
922
923 let graph = build(std::slice::from_ref(&entry));
924 let imported = graph
925 .imported_names_for_file(&entry)
926 .expect("entry imports should resolve");
927 assert!(imported.contains("mid_fn"));
929 assert!(!imported.contains("leaf_fn"));
930
931 let leaf_path = root.join("leaf.harn");
934 assert!(graph.definition_of(&leaf_path, "leaf_fn").is_some());
935 }
936
937 #[test]
938 fn imported_names_returns_none_when_import_unresolved() {
939 let tmp = tempfile::tempdir().unwrap();
940 let root = tmp.path();
941 let entry = write_file(root, "entry.harn", "import \"./does_not_exist\"\n");
942
943 let graph = build(std::slice::from_ref(&entry));
944 assert!(graph.imported_names_for_file(&entry).is_none());
945 }
946
947 #[test]
948 fn selective_imports_contribute_only_requested_names() {
949 let tmp = tempfile::tempdir().unwrap();
950 let root = tmp.path();
951 write_file(root, "util.harn", "pub fn a() { 1 }\npub fn b() { 2 }\n");
952 let entry = write_file(root, "entry.harn", "import { a } from \"./util\"\n");
953
954 let graph = build(std::slice::from_ref(&entry));
955 let imported = graph
956 .imported_names_for_file(&entry)
957 .expect("entry imports should resolve");
958 assert!(imported.contains("a"));
959 assert!(!imported.contains("b"));
960 }
961
962 #[test]
963 fn stdlib_imports_resolve_to_embedded_sources() {
964 let tmp = tempfile::tempdir().unwrap();
965 let root = tmp.path();
966 let entry = write_file(root, "entry.harn", "import \"std/math\"\nclamp(5, 0, 10)\n");
967
968 let graph = build(std::slice::from_ref(&entry));
969 let imported = graph
970 .imported_names_for_file(&entry)
971 .expect("std/math should resolve");
972 assert!(imported.contains("clamp"));
974 }
975
976 #[test]
977 fn runtime_stdlib_import_surface_resolves_to_embedded_sources() {
978 let tmp = tempfile::tempdir().unwrap();
979 let entry = write_file(tmp.path(), "entry.harn", "");
980
981 for (module, _) in stdlib::STDLIB_SOURCES {
982 let import_path = format!("std/{module}");
983 assert!(
984 resolve_import_path(&entry, &import_path).is_some(),
985 "{import_path} should resolve in the module graph"
986 );
987 }
988 }
989
990 #[test]
991 fn stdlib_imports_expose_type_declarations() {
992 let tmp = tempfile::tempdir().unwrap();
993 let root = tmp.path();
994 let entry = write_file(
995 root,
996 "entry.harn",
997 "import \"std/triggers\"\nlet provider = \"github\"\n",
998 );
999
1000 let graph = build(std::slice::from_ref(&entry));
1001 let decls = graph
1002 .imported_type_declarations_for_file(&entry)
1003 .expect("std/triggers type declarations should resolve");
1004 let names: HashSet<String> = decls
1005 .iter()
1006 .filter_map(type_decl_name)
1007 .map(ToString::to_string)
1008 .collect();
1009 assert!(names.contains("TriggerEvent"));
1010 assert!(names.contains("ProviderPayload"));
1011 assert!(names.contains("SignatureStatus"));
1012 }
1013
1014 #[test]
1015 fn package_export_map_resolves_declared_module() {
1016 let tmp = tempfile::tempdir().unwrap();
1017 let root = tmp.path();
1018 let packages = root.join(".harn/packages/acme/runtime");
1019 fs::create_dir_all(&packages).unwrap();
1020 fs::write(
1021 root.join(".harn/packages/acme/harn.toml"),
1022 "[exports]\ncapabilities = \"runtime/capabilities.harn\"\n",
1023 )
1024 .unwrap();
1025 fs::write(
1026 packages.join("capabilities.harn"),
1027 "pub fn exported_capability() { 1 }\n",
1028 )
1029 .unwrap();
1030 let entry = write_file(
1031 root,
1032 "entry.harn",
1033 "import \"acme/capabilities\"\nexported_capability()\n",
1034 );
1035
1036 let graph = build(std::slice::from_ref(&entry));
1037 let imported = graph
1038 .imported_names_for_file(&entry)
1039 .expect("package export should resolve");
1040 assert!(imported.contains("exported_capability"));
1041 }
1042
1043 #[test]
1044 fn package_direct_import_cannot_escape_packages_root() {
1045 let tmp = tempfile::tempdir().unwrap();
1046 let root = tmp.path();
1047 fs::create_dir_all(root.join(".harn/packages/acme")).unwrap();
1048 fs::write(root.join("secret.harn"), "pub fn leaked() { 1 }\n").unwrap();
1049 let entry = write_file(root, "entry.harn", "");
1050
1051 let resolved = resolve_import_path(&entry, "acme/../../secret");
1052 assert!(resolved.is_none(), "package import escaped package root");
1053 }
1054
1055 #[test]
1056 fn package_export_map_cannot_escape_package_root() {
1057 let tmp = tempfile::tempdir().unwrap();
1058 let root = tmp.path();
1059 fs::create_dir_all(root.join(".harn/packages/acme")).unwrap();
1060 fs::write(root.join("secret.harn"), "pub fn leaked() { 1 }\n").unwrap();
1061 fs::write(
1062 root.join(".harn/packages/acme/harn.toml"),
1063 "[exports]\nleak = \"../../secret.harn\"\n",
1064 )
1065 .unwrap();
1066 let entry = write_file(root, "entry.harn", "");
1067
1068 let resolved = resolve_import_path(&entry, "acme/leak");
1069 assert!(resolved.is_none(), "package export escaped package root");
1070 }
1071
1072 #[test]
1073 fn package_export_map_allows_symlinked_path_dependencies() {
1074 let tmp = tempfile::tempdir().unwrap();
1075 let root = tmp.path();
1076 let source = root.join("source-package");
1077 fs::create_dir_all(source.join("runtime")).unwrap();
1078 fs::write(
1079 source.join("harn.toml"),
1080 "[exports]\ncapabilities = \"runtime/capabilities.harn\"\n",
1081 )
1082 .unwrap();
1083 fs::write(
1084 source.join("runtime/capabilities.harn"),
1085 "pub fn exported_capability() { 1 }\n",
1086 )
1087 .unwrap();
1088 fs::create_dir_all(root.join(".harn/packages")).unwrap();
1089 #[cfg(unix)]
1090 std::os::unix::fs::symlink(&source, root.join(".harn/packages/acme")).unwrap();
1091 #[cfg(windows)]
1092 std::os::windows::fs::symlink_dir(&source, root.join(".harn/packages/acme")).unwrap();
1093 let entry = write_file(root, "entry.harn", "");
1094
1095 let resolved = resolve_import_path(&entry, "acme/capabilities")
1096 .expect("symlinked package export should resolve");
1097 assert!(resolved.ends_with("runtime/capabilities.harn"));
1098 }
1099
1100 #[test]
1101 fn package_imports_resolve_from_nested_package_module() {
1102 let tmp = tempfile::tempdir().unwrap();
1103 let root = tmp.path();
1104 fs::create_dir_all(root.join(".git")).unwrap();
1105 fs::create_dir_all(root.join(".harn/packages/acme")).unwrap();
1106 fs::create_dir_all(root.join(".harn/packages/shared")).unwrap();
1107 fs::write(
1108 root.join(".harn/packages/shared/lib.harn"),
1109 "pub fn shared_helper() { 1 }\n",
1110 )
1111 .unwrap();
1112 fs::write(
1113 root.join(".harn/packages/acme/lib.harn"),
1114 "import \"shared\"\npub fn use_shared() { shared_helper() }\n",
1115 )
1116 .unwrap();
1117 let entry = write_file(root, "entry.harn", "import \"acme\"\nuse_shared()\n");
1118
1119 let graph = build(std::slice::from_ref(&entry));
1120 let imported = graph
1121 .imported_names_for_file(&entry)
1122 .expect("nested package import should resolve");
1123 assert!(imported.contains("use_shared"));
1124 let acme_path = root.join(".harn/packages/acme/lib.harn");
1125 let acme_imports = graph
1126 .imported_names_for_file(&acme_path)
1127 .expect("package module imports should resolve");
1128 assert!(acme_imports.contains("shared_helper"));
1129 }
1130
1131 #[test]
1132 fn unknown_stdlib_import_is_unresolved() {
1133 let tmp = tempfile::tempdir().unwrap();
1134 let root = tmp.path();
1135 let entry = write_file(root, "entry.harn", "import \"std/does_not_exist\"\n");
1136
1137 let graph = build(std::slice::from_ref(&entry));
1138 assert!(
1139 graph.imported_names_for_file(&entry).is_none(),
1140 "unknown std module should fail resolution and disable strict check"
1141 );
1142 }
1143
1144 #[test]
1145 fn import_cycles_do_not_loop_forever() {
1146 let tmp = tempfile::tempdir().unwrap();
1147 let root = tmp.path();
1148 write_file(root, "a.harn", "import \"./b\"\npub fn a_fn() { 1 }\n");
1149 write_file(root, "b.harn", "import \"./a\"\npub fn b_fn() { 1 }\n");
1150 let entry = root.join("a.harn");
1151
1152 let graph = build(std::slice::from_ref(&entry));
1154 let imported = graph
1155 .imported_names_for_file(&entry)
1156 .expect("cyclic imports still resolve to known exports");
1157 assert!(imported.contains("b_fn"));
1158 }
1159
1160 #[test]
1161 fn pub_import_selective_re_exports_named_symbols() {
1162 let tmp = tempfile::tempdir().unwrap();
1163 let root = tmp.path();
1164 write_file(
1165 root,
1166 "src.harn",
1167 "pub fn alpha() { 1 }\npub fn beta() { 2 }\n",
1168 );
1169 write_file(root, "facade.harn", "pub import { alpha } from \"./src\"\n");
1170 let entry = write_file(root, "entry.harn", "import \"./facade\"\nalpha()\n");
1171
1172 let graph = build(std::slice::from_ref(&entry));
1173 let imported = graph
1174 .imported_names_for_file(&entry)
1175 .expect("entry should resolve");
1176 assert!(imported.contains("alpha"), "selective re-export missing");
1177 assert!(
1178 !imported.contains("beta"),
1179 "non-listed name leaked through facade"
1180 );
1181
1182 let facade_path = root.join("facade.harn");
1183 let def = graph
1184 .definition_of(&facade_path, "alpha")
1185 .expect("definition_of should chase re-export");
1186 assert!(def.file.ends_with("src.harn"));
1187 }
1188
1189 #[test]
1190 fn pub_import_wildcard_re_exports_full_surface() {
1191 let tmp = tempfile::tempdir().unwrap();
1192 let root = tmp.path();
1193 write_file(
1194 root,
1195 "src.harn",
1196 "pub fn alpha() { 1 }\npub fn beta() { 2 }\n",
1197 );
1198 write_file(root, "facade.harn", "pub import \"./src\"\n");
1199 let entry = write_file(root, "entry.harn", "import \"./facade\"\nalpha()\n");
1200
1201 let graph = build(std::slice::from_ref(&entry));
1202 let imported = graph
1203 .imported_names_for_file(&entry)
1204 .expect("entry should resolve");
1205 assert!(imported.contains("alpha"));
1206 assert!(imported.contains("beta"));
1207 }
1208
1209 #[test]
1210 fn pub_import_chain_resolves_definition_to_origin() {
1211 let tmp = tempfile::tempdir().unwrap();
1212 let root = tmp.path();
1213 write_file(root, "inner.harn", "pub fn deep() { 1 }\n");
1214 write_file(
1215 root,
1216 "middle.harn",
1217 "pub import { deep } from \"./inner\"\n",
1218 );
1219 write_file(
1220 root,
1221 "outer.harn",
1222 "pub import { deep } from \"./middle\"\n",
1223 );
1224 let entry = write_file(
1225 root,
1226 "entry.harn",
1227 "import { deep } from \"./outer\"\ndeep()\n",
1228 );
1229
1230 let graph = build(std::slice::from_ref(&entry));
1231 let def = graph
1232 .definition_of(&entry, "deep")
1233 .expect("definition_of should follow chain");
1234 assert!(def.file.ends_with("inner.harn"));
1235
1236 let imported = graph
1237 .imported_names_for_file(&entry)
1238 .expect("entry should resolve");
1239 assert!(imported.contains("deep"));
1240 }
1241
1242 #[test]
1243 fn duplicate_pub_import_reports_re_export_conflict() {
1244 let tmp = tempfile::tempdir().unwrap();
1245 let root = tmp.path();
1246 write_file(root, "a.harn", "pub fn shared() { 1 }\n");
1247 write_file(root, "b.harn", "pub fn shared() { 2 }\n");
1248 let facade = write_file(
1249 root,
1250 "facade.harn",
1251 "pub import { shared } from \"./a\"\npub import { shared } from \"./b\"\n",
1252 );
1253
1254 let graph = build(std::slice::from_ref(&facade));
1255 let conflicts = graph.re_export_conflicts(&facade);
1256 assert_eq!(
1257 conflicts.len(),
1258 1,
1259 "expected exactly one re-export conflict, got {:?}",
1260 conflicts
1261 );
1262 assert_eq!(conflicts[0].name, "shared");
1263 assert_eq!(conflicts[0].sources.len(), 2);
1264 }
1265
1266 #[test]
1267 fn cross_directory_cycle_does_not_explode_module_count() {
1268 let tmp = tempfile::tempdir().unwrap();
1276 let root = tmp.path();
1277 let context = root.join("context");
1278 let runtime = root.join("runtime");
1279 fs::create_dir_all(&context).unwrap();
1280 fs::create_dir_all(&runtime).unwrap();
1281 write_file(
1282 &context,
1283 "a.harn",
1284 "import \"../runtime/b\"\npub fn a_fn() { 1 }\n",
1285 );
1286 write_file(
1287 &runtime,
1288 "b.harn",
1289 "import \"../context/a\"\npub fn b_fn() { 1 }\n",
1290 );
1291 let entry = context.join("a.harn");
1292
1293 let graph = build(std::slice::from_ref(&entry));
1294 assert_eq!(
1297 graph.modules.len(),
1298 2,
1299 "cross-directory cycle loaded {} modules, expected 2",
1300 graph.modules.len()
1301 );
1302 let imported = graph
1303 .imported_names_for_file(&entry)
1304 .expect("cyclic imports still resolve to known exports");
1305 assert!(imported.contains("b_fn"));
1306 }
1307}