harn_modules/
package_imports.rs1use 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
14enum LocalResolution {
21 Resolved(PathBuf),
23 Rejected,
25 NotPackage,
27}
28
29fn resolve_local_import(current_file: &Path, import_path: &str) -> LocalResolution {
34 if let Some(module) = import_path
35 .strip_prefix("std/")
36 .or_else(|| (import_path == "observability").then_some("observability"))
37 {
38 return match super::stdlib::get_stdlib_source(module) {
39 Some(_) => LocalResolution::Resolved(super::stdlib::stdlib_virtual_path(module)),
40 None => LocalResolution::Rejected,
41 };
42 }
43
44 let base = current_file.parent().unwrap_or(Path::new("."));
45 let mut file_path = base.join(import_path);
46 if !file_path.exists() && file_path.extension().is_none() {
47 file_path.set_extension("harn");
48 }
49 if file_path.exists() {
50 return LocalResolution::Resolved(file_path);
51 }
52
53 LocalResolution::NotPackage
54}
55
56pub fn resolve_import_path(current_file: &Path, import_path: &str) -> Option<PathBuf> {
61 match resolve_local_import(current_file, import_path) {
62 LocalResolution::Resolved(path) => Some(path),
63 LocalResolution::Rejected => None,
64 LocalResolution::NotPackage => {
71 let snapshots = PackageSnapshot::acquire_nearest(current_file)
72 .ok()
73 .flatten()
74 .into_iter()
75 .collect::<Vec<_>>();
76 resolve_package_import(current_file, import_path, &snapshots)
77 }
78 }
79}
80
81pub(crate) fn resolve_import_path_with_snapshots(
82 current_file: &Path,
83 import_path: &str,
84 package_snapshots: &[PackageSnapshot],
85) -> Option<PathBuf> {
86 match resolve_local_import(current_file, import_path) {
87 LocalResolution::Resolved(path) => Some(path),
88 LocalResolution::Rejected => None,
89 LocalResolution::NotPackage => {
90 resolve_package_import(current_file, import_path, package_snapshots)
91 }
92 }
93}
94
95pub(crate) fn acquire_package_snapshots(files: &[PathBuf]) -> Vec<PackageSnapshot> {
103 let mut walked_roots = HashSet::new();
104 let mut canonical_roots = HashSet::new();
105 let mut snapshots = Vec::new();
106 for file in files {
107 let Some(root) = PackageSnapshot::nearest_project_root(file) else {
109 continue;
110 };
111 if !walked_roots.insert(root.clone()) {
112 continue;
113 }
114 let Ok(Some(snapshot)) = PackageSnapshot::acquire(&root) else {
116 continue;
117 };
118 if canonical_roots.insert(snapshot.project_root().to_path_buf()) {
123 snapshots.push(snapshot);
124 }
125 }
126 snapshots
127}
128
129fn resolve_package_import(
130 current_file: &Path,
131 import_path: &str,
132 package_snapshots: &[PackageSnapshot],
133) -> Option<PathBuf> {
134 let current_file = canonicalize_with_existing_parent(current_file);
135 package_snapshots
136 .iter()
137 .filter(|snapshot| current_file.starts_with(snapshot.project_root()))
138 .max_by_key(|snapshot| snapshot.project_root().components().count())
139 .and_then(|snapshot| resolve_from_packages_root(snapshot.packages_root(), import_path))
140}
141
142fn canonicalize_with_existing_parent(path: &Path) -> PathBuf {
143 path.canonicalize().unwrap_or_else(|_| {
144 path.parent()
145 .and_then(|parent| parent.canonicalize().ok())
146 .and_then(|parent| path.file_name().map(|name| parent.join(name)))
147 .unwrap_or_else(|| path.to_path_buf())
148 })
149}
150
151fn resolve_from_packages_root(packages_root: &Path, import_path: &str) -> Option<PathBuf> {
152 let safe_import_path = safe_package_relative_path(import_path)?;
153 let package_name = package_name_from_relative_path(&safe_import_path)?;
154 let package_root = packages_root.join(package_name);
155
156 let direct_path = packages_root.join(&safe_import_path);
157 if let Some(path) = finalize_package_target(&package_root, &direct_path) {
158 return Some(path);
159 }
160
161 let export_name = export_name_from_relative_path(&safe_import_path)?;
162 let manifest = read_package_manifest(&package_root.join("harn.toml"))?;
163 let safe_export_path = safe_package_relative_path(manifest.exports.get(export_name)?)?;
164 finalize_package_target(&package_root, &package_root.join(safe_export_path))
165}
166
167fn read_package_manifest(path: &Path) -> Option<PackageManifest> {
168 let content = std::fs::read_to_string(path).ok()?;
169 toml::from_str(&content).ok()
170}
171
172fn safe_package_relative_path(raw: &str) -> Option<PathBuf> {
173 if raw.is_empty() || raw.contains('\\') {
174 return None;
175 }
176 let mut out = PathBuf::new();
177 let mut saw_component = false;
178 for component in Path::new(raw).components() {
179 match component {
180 Component::Normal(part) => {
181 saw_component = true;
182 out.push(part);
183 }
184 Component::CurDir => {}
185 Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
186 }
187 }
188 saw_component.then_some(out)
189}
190
191fn package_name_from_relative_path(path: &Path) -> Option<&str> {
192 match path.components().next()? {
193 Component::Normal(name) => name.to_str(),
194 _ => None,
195 }
196}
197
198fn export_name_from_relative_path(path: &Path) -> Option<&str> {
199 let mut components = path.components();
200 components.next()?;
201 let rest = components.as_path();
202 if rest.as_os_str().is_empty() {
203 None
204 } else {
205 rest.to_str()
206 }
207}
208
209fn target_within_package_root(package_root: &Path, path: PathBuf) -> Option<PathBuf> {
210 let root = package_root.canonicalize().ok()?;
211 let canonical = path.canonicalize().ok()?;
212 (canonical == root || canonical.starts_with(&root)).then_some(path)
213}
214
215fn finalize_package_target(package_root: &Path, path: &Path) -> Option<PathBuf> {
216 if path.is_dir() {
217 let lib = path.join("lib.harn");
218 return if lib.exists() {
219 target_within_package_root(package_root, lib)
220 } else {
221 target_within_package_root(package_root, path.to_path_buf())
222 };
223 }
224 if path.exists() {
225 return target_within_package_root(package_root, path.to_path_buf());
226 }
227 if path.extension().is_none() {
228 let mut with_extension = path.to_path_buf();
229 with_extension.set_extension("harn");
230 if with_extension.exists() {
231 return target_within_package_root(package_root, with_extension);
232 }
233 }
234 None
235}