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