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