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