harn_modules/
package_imports.rs1use std::collections::{HashMap, HashSet};
2use std::path::{Component, Path, PathBuf};
3
4use serde::Deserialize;
5
6use crate::package_execution::{PackageExecutionError, PackageExecutionGuard};
7use crate::package_snapshot::PackageSnapshot;
8use crate::ModuleGraph;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct PackageImport {
17 pub importer: PathBuf,
18 pub alias: String,
19}
20
21#[derive(Debug, Default, Deserialize)]
22struct PackageManifest {
23 #[serde(default)]
24 exports: HashMap<String, String>,
25}
26
27pub(super) enum LocalResolution {
34 Resolved(PathBuf),
36 Rejected,
39 NotPackage,
41}
42
43impl ModuleGraph {
44 pub fn package_imports(&self) -> Vec<PackageImport> {
46 let mut imports = self
47 .modules
48 .iter()
49 .flat_map(|(file, module)| {
50 module.imports.iter().filter_map(move |import| {
51 if !matches!(
52 resolve_local_import(file, &import.raw_path),
53 LocalResolution::NotPackage
54 ) {
55 return None;
56 }
57 Some(PackageImport {
58 importer: file.clone(),
59 alias: package_alias_from_import(&import.raw_path)?,
60 })
61 })
62 })
63 .collect::<Vec<_>>();
64 imports.sort_by(|left, right| {
65 left.importer
66 .cmp(&right.importer)
67 .then_with(|| left.alias.cmp(&right.alias))
68 });
69 imports.dedup();
70 imports
71 }
72
73 pub fn package_import_aliases(&self) -> Vec<String> {
78 let mut aliases = self
79 .package_imports()
80 .into_iter()
81 .map(|import| import.alias)
82 .collect::<Vec<_>>();
83 aliases.sort();
84 aliases.dedup();
85 aliases
86 }
87}
88
89pub(super) fn resolve_local_import(current_file: &Path, import_path: &str) -> LocalResolution {
94 if let Some(module) = import_path
95 .strip_prefix("std/")
96 .or_else(|| (import_path == "observability").then_some("observability"))
97 {
98 return match super::stdlib::get_stdlib_source(module) {
99 Some(_) => LocalResolution::Resolved(super::stdlib::stdlib_virtual_path(module)),
100 None => LocalResolution::Rejected,
101 };
102 }
103
104 let base = current_file.parent().unwrap_or(Path::new("."));
105 let mut file_path = base.join(import_path);
106 if !file_path.exists() && file_path.extension().is_none() {
107 file_path.set_extension("harn");
108 }
109 if file_path.exists() {
110 return LocalResolution::Resolved(file_path);
111 }
112
113 if import_path.starts_with("./") || import_path.starts_with("../") {
114 return LocalResolution::Rejected;
115 }
116
117 LocalResolution::NotPackage
118}
119
120pub fn resolve_import_path(current_file: &Path, import_path: &str) -> Option<PathBuf> {
125 match resolve_local_import(current_file, import_path) {
126 LocalResolution::Resolved(path) => Some(path),
127 LocalResolution::Rejected => None,
128 LocalResolution::NotPackage => {
135 let snapshots = PackageSnapshot::acquire_nearest(current_file)
136 .ok()
137 .flatten()
138 .into_iter()
139 .collect::<Vec<_>>();
140 let resolved = resolve_package_import(current_file, import_path, &snapshots);
141 if resolved.is_some() {
142 for snapshot in snapshots {
143 snapshot.retain_for_process();
144 }
145 }
146 resolved
147 }
148 }
149}
150
151pub(crate) fn resolve_import_path_with_snapshots(
152 current_file: &Path,
153 import_path: &str,
154 package_snapshots: &[PackageSnapshot],
155) -> Option<PathBuf> {
156 match resolve_local_import(current_file, import_path) {
157 LocalResolution::Resolved(path) => Some(path),
158 LocalResolution::Rejected => None,
159 LocalResolution::NotPackage => {
160 resolve_package_import(current_file, import_path, package_snapshots)
161 }
162 }
163}
164
165pub fn resolve_import_path_with_snapshot(
166 current_file: &Path,
167 import_path: &str,
168 package_snapshot: &PackageSnapshot,
169) -> Option<PathBuf> {
170 match resolve_local_import(current_file, import_path) {
171 LocalResolution::Resolved(path) => Some(path),
172 LocalResolution::Rejected => None,
173 LocalResolution::NotPackage => {
177 resolve_from_packages_root(package_snapshot.packages_root(), import_path)
178 }
179 }
180}
181
182pub fn resolve_import_path_with_guard(
183 current_file: &Path,
184 import_path: &str,
185 guard: &PackageExecutionGuard,
186) -> Result<Option<PathBuf>, PackageExecutionError> {
187 guard.validate_import_path(current_file, import_path)?;
188 match resolve_local_import(current_file, import_path) {
189 LocalResolution::Resolved(path) => Ok(Some(path)),
190 LocalResolution::Rejected => Ok(None),
191 LocalResolution::NotPackage => resolve_from_packages_root_with_guard(
192 guard.snapshot().packages_root(),
193 import_path,
194 guard,
195 ),
196 }
197}
198
199pub(crate) fn acquire_package_snapshots(files: &[PathBuf]) -> Vec<PackageSnapshot> {
207 let mut walked_roots = HashSet::new();
208 let mut canonical_roots = HashSet::new();
209 let mut snapshots = Vec::new();
210 for file in files {
211 let Some(root) = PackageSnapshot::nearest_project_root(file) else {
213 continue;
214 };
215 if !walked_roots.insert(root.clone()) {
216 continue;
217 }
218 let Ok(Some(snapshot)) = PackageSnapshot::acquire(&root) else {
220 continue;
221 };
222 if canonical_roots.insert(snapshot.project_root().to_path_buf()) {
227 snapshots.push(snapshot);
228 }
229 }
230 snapshots
231}
232
233fn resolve_package_import(
234 current_file: &Path,
235 import_path: &str,
236 package_snapshots: &[PackageSnapshot],
237) -> Option<PathBuf> {
238 let current_file = canonicalize_with_existing_parent(current_file);
239 package_snapshots
240 .iter()
241 .filter(|snapshot| current_file.starts_with(snapshot.project_root()))
242 .max_by_key(|snapshot| snapshot.project_root().components().count())
243 .and_then(|snapshot| resolve_from_packages_root(snapshot.packages_root(), import_path))
244}
245
246fn canonicalize_with_existing_parent(path: &Path) -> PathBuf {
247 path.canonicalize().unwrap_or_else(|_| {
248 path.parent()
249 .and_then(|parent| parent.canonicalize().ok())
250 .and_then(|parent| path.file_name().map(|name| parent.join(name)))
251 .unwrap_or_else(|| path.to_path_buf())
252 })
253}
254
255fn resolve_from_packages_root(packages_root: &Path, import_path: &str) -> Option<PathBuf> {
256 let safe_import_path = safe_package_relative_path(import_path)?;
257 let package_name = package_name_from_relative_path(&safe_import_path)?;
258 let package_root = packages_root.join(package_name);
259
260 let direct_path = packages_root.join(&safe_import_path);
261 if let Some(path) = finalize_package_target(&package_root, &direct_path) {
262 return Some(path);
263 }
264
265 let export_name = export_name_from_relative_path(&safe_import_path)?;
266 let manifest = read_package_manifest(&package_root.join("harn.toml"))?;
267 let safe_export_path = safe_package_relative_path(manifest.exports.get(export_name)?)?;
268 finalize_package_target(&package_root, &package_root.join(safe_export_path))
269}
270
271fn resolve_from_packages_root_with_guard(
272 packages_root: &Path,
273 import_path: &str,
274 guard: &PackageExecutionGuard,
275) -> Result<Option<PathBuf>, PackageExecutionError> {
276 let Some(safe_import_path) = safe_package_relative_path(import_path) else {
277 return Ok(None);
278 };
279 let Some(package_name) = package_name_from_relative_path(&safe_import_path) else {
280 return Ok(None);
281 };
282 let package_root = packages_root.join(package_name);
283 let direct_path = packages_root.join(&safe_import_path);
284 if let Some(path) = finalize_package_target(&package_root, &direct_path) {
285 return Ok(Some(path));
286 }
287
288 let Some(export_name) = export_name_from_relative_path(&safe_import_path) else {
289 return Ok(None);
290 };
291 let manifest_path = package_root.join("harn.toml");
292 let bytes = guard.verify_entry_source(&manifest_path)?;
293 let source = std::str::from_utf8(&bytes).map_err(|error| {
294 PackageExecutionError::Invalid(format!(
295 "package manifest {} is not valid UTF-8: {error}",
296 manifest_path.display()
297 ))
298 })?;
299 let manifest = toml::from_str::<PackageManifest>(source).map_err(|error| {
300 PackageExecutionError::Invalid(format!(
301 "failed to parse package exports from {}: {error}",
302 manifest_path.display()
303 ))
304 })?;
305 let Some(export_path) = manifest.exports.get(export_name) else {
306 return Ok(None);
307 };
308 let Some(safe_export_path) = safe_package_relative_path(export_path) else {
309 return Ok(None);
310 };
311 Ok(finalize_package_target(
312 &package_root,
313 &package_root.join(safe_export_path),
314 ))
315}
316
317fn read_package_manifest(path: &Path) -> Option<PackageManifest> {
318 let content = std::fs::read_to_string(path).ok()?;
319 toml::from_str(&content).ok()
320}
321
322fn safe_package_relative_path(raw: &str) -> Option<PathBuf> {
323 if raw.is_empty() || raw.contains('\\') {
324 return None;
325 }
326 let mut out = PathBuf::new();
327 let mut saw_component = false;
328 for component in Path::new(raw).components() {
329 match component {
330 Component::Normal(part) => {
331 saw_component = true;
332 out.push(part);
333 }
334 Component::CurDir => {}
335 Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
336 }
337 }
338 saw_component.then_some(out)
339}
340
341pub(super) fn package_alias_from_import(raw: &str) -> Option<String> {
342 let path = safe_package_relative_path(raw)?;
343 package_name_from_relative_path(&path).map(ToString::to_string)
344}
345
346fn package_name_from_relative_path(path: &Path) -> Option<&str> {
347 match path.components().next()? {
348 Component::Normal(name) => name.to_str(),
349 _ => None,
350 }
351}
352
353fn export_name_from_relative_path(path: &Path) -> Option<&str> {
354 let mut components = path.components();
355 components.next()?;
356 let rest = components.as_path();
357 if rest.as_os_str().is_empty() {
358 None
359 } else {
360 rest.to_str()
361 }
362}
363
364fn target_within_package_root(package_root: &Path, path: PathBuf) -> Option<PathBuf> {
365 let root = package_root.canonicalize().ok()?;
366 let canonical = path.canonicalize().ok()?;
367 (canonical == root || canonical.starts_with(&root)).then_some(path)
368}
369
370fn finalize_package_target(package_root: &Path, path: &Path) -> Option<PathBuf> {
371 if path.is_dir() {
372 let lib = path.join("lib.harn");
373 return if lib.exists() {
374 target_within_package_root(package_root, lib)
375 } else {
376 target_within_package_root(package_root, path.to_path_buf())
377 };
378 }
379 if path.exists() {
380 return target_within_package_root(package_root, path.to_path_buf());
381 }
382 if path.extension().is_none() {
383 let mut with_extension = path.to_path_buf();
384 with_extension.set_extension("harn");
385 if with_extension.exists() {
386 return target_within_package_root(package_root, with_extension);
387 }
388 }
389 None
390}