Skip to main content

harn_modules/
package_imports.rs

1use std::collections::{HashMap, HashSet};
2use std::path::{Component, Path, PathBuf};
3
4use serde::Deserialize;
5
6use crate::package_snapshot::PackageSnapshot;
7
8#[derive(Debug, Default, Deserialize)]
9struct PackageManifest {
10    #[serde(default)]
11    exports: HashMap<String, String>,
12}
13
14/// Resolve an import string relative to the importing file.
15///
16/// Returns the path as constructed so callers can compare it with their own
17/// `PathBuf::join` result. The module graph canonicalizes its internal keys.
18pub fn resolve_import_path(current_file: &Path, import_path: &str) -> Option<PathBuf> {
19    let snapshots = PackageSnapshot::acquire_nearest(current_file)
20        .ok()
21        .flatten()
22        .into_iter()
23        .collect::<Vec<_>>();
24    resolve_import_path_with_snapshots(current_file, import_path, &snapshots)
25}
26
27pub(crate) fn resolve_import_path_with_snapshots(
28    current_file: &Path,
29    import_path: &str,
30    package_snapshots: &[PackageSnapshot],
31) -> Option<PathBuf> {
32    if let Some(module) = import_path
33        .strip_prefix("std/")
34        .or_else(|| (import_path == "observability").then_some("observability"))
35    {
36        return super::stdlib::get_stdlib_source(module)
37            .map(|_| super::stdlib::stdlib_virtual_path(module));
38    }
39
40    let base = current_file.parent().unwrap_or(Path::new("."));
41    let mut file_path = base.join(import_path);
42    if !file_path.exists() && file_path.extension().is_none() {
43        file_path.set_extension("harn");
44    }
45    if file_path.exists() {
46        return Some(file_path);
47    }
48
49    resolve_package_import(current_file, import_path, package_snapshots)
50}
51
52pub(crate) fn acquire_package_snapshots(files: &[PathBuf]) -> Vec<PackageSnapshot> {
53    let mut roots = HashSet::new();
54    let mut snapshots = Vec::new();
55    for file in files {
56        let Ok(Some(snapshot)) = PackageSnapshot::acquire_nearest(file) else {
57            continue;
58        };
59        if roots.insert(snapshot.project_root().to_path_buf()) {
60            snapshots.push(snapshot);
61        }
62    }
63    snapshots
64}
65
66fn resolve_package_import(
67    current_file: &Path,
68    import_path: &str,
69    package_snapshots: &[PackageSnapshot],
70) -> Option<PathBuf> {
71    let current_file = canonicalize_with_existing_parent(current_file);
72    package_snapshots
73        .iter()
74        .filter(|snapshot| current_file.starts_with(snapshot.project_root()))
75        .max_by_key(|snapshot| snapshot.project_root().components().count())
76        .and_then(|snapshot| resolve_from_packages_root(snapshot.packages_root(), import_path))
77}
78
79fn canonicalize_with_existing_parent(path: &Path) -> PathBuf {
80    path.canonicalize().unwrap_or_else(|_| {
81        path.parent()
82            .and_then(|parent| parent.canonicalize().ok())
83            .and_then(|parent| path.file_name().map(|name| parent.join(name)))
84            .unwrap_or_else(|| path.to_path_buf())
85    })
86}
87
88fn resolve_from_packages_root(packages_root: &Path, import_path: &str) -> Option<PathBuf> {
89    let safe_import_path = safe_package_relative_path(import_path)?;
90    let package_name = package_name_from_relative_path(&safe_import_path)?;
91    let package_root = packages_root.join(package_name);
92
93    let direct_path = packages_root.join(&safe_import_path);
94    if let Some(path) = finalize_package_target(&package_root, &direct_path) {
95        return Some(path);
96    }
97
98    let export_name = export_name_from_relative_path(&safe_import_path)?;
99    let manifest = read_package_manifest(&package_root.join("harn.toml"))?;
100    let safe_export_path = safe_package_relative_path(manifest.exports.get(export_name)?)?;
101    finalize_package_target(&package_root, &package_root.join(safe_export_path))
102}
103
104fn read_package_manifest(path: &Path) -> Option<PackageManifest> {
105    let content = std::fs::read_to_string(path).ok()?;
106    toml::from_str(&content).ok()
107}
108
109fn safe_package_relative_path(raw: &str) -> Option<PathBuf> {
110    if raw.is_empty() || raw.contains('\\') {
111        return None;
112    }
113    let mut out = PathBuf::new();
114    let mut saw_component = false;
115    for component in Path::new(raw).components() {
116        match component {
117            Component::Normal(part) => {
118                saw_component = true;
119                out.push(part);
120            }
121            Component::CurDir => {}
122            Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
123        }
124    }
125    saw_component.then_some(out)
126}
127
128fn package_name_from_relative_path(path: &Path) -> Option<&str> {
129    match path.components().next()? {
130        Component::Normal(name) => name.to_str(),
131        _ => None,
132    }
133}
134
135fn export_name_from_relative_path(path: &Path) -> Option<&str> {
136    let mut components = path.components();
137    components.next()?;
138    let rest = components.as_path();
139    if rest.as_os_str().is_empty() {
140        None
141    } else {
142        rest.to_str()
143    }
144}
145
146fn target_within_package_root(package_root: &Path, path: PathBuf) -> Option<PathBuf> {
147    let root = package_root.canonicalize().ok()?;
148    let canonical = path.canonicalize().ok()?;
149    (canonical == root || canonical.starts_with(&root)).then_some(path)
150}
151
152fn finalize_package_target(package_root: &Path, path: &Path) -> Option<PathBuf> {
153    if path.is_dir() {
154        let lib = path.join("lib.harn");
155        return if lib.exists() {
156            target_within_package_root(package_root, lib)
157        } else {
158            target_within_package_root(package_root, path.to_path_buf())
159        };
160    }
161    if path.exists() {
162        return target_within_package_root(package_root, path.to_path_buf());
163    }
164    if path.extension().is_none() {
165        let mut with_extension = path.to_path_buf();
166        with_extension.set_extension("harn");
167        if with_extension.exists() {
168            return target_within_package_root(package_root, with_extension);
169        }
170    }
171    None
172}