Skip to main content

harn_modules/
lib.rs

1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4use crate::package_imports::acquire_package_snapshots;
5use crate::package_snapshot::PackageSnapshot;
6use harn_lexer::Span;
7use harn_parser::{Parser, SNode};
8
9pub mod asset_paths;
10mod declarations;
11pub mod fingerprint;
12mod import_recording;
13pub mod manifest_walk;
14mod namespace_imports;
15pub mod package_execution;
16mod package_imports;
17pub mod package_snapshot;
18pub mod personas;
19pub mod project_config;
20mod stdlib;
21mod symbol_reachability;
22mod type_dependencies;
23
24use declarations::{
25    callable_decl_name, collect_callable_declarations, collect_module_info,
26    collect_type_declarations, decl_site, type_decl_name,
27};
28pub use declarations::{public_declarations, DefKind, PublicDeclaration};
29pub use namespace_imports::NamespaceImportInfo;
30pub use package_imports::{
31    resolve_import_path, resolve_import_path_with_guard, resolve_import_path_with_snapshot,
32};
33pub use symbol_reachability::{
34    closed_program_reachability, ExportDemand, ModuleSymbolDemand, SymbolReachability,
35};
36
37/// A resolved definition site within a module.
38#[derive(Debug, Clone)]
39pub struct DefSite {
40    pub name: String,
41    pub file: PathBuf,
42    pub kind: DefKind,
43    pub span: Span,
44}
45
46/// Wildcard import resolution status for a single importing module.
47#[derive(Debug, Clone)]
48pub enum WildcardResolution {
49    /// Resolved all wildcard imports and can expose wildcard exports.
50    Resolved(HashSet<String>),
51    /// At least one wildcard import could not be resolved.
52    Unknown,
53}
54
55/// Parsed information for a set of module files.
56#[derive(Debug, Default)]
57pub struct ModuleGraph {
58    modules: HashMap<PathBuf, ModuleInfo>,
59    // Resolved definition/import paths remain valid for the graph lifetime.
60    _package_snapshots: Vec<PackageSnapshot>,
61}
62
63#[derive(Debug, Clone)]
64pub struct ParsedModuleSource {
65    pub source: String,
66    pub program: Vec<SNode>,
67}
68
69#[derive(Debug, Default)]
70pub struct ModuleGraphBuild {
71    pub graph: ModuleGraph,
72    pub parsed_sources: HashMap<PathBuf, ParsedModuleSource>,
73}
74
75#[derive(Debug, Default)]
76struct ModuleInfo {
77    /// All declarations visible in this module (for local symbol lookup and
78    /// go-to-definition resolution).
79    declarations: HashMap<String, DefSite>,
80    /// Names exported by this module after re-export resolution. Equal to
81    /// [`own_exports`] union the keys of [`selective_re_exports`] union the
82    /// transitive exports of [`wildcard_re_export_paths`]. Populated in
83    /// `build()` after all modules are loaded.
84    exports: HashSet<String>,
85    /// Names declared locally and exported by this module — i.e. `pub fn`,
86    /// `pub struct`, etc.
87    own_exports: HashSet<String>,
88    /// Selective re-exports introduced by `pub import { name } from "..."`.
89    /// Maps the re-exported name to every canonical source module path it
90    /// could originate from. Multiple entries per name indicate a conflict
91    /// (`pub import { foo } from "a"` and `pub import { foo } from "b"`)
92    /// and are surfaced by [`ModuleGraph::re_export_conflicts`]. Lookup
93    /// callers (e.g. go-to-definition) follow the first recorded source.
94    selective_re_exports: HashMap<String, Vec<PathBuf>>,
95    /// Wildcard re-exports introduced by `pub import "..."`. Each entry is
96    /// the canonical path of a module whose entire public export surface
97    /// this module re-exports.
98    wildcard_re_export_paths: Vec<PathBuf>,
99    /// Namespace re-exports introduced by `pub import * as alias from "..."`.
100    /// Maps the published alias to the canonical target module path. Unlike
101    /// wildcard re-exports, members are **not** flattened into this module's
102    /// public surface — only the alias object itself is exported
103    /// (`DefKind::Variable`). Folded into [`exports`] / [`declarations`] at
104    /// collection time so graph finalization needs no second pass.
105    namespace_re_exports: HashMap<String, PathBuf>,
106    /// Names introduced by selective imports across this module.
107    selective_import_names: HashSet<String>,
108    /// Import references encountered in this file.
109    imports: Vec<ImportRef>,
110    /// True when at least one wildcard import could not be resolved.
111    has_unresolved_wildcard_import: bool,
112    /// True when at least one selective import could not be resolved
113    /// (importing file path missing). Prevents `imported_names_for_file`
114    /// from returning a partial answer when any import is broken.
115    has_unresolved_selective_import: bool,
116    /// True when at least one namespace import could not be resolved.
117    has_unresolved_namespace_import: bool,
118    /// Top-level type-like declarations that can be imported into a caller's
119    /// static type environment.
120    type_declarations: Vec<SNode>,
121    /// Top-level callable declarations whose signatures can be imported into
122    /// a caller's static type environment.
123    callable_declarations: Vec<SNode>,
124    /// Set when this module's own source failed to lex or parse. The module is
125    /// still recorded in the graph (with an otherwise-empty surface) so that
126    /// importers can be told their target is broken — instead of silently
127    /// seeing zero exports and mislabeling the imported symbol as "undefined"
128    /// at the call site.
129    load_error: Option<ModuleLoadError>,
130}
131
132/// A lex/parse failure captured while loading a module into the graph.
133///
134/// Retained so that `harn check <consumer>` can surface the real error in an
135/// imported file rather than downgrading its exports to "undefined" at the
136/// consumer's call site.
137#[derive(Debug, Clone)]
138pub struct ModuleLoadError {
139    /// Rendered lex/parse error message (includes the failing line:column).
140    pub message: String,
141    /// Span of the failure within the imported module's own source.
142    pub span: Span,
143}
144
145/// A consumer import whose resolved target module failed to compile. Reported
146/// by [`ModuleGraph::import_compile_failures`].
147#[derive(Debug, Clone)]
148pub struct ImportCompileFailure {
149    /// The import path exactly as written in the consumer.
150    pub import_raw_path: String,
151    /// Span of the consumer's `import` statement.
152    pub import_span: Span,
153    /// Canonical path of the broken imported module.
154    pub module_path: PathBuf,
155    /// The imported module's real lex/parse error.
156    pub error: ModuleLoadError,
157}
158
159#[derive(Debug, Clone)]
160struct ImportRef {
161    raw_path: String,
162    path: Option<PathBuf>,
163    selective_names: Option<HashSet<String>>,
164    /// When set, this is `import * as alias from "..."` — only the alias is
165    /// bound in the importer (members stay under the namespace object).
166    namespace_alias: Option<String>,
167    is_pub: bool,
168    import_span: Span,
169}
170
171/// Public import edge summary for static module graph consumers.
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct ModuleImport {
174    /// The import string as written in source.
175    pub raw_path: String,
176    /// Resolved module path when the import could be resolved.
177    pub resolved_path: Option<PathBuf>,
178    /// `None` for wildcard / namespace imports; otherwise the selected names.
179    pub selective_names: Option<Vec<String>>,
180    /// When set, this is a namespace import (`import * as alias from "..."`).
181    pub namespace_alias: Option<String>,
182    /// Whether the import republishes its selected projection.
183    pub is_pub: bool,
184}
185
186/// Return the source for a resolved module path.
187///
188/// Real paths are read from disk. `<std>/<module>` virtual paths are backed by
189/// the embedded stdlib source table, so callers can parse resolved stdlib
190/// modules without knowing about the stdlib mirror layout.
191pub fn read_module_source(path: &Path) -> Option<String> {
192    if let Some(stdlib_module) = stdlib_module_from_path(path) {
193        return stdlib::get_stdlib_source(stdlib_module).map(ToString::to_string);
194    }
195    std::fs::read_to_string(path).ok()
196}
197
198/// Build a module graph from a set of files.
199///
200/// Files referenced via `import` statements are loaded recursively so the
201/// graph contains every module reachable from the seed set. Cycles and
202/// already-loaded files are skipped via a visited set.
203pub fn build(files: &[PathBuf]) -> ModuleGraph {
204    build_inner(files, None, false, None).graph
205}
206
207/// Build a module graph using caller-owned source for one root file.
208///
209/// Imported modules still resolve from their normal filesystem or embedded
210/// stdlib locations. This keeps editor diagnostics aligned with unsaved root
211/// buffers without creating a second module resolver.
212pub fn build_with_source(file: &Path, source: &str) -> ModuleGraph {
213    let file = normalize_path(file);
214    let source_overrides = HashMap::from([(file.clone(), source.to_string())]);
215    build_inner(&[file], None, false, Some(&source_overrides)).graph
216}
217
218/// Build a module graph while retaining parsed sources for the seed files.
219///
220/// Imported-only modules still participate in the graph, but their ASTs are
221/// dropped after graph extraction so callers do not pay extra peak memory for
222/// parsed sources they will not reuse.
223pub fn build_with_parsed_sources(files: &[PathBuf]) -> ModuleGraphBuild {
224    let parsed_source_targets = files.iter().map(|file| normalize_path(file)).collect();
225    build_inner(files, Some(&parsed_source_targets), false, None)
226}
227
228/// Build a closed module graph while retaining every reachable parsed source.
229///
230/// This is intentionally separate from editor-oriented graph construction:
231/// package linking consumes every module AST, while ordinary diagnostics should
232/// not retain an entire dependency graph after extracting its public surface.
233pub fn build_closed_program(files: &[PathBuf]) -> ModuleGraphBuild {
234    build_inner(files, None, true, None)
235}
236
237fn build_inner(
238    files: &[PathBuf],
239    parsed_source_targets: Option<&HashSet<PathBuf>>,
240    retain_all_parsed_sources: bool,
241    source_overrides: Option<&HashMap<PathBuf, String>>,
242) -> ModuleGraphBuild {
243    let package_snapshots = acquire_package_snapshots(files);
244    let mut modules: HashMap<PathBuf, ModuleInfo> = HashMap::new();
245    let mut parsed_sources: HashMap<PathBuf, ParsedModuleSource> = HashMap::new();
246    let mut seen: HashSet<PathBuf> = HashSet::new();
247    let mut wave: Vec<PathBuf> = Vec::new();
248    for file in files {
249        let canonical = normalize_path(file);
250        if seen.insert(canonical.clone()) {
251            wave.push(canonical);
252        }
253    }
254    // Breadth-first over import waves. Every path in a wave is new (the
255    // `seen` set dedupes before enqueue), and `load_module` is a pure
256    // read+lex+parse+extract, so each wave loads in parallel; discovery of
257    // the next wave stays sequential to keep the dedup deterministic. A
258    // whole-tree seed set arrives as one large first wave, which is where
259    // nearly all the parse work is, so the serial-BFS tail on deep import
260    // chains does not matter in practice.
261    while !wave.is_empty() {
262        let loaded = load_wave(&wave, &package_snapshots, source_overrides);
263        let mut next_wave: Vec<PathBuf> = Vec::new();
264        for (path, (module, parsed)) in wave.drain(..).zip(loaded) {
265            let retain_parsed_source = retain_all_parsed_sources
266                || parsed_source_targets.is_some_and(|targets| targets.contains(&path));
267            if retain_parsed_source {
268                if let Some(parsed) = parsed {
269                    parsed_sources.insert(path.clone(), parsed);
270                }
271            }
272            // Enqueue resolved import targets so the whole reachable graph is
273            // discovered without the caller having to pre-walk imports.
274            //
275            // `resolve_import_path` returns paths as `base.join(import)` —
276            // i.e. with `..` segments preserved rather than collapsed. If we
277            // dedupe on those raw forms, two files that import each other
278            // across sibling dirs (`lib/context/` ↔ `lib/runtime/`) produce a
279            // different path spelling on every cycle — `.../context/../runtime/`,
280            // then `.../context/../runtime/../context/`, and so on — each of
281            // which is treated as a new file. The walk only terminates when
282            // `path.exists()` starts failing at the filesystem's `PATH_MAX`,
283            // which is 1024 on macOS but 4096 on Linux. Linux therefore
284            // re-parses the same handful of files thousands of times, balloons
285            // RSS into the multi-GB range, and gets SIGKILL'd by CI runners.
286            // Canonicalize once here so `seen` dedupes by the underlying file,
287            // not by its path spelling.
288            for import in &module.imports {
289                if let Some(import_path) = &import.path {
290                    let canonical = normalize_path(import_path);
291                    if seen.insert(canonical.clone()) {
292                        next_wave.push(canonical);
293                    }
294                }
295            }
296            modules.insert(path, module);
297        }
298        wave = next_wave;
299    }
300    resolve_re_exports(&mut modules);
301    ModuleGraphBuild {
302        graph: ModuleGraph {
303            modules,
304            _package_snapshots: package_snapshots,
305        },
306        parsed_sources,
307    }
308}
309
310/// Environment override for the graph-build worker-pool size. `1` forces the
311/// serial walk; unset defaults to the machine's available parallelism.
312pub const MODULE_GRAPH_JOBS_ENV: &str = "HARN_MODULE_GRAPH_JOBS";
313
314/// Load one BFS wave of module paths, in parallel when the wave is large
315/// enough to pay for the threads. Results are index-aligned with `paths`.
316fn load_wave(
317    paths: &[PathBuf],
318    package_snapshots: &[PackageSnapshot],
319    source_overrides: Option<&HashMap<PathBuf, String>>,
320) -> Vec<(ModuleInfo, Option<ParsedModuleSource>)> {
321    const MIN_PARALLEL_WAVE: usize = 8;
322    let configured = std::env::var(MODULE_GRAPH_JOBS_ENV)
323        .ok()
324        .and_then(|value| value.parse::<usize>().ok())
325        .filter(|&jobs| jobs > 0);
326    let workers = configured
327        .unwrap_or_else(|| {
328            std::thread::available_parallelism()
329                .map(std::num::NonZeroUsize::get)
330                .unwrap_or(1)
331        })
332        .min(paths.len());
333    if workers <= 1 || paths.len() < MIN_PARALLEL_WAVE {
334        return paths
335            .iter()
336            .map(|path| load_module(path, package_snapshots, source_overrides))
337            .collect();
338    }
339    let next = std::sync::atomic::AtomicUsize::new(0);
340    let mut produced: Vec<(usize, (ModuleInfo, Option<ParsedModuleSource>))> =
341        std::thread::scope(|scope| {
342            let handles: Vec<_> = (0..workers)
343                .map(|_| {
344                    scope.spawn(|| {
345                        let mut local = Vec::new();
346                        loop {
347                            let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
348                            let Some(path) = paths.get(index) else {
349                                break;
350                            };
351                            local.push((
352                                index,
353                                load_module(path, package_snapshots, source_overrides),
354                            ));
355                        }
356                        local
357                    })
358                })
359                .collect();
360            handles
361                .into_iter()
362                .flat_map(|handle| match handle.join() {
363                    Ok(local) => local,
364                    Err(panic) => std::panic::resume_unwind(panic),
365                })
366                .collect()
367        });
368    produced.sort_unstable_by_key(|(index, _)| *index);
369    produced.into_iter().map(|(_, loaded)| loaded).collect()
370}
371
372/// Iteratively expand each module's `exports` set to include the transitive
373/// public surface of its `pub import "..."` re-export targets. Cycles are
374/// safe because the loop only adds names — once no module's set grows in a
375/// pass, the fixpoint is reached.
376fn resolve_re_exports(modules: &mut HashMap<PathBuf, ModuleInfo>) {
377    let keys: Vec<PathBuf> = modules.keys().cloned().collect();
378    loop {
379        let mut changed = false;
380        for path in &keys {
381            // Snapshot the wildcard target list and gather the union of
382            // their current exports without holding a mutable borrow.
383            let wildcard_paths = modules
384                .get(path)
385                .map(|m| m.wildcard_re_export_paths.clone())
386                .unwrap_or_default();
387            if wildcard_paths.is_empty() {
388                continue;
389            }
390            let mut additions: Vec<String> = Vec::new();
391            for src in &wildcard_paths {
392                let src_canonical = normalize_path(src);
393                if let Some(src_module) = modules.get(src).or_else(|| modules.get(&src_canonical)) {
394                    additions.extend(src_module.exports.iter().cloned());
395                }
396            }
397            if let Some(module) = modules.get_mut(path) {
398                for name in additions {
399                    if module.exports.insert(name) {
400                        changed = true;
401                    }
402                }
403            }
404        }
405        if !changed {
406            break;
407        }
408    }
409}
410
411impl ModuleGraph {
412    /// Sorted list of every module path discovered by [`build`]. Includes
413    /// `<std>/<name>` virtual paths for stdlib modules reached transitively.
414    /// Callers that want only real-disk modules can filter for paths whose
415    /// string form does not start with `<std>/`.
416    pub fn module_paths(&self) -> Vec<PathBuf> {
417        let mut paths: Vec<PathBuf> = self.modules.keys().cloned().collect();
418        paths.sort();
419        paths
420    }
421
422    /// True when `path` (or its canonical form) was discovered during the
423    /// module-graph walk.
424    pub fn contains_module(&self, path: &Path) -> bool {
425        self.modules.contains_key(path) || self.modules.contains_key(&normalize_path(path))
426    }
427
428    /// Collect every name used in selective imports from all files.
429    pub fn all_selective_import_names(&self) -> HashSet<&str> {
430        let mut names = HashSet::new();
431        for module in self.modules.values() {
432            for name in &module.selective_import_names {
433                names.insert(name.as_str());
434            }
435        }
436        names
437    }
438
439    /// Files that directly import `target`. Resolves `target` to a
440    /// canonical path before lookup so callers can pass either spelling.
441    pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
442        let target = normalize_path(target);
443        let mut out: Vec<PathBuf> = self
444            .modules
445            .iter()
446            .filter(|(_, info)| {
447                info.imports.iter().any(|import| {
448                    import
449                        .path
450                        .as_ref()
451                        .is_some_and(|p| normalize_path(p) == target)
452                })
453            })
454            .map(|(path, _)| path.clone())
455            .collect();
456        out.sort();
457        out
458    }
459
460    /// Import edges declared by `file`, sorted by raw path and selected names.
461    pub fn imports_for_module(&self, file: &Path) -> Vec<ModuleImport> {
462        let file = normalize_path(file);
463        let Some(module) = self.modules.get(&file) else {
464            return Vec::new();
465        };
466        let mut imports: Vec<ModuleImport> = module
467            .imports
468            .iter()
469            .map(|import| {
470                let mut selective_names = import
471                    .selective_names
472                    .as_ref()
473                    .map(|names| names.iter().cloned().collect::<Vec<_>>());
474                if let Some(names) = selective_names.as_mut() {
475                    names.sort();
476                }
477                ModuleImport {
478                    raw_path: import.raw_path.clone(),
479                    resolved_path: import.path.as_ref().map(|path| normalize_path(path)),
480                    selective_names,
481                    namespace_alias: import.namespace_alias.clone(),
482                    is_pub: import.is_pub,
483                }
484            })
485            .collect();
486        imports.sort_by(|left, right| {
487            left.raw_path
488                .cmp(&right.raw_path)
489                .then_with(|| left.selective_names.cmp(&right.selective_names))
490                .then_with(|| left.namespace_alias.cmp(&right.namespace_alias))
491                .then_with(|| left.resolved_path.cmp(&right.resolved_path))
492        });
493        imports
494    }
495
496    /// Exported symbol names for `file`, sorted alphabetically.
497    pub fn exports_for_module(&self, file: &Path) -> Vec<String> {
498        let file = normalize_path(file);
499        let Some(module) = self.modules.get(&file) else {
500            return Vec::new();
501        };
502        let mut exports: Vec<String> = module.exports.iter().cloned().collect();
503        exports.sort();
504        exports
505    }
506
507    /// Resolve wildcard imports for `file`.
508    ///
509    /// Returns `Unknown` when any wildcard import cannot be resolved, because
510    /// callers should conservatively disable wildcard-import-sensitive checks.
511    pub fn wildcard_exports_for(&self, file: &Path) -> WildcardResolution {
512        let file = normalize_path(file);
513        let Some(module) = self.modules.get(&file) else {
514            return WildcardResolution::Unknown;
515        };
516        if module.has_unresolved_wildcard_import {
517            return WildcardResolution::Unknown;
518        }
519
520        let mut names = HashSet::new();
521        for import in module
522            .imports
523            .iter()
524            .filter(|import| import.selective_names.is_none())
525        {
526            let Some(import_path) = &import.path else {
527                return WildcardResolution::Unknown;
528            };
529            let imported = self.modules.get(import_path).or_else(|| {
530                let normalized = normalize_path(import_path);
531                self.modules.get(&normalized)
532            });
533            let Some(imported) = imported else {
534                return WildcardResolution::Unknown;
535            };
536            names.extend(imported.exports.iter().cloned());
537        }
538        WildcardResolution::Resolved(names)
539    }
540
541    /// Collect every statically callable/referenceable name introduced into
542    /// `file` by its imports.
543    ///
544    /// Returns `Some` only when **every** import (wildcard or selective) in
545    /// `file` is fully resolvable via the graph. Returns `None` when any
546    /// import is unresolved, so callers can fall back to conservative
547    /// behavior instead of emitting spurious "undefined name" errors.
548    ///
549    /// The returned set contains:
550    /// - all public exports from wildcard-imported modules (transitively
551    ///   following `pub import` re-export chains), and
552    /// - selectively imported names that the target module actually exports
553    ///   (its `pub` surface or re-exports) — matching what the VM accepts at
554    ///   runtime. A name that exists only privately in the target is NOT
555    ///   importable.
556    ///
557    /// Every import in `file` whose resolved target module failed to lex or
558    /// parse. Lets `harn check <file>` surface the real error inside the
559    /// imported module (anchored at the consumer's `import` statement) instead
560    /// of downgrading the imported symbols to "undefined" at their call sites.
561    #[must_use]
562    pub fn import_compile_failures(&self, file: &Path) -> Vec<ImportCompileFailure> {
563        let file = normalize_path(file);
564        let Some(module) = self.modules.get(&file) else {
565            return Vec::new();
566        };
567        let mut failures = Vec::new();
568        for import in &module.imports {
569            let Some(import_path) = &import.path else {
570                continue;
571            };
572            let Some(target) = self
573                .modules
574                .get(import_path)
575                .or_else(|| self.modules.get(&normalize_path(import_path)))
576            else {
577                continue;
578            };
579            if let Some(error) = &target.load_error {
580                failures.push(ImportCompileFailure {
581                    import_raw_path: import.raw_path.clone(),
582                    import_span: import.import_span,
583                    module_path: normalize_path(import_path),
584                    error: error.clone(),
585                });
586            }
587        }
588        failures
589    }
590
591    pub fn imported_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
592        let file = normalize_path(file);
593        let module = self.modules.get(&file)?;
594        if module.has_unresolved_wildcard_import
595            || module.has_unresolved_selective_import
596            || module.has_unresolved_namespace_import
597        {
598            return None;
599        }
600
601        let mut names = HashSet::new();
602        for import in &module.imports {
603            // Namespace imports bind only the alias — never flatten members.
604            if let Some(alias) = &import.namespace_alias {
605                names.insert(alias.clone());
606                continue;
607            }
608            let import_path = import.path.as_ref()?;
609            let imported = self
610                .modules
611                .get(import_path)
612                .or_else(|| self.modules.get(&normalize_path(import_path)))?;
613            // The target parsed nothing (lex/parse failure). Fall back to the
614            // conservative `None` answer so the cross-module undefined-name
615            // check stays silent — the real error is surfaced separately by
616            // `import_compile_failures`, not mislabeled as an undefined symbol
617            // at this consumer's call site.
618            if imported.load_error.is_some() {
619                return None;
620            }
621            match &import.selective_names {
622                None => {
623                    names.extend(imported.exports.iter().cloned());
624                }
625                Some(selective) => {
626                    // A selectively imported name is in scope when it exists in
627                    // the target module (as a declaration or a re-export). The
628                    // stricter "must be `pub`" check is reported precisely at
629                    // the import site by the `HARN-IMP-002` preflight scan
630                    // (`scan_selective_import_visibility`) and enforced at load
631                    // time, so it is intentionally *not* duplicated here —
632                    // otherwise a private import would surface both an
633                    // import-site and a redundant call-site error.
634                    for name in selective {
635                        if imported.declarations.contains_key(name)
636                            || imported.exports.contains(name)
637                        {
638                            names.insert(name.clone());
639                        }
640                    }
641                }
642            }
643        }
644        Some(names)
645    }
646
647    /// Collect imported names of one declaration kind from a module's public
648    /// imports. This preserves the target module's export boundary across
649    /// selective, wildcard, and re-exported imports while giving compilers
650    /// the narrow semantic metadata needed for syntax-sensitive lowering.
651    pub fn imported_names_by_kind_for_file(
652        &self,
653        file: &Path,
654        kind: DefKind,
655    ) -> Option<HashSet<String>> {
656        let file = normalize_path(file);
657        let module = self.modules.get(&file)?;
658        if module.has_unresolved_wildcard_import
659            || module.has_unresolved_selective_import
660            || module.has_unresolved_namespace_import
661        {
662            return None;
663        }
664
665        let mut names = HashSet::new();
666        for import in &module.imports {
667            // Namespace aliases are variables, not flattened member kinds.
668            if import.namespace_alias.is_some() {
669                continue;
670            }
671            let import_path = import.path.as_ref()?;
672            let imported_names: Vec<String> = match &import.selective_names {
673                Some(selective) => selective.iter().cloned().collect(),
674                None => self
675                    .modules
676                    .get(import_path)
677                    .or_else(|| self.modules.get(&normalize_path(import_path)))?
678                    .exports
679                    .iter()
680                    .cloned()
681                    .collect(),
682            };
683            for name in imported_names {
684                if self.exported_kind(import_path, &name) == Some(kind) {
685                    names.insert(name);
686                }
687            }
688        }
689        Some(names)
690    }
691
692    /// Collect type / struct / enum / interface declarations made visible to
693    /// `file` by its imports. Returns `None` when any import is unresolved so
694    /// callers can fall back to conservative behavior.
695    pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
696        let file = normalize_path(file);
697        let module = self.modules.get(&file)?;
698        if module.has_unresolved_wildcard_import
699            || module.has_unresolved_selective_import
700            || module.has_unresolved_namespace_import
701        {
702            return None;
703        }
704
705        let mut decls = Vec::new();
706        let mut seen = HashSet::new();
707        for import in &module.imports {
708            // Namespace imports do not flatten type names into the caller.
709            if import.namespace_alias.is_some() {
710                continue;
711            }
712            let import_path = import.path.as_ref()?;
713            let imported = self
714                .modules
715                .get(import_path)
716                .or_else(|| self.modules.get(&normalize_path(import_path)))?;
717            // The target parsed nothing (lex/parse failure). Fall back to the
718            // conservative `None` answer so the cross-module undefined-name
719            // check stays silent — the real error is surfaced separately by
720            // `import_compile_failures`, not mislabeled as an undefined symbol
721            // at this consumer's call site.
722            if imported.load_error.is_some() {
723                return None;
724            }
725            let mut names_to_collect: Vec<String> = match &import.selective_names {
726                None => imported.exports.iter().cloned().collect(),
727                Some(selective) => selective.iter().cloned().collect(),
728            };
729            names_to_collect.sort();
730            for name in &names_to_collect {
731                let mut visited = HashSet::new();
732                if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
733                    let origin = self
734                        .export_definition_of(import_path, name)
735                        .map_or_else(|| import_path.clone(), |definition| definition.file);
736                    self.extend_type_dependency(&origin, &decl, &mut decls, &mut seen);
737                }
738            }
739            // Every type alias / struct / enum / interface declared in the
740            // imported module is visible to the *typechecker*, `pub` or not:
741            // an imported fn's signature may reference a module-private alias
742            // ("options: PickKeysOptions"), and without its definition the
743            // caller sees only a phantom `Named(...)` and skips contract
744            // checks. This visibility is typing-only — name-level import
745            // privacy is still enforced by `selective_import_issues`
746            // and the runtime loader, which reject importing a non-`pub`
747            // type by name.
748            for ty_decl in &imported.type_declarations {
749                if type_decl_name(ty_decl).is_some() {
750                    self.extend_type_dependency(import_path, ty_decl, &mut decls, &mut seen);
751                }
752            }
753
754            for name in &names_to_collect {
755                let mut visited = HashSet::new();
756                let Some(callable) =
757                    self.find_exported_callable_decl(import_path, name, &mut visited)
758                else {
759                    continue;
760                };
761                let origin = self
762                    .export_definition_of(import_path, name)
763                    .map_or_else(|| import_path.clone(), |definition| definition.file);
764                self.extend_callable_type_dependencies(&origin, &callable, &mut decls, &mut seen);
765            }
766        }
767        Some(decls)
768    }
769
770    /// Collect callable declarations made visible to `file` by its imports.
771    /// Only signatures are consumed by the type checker; imported bodies
772    /// remain owned by their defining modules.
773    pub fn imported_callable_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
774        let file = normalize_path(file);
775        let module = self.modules.get(&file)?;
776        if module.has_unresolved_wildcard_import
777            || module.has_unresolved_selective_import
778            || module.has_unresolved_namespace_import
779        {
780            return None;
781        }
782
783        let mut decls = Vec::new();
784        for import in &module.imports {
785            // Namespace imports keep callables under the alias object.
786            if import.namespace_alias.is_some() {
787                continue;
788            }
789            let import_path = import.path.as_ref()?;
790            let imported = self
791                .modules
792                .get(import_path)
793                .or_else(|| self.modules.get(&normalize_path(import_path)))?;
794            // The target parsed nothing (lex/parse failure). Fall back to the
795            // conservative `None` answer so the cross-module undefined-name
796            // check stays silent — the real error is surfaced separately by
797            // `import_compile_failures`, not mislabeled as an undefined symbol
798            // at this consumer's call site.
799            if imported.load_error.is_some() {
800                return None;
801            }
802            let selective_import = import.selective_names.is_some();
803            let names_to_collect: Vec<String> = match &import.selective_names {
804                None => imported.exports.iter().cloned().collect(),
805                Some(selective) => selective.iter().cloned().collect(),
806            };
807            for name in &names_to_collect {
808                if selective_import || imported.own_exports.contains(name) {
809                    if let Some(decl) = imported
810                        .callable_declarations
811                        .iter()
812                        .find(|decl| callable_decl_name(decl) == Some(name.as_str()))
813                    {
814                        decls.push(decl.clone());
815                        continue;
816                    }
817                }
818                let mut visited = HashSet::new();
819                if let Some(decl) =
820                    self.find_exported_callable_decl(import_path, name, &mut visited)
821                {
822                    decls.push(decl);
823                }
824            }
825        }
826        Some(decls)
827    }
828
829    /// Walk a module's local type declarations and re-export chains to find
830    /// the SNode for an exported type/struct/enum/interface named `name`.
831    fn find_exported_type_decl(
832        &self,
833        path: &Path,
834        name: &str,
835        visited: &mut HashSet<PathBuf>,
836    ) -> Option<SNode> {
837        let canonical = normalize_path(path);
838        if !visited.insert(canonical.clone()) {
839            return None;
840        }
841        let module = self
842            .modules
843            .get(&canonical)
844            .or_else(|| self.modules.get(path))?;
845        for decl in &module.type_declarations {
846            if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
847                return Some(decl.clone());
848            }
849        }
850        if let Some(sources) = module.selective_re_exports.get(name) {
851            for source in sources {
852                if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
853                    return Some(decl);
854                }
855            }
856        }
857        for source in &module.wildcard_re_export_paths {
858            if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
859                return Some(decl);
860            }
861        }
862        None
863    }
864
865    fn find_exported_callable_decl(
866        &self,
867        path: &Path,
868        name: &str,
869        visited: &mut HashSet<PathBuf>,
870    ) -> Option<SNode> {
871        let canonical = normalize_path(path);
872        if !visited.insert(canonical.clone()) {
873            return None;
874        }
875        let module = self
876            .modules
877            .get(&canonical)
878            .or_else(|| self.modules.get(path))?;
879        for decl in &module.callable_declarations {
880            if callable_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
881                return Some(decl.clone());
882            }
883        }
884        if let Some(sources) = module.selective_re_exports.get(name) {
885            for source in sources {
886                if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
887                    return Some(decl);
888                }
889            }
890        }
891        for source in &module.wildcard_re_export_paths {
892            if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
893                return Some(decl);
894            }
895        }
896        None
897    }
898
899    /// Find the definition of `name` visible from `file`.
900    ///
901    /// Recurses through `pub import` re-export chains so go-to-definition
902    /// lands on the symbol's actual declaration site instead of the facade
903    /// module that forwarded it.
904    pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
905        let mut visited = HashSet::new();
906        self.definition_of_inner(file, name, &mut visited)
907    }
908
909    /// Find the declaration that contributes exported `name` to `file`.
910    ///
911    /// Unlike [`Self::definition_of`], this ignores private local declarations
912    /// and private imports. It follows only the module's public declaration and
913    /// `pub import` graph, making it suitable for package manifests, API docs,
914    /// and other consumers of a module's externally visible surface.
915    pub fn export_definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
916        let mut visited = HashSet::new();
917        self.export_definition_of_inner(file, name, &mut visited)
918    }
919
920    /// Sorted names of every declaration recorded for `file` (functions,
921    /// pipelines, tools, structs, ...). Used by the check-result cache to
922    /// key the cross-file lint-exemption subset that applies to this file.
923    pub fn declared_names_for_file(&self, file: &Path) -> Option<Vec<&str>> {
924        let module = self.modules.get(&normalize_path(file))?;
925        let mut names: Vec<&str> = module.declarations.keys().map(String::as_str).collect();
926        names.sort_unstable();
927        Some(names)
928    }
929
930    fn definition_of_inner(
931        &self,
932        file: &Path,
933        name: &str,
934        visited: &mut HashSet<PathBuf>,
935    ) -> Option<DefSite> {
936        let file = normalize_path(file);
937        if !visited.insert(file.clone()) {
938            return None;
939        }
940        let current = self.modules.get(&file)?;
941
942        if let Some(local) = current.declarations.get(name) {
943            return Some(local.clone());
944        }
945
946        // `pub import { name } from "..."` — follow the first recorded
947        // source. Conflicting re-exports surface separately as
948        // diagnostics; here we just pick a canonical destination so
949        // go-to-definition lands somewhere useful.
950        if let Some(sources) = current.selective_re_exports.get(name) {
951            for source in sources {
952                if let Some(def) = self.definition_of_inner(source, name, visited) {
953                    return Some(def);
954                }
955            }
956        }
957
958        // `pub import "..."` — chase each wildcard re-export source.
959        for source in &current.wildcard_re_export_paths {
960            if let Some(def) = self.definition_of_inner(source, name, visited) {
961                return Some(def);
962            }
963        }
964
965        // Private selective imports.
966        for import in &current.imports {
967            let Some(selective_names) = &import.selective_names else {
968                continue;
969            };
970            if !selective_names.contains(name) {
971                continue;
972            }
973            if let Some(path) = &import.path {
974                if let Some(def) = self.definition_of_inner(path, name, visited) {
975                    return Some(def);
976                }
977            }
978        }
979
980        // Private wildcard imports (not namespace — those bind only the alias).
981        for import in &current.imports {
982            if import.selective_names.is_some() || import.namespace_alias.is_some() {
983                continue;
984            }
985            if let Some(path) = &import.path {
986                if let Some(def) = self.definition_of_inner(path, name, visited) {
987                    return Some(def);
988                }
989            }
990        }
991
992        None
993    }
994
995    fn export_definition_of_inner(
996        &self,
997        file: &Path,
998        name: &str,
999        visited: &mut HashSet<PathBuf>,
1000    ) -> Option<DefSite> {
1001        let file = normalize_path(file);
1002        if !visited.insert(file.clone()) {
1003            return None;
1004        }
1005        let current = self.modules.get(&file)?;
1006
1007        if current.own_exports.contains(name) {
1008            if let Some(local) = current.declarations.get(name) {
1009                return Some(local.clone());
1010            }
1011        }
1012        if let Some(sources) = current.selective_re_exports.get(name) {
1013            for source in sources {
1014                if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1015                    return Some(definition);
1016                }
1017            }
1018        }
1019        for source in &current.wildcard_re_export_paths {
1020            if let Some(definition) = self.export_definition_of_inner(source, name, visited) {
1021                return Some(definition);
1022            }
1023        }
1024        None
1025    }
1026
1027    /// Diagnostics for re-export conflicts inside `file`. Each diagnostic
1028    /// names the conflicting symbol and the modules that contributed it,
1029    /// so check-time errors can be precise.
1030    pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
1031        let file = normalize_path(file);
1032        let Some(module) = self.modules.get(&file) else {
1033            return Vec::new();
1034        };
1035
1036        // Build, for each re-exported name, the set of source modules it
1037        // could resolve to. Names that resolve to more than one source are
1038        // ambiguous and reported.
1039        let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
1040
1041        for (name, srcs) in &module.selective_re_exports {
1042            sources
1043                .entry(name.clone())
1044                .or_default()
1045                .extend(srcs.iter().cloned());
1046        }
1047        for src in &module.wildcard_re_export_paths {
1048            let canonical = normalize_path(src);
1049            let Some(src_module) = self
1050                .modules
1051                .get(&canonical)
1052                .or_else(|| self.modules.get(src))
1053            else {
1054                continue;
1055            };
1056            for name in &src_module.exports {
1057                sources
1058                    .entry(name.clone())
1059                    .or_default()
1060                    .push(canonical.clone());
1061            }
1062        }
1063
1064        // A re-export that collides with a locally exported declaration is
1065        // also an error: the facade module cannot expose two different
1066        // bindings under the same name.
1067        for name in &module.own_exports {
1068            if let Some(entry) = sources.get_mut(name) {
1069                entry.push(file.clone());
1070            }
1071        }
1072
1073        let mut conflicts = Vec::new();
1074        for (name, mut srcs) in sources {
1075            srcs.sort();
1076            srcs.dedup();
1077            if srcs.len() > 1 {
1078                conflicts.push(ReExportConflict {
1079                    name,
1080                    sources: srcs,
1081                });
1082            }
1083        }
1084        conflicts.sort_by(|a, b| a.name.cmp(&b.name));
1085        conflicts
1086    }
1087
1088    /// Invalid selective imports in `file`, classified as missing or private.
1089    ///
1090    /// This is the static single source of truth for the runtime's export
1091    /// boundary. Consumers project the typed issue into CLI or editor
1092    /// diagnostics without independently re-resolving names or spans.
1093    pub fn selective_import_issues(&self, file: &Path) -> Vec<SelectiveImportIssue> {
1094        let file = normalize_path(file);
1095        let Some(module) = self.modules.get(&file) else {
1096            return Vec::new();
1097        };
1098
1099        let mut out = Vec::new();
1100        for import in &module.imports {
1101            let Some(selective) = &import.selective_names else {
1102                continue;
1103            };
1104            let Some(import_path) = &import.path else {
1105                continue;
1106            };
1107            let Some(target) = self
1108                .modules
1109                .get(import_path)
1110                .or_else(|| self.modules.get(&normalize_path(import_path)))
1111            else {
1112                continue;
1113            };
1114            if target.load_error.is_some() {
1115                continue;
1116            }
1117            for name in selective {
1118                let kind = if target.exports.contains(name) {
1119                    continue;
1120                } else if target.declarations.contains_key(name) {
1121                    SelectiveImportIssueKind::Private
1122                } else {
1123                    SelectiveImportIssueKind::Missing
1124                };
1125                out.push(SelectiveImportIssue {
1126                    name: name.clone(),
1127                    module: import.raw_path.clone(),
1128                    span: import.import_span,
1129                    kind,
1130                });
1131            }
1132        }
1133        out.sort_by(|a, b| (&a.name, &a.module, a.kind).cmp(&(&b.name, &b.module, b.kind)));
1134        out.dedup();
1135        out
1136    }
1137
1138    /// Return the declaration kind for a name on a module's public surface.
1139    /// Explicit and wildcard re-exports are followed so callers can consume
1140    /// the same source-owned contract regardless of the import path used.
1141    pub fn exported_kind(&self, file: &Path, name: &str) -> Option<DefKind> {
1142        self.exported_kind_inner(file, name, &mut HashSet::new())
1143    }
1144
1145    fn exported_kind_inner(
1146        &self,
1147        file: &Path,
1148        name: &str,
1149        visited: &mut HashSet<PathBuf>,
1150    ) -> Option<DefKind> {
1151        let file = normalize_path(file);
1152        if !visited.insert(file.clone()) {
1153            return None;
1154        }
1155        let result = self.modules.get(&file).and_then(|module| {
1156            if module.own_exports.contains(name) {
1157                return module
1158                    .declarations
1159                    .get(name)
1160                    .map(|definition| definition.kind)
1161                    .or_else(|| {
1162                        stdlib_module_from_path(&file).and_then(|stdlib_module| {
1163                            stdlib::builtin_reexports(stdlib_module)
1164                                .contains(&name)
1165                                .then_some(DefKind::Function)
1166                        })
1167                    });
1168            }
1169            if let Some(sources) = module.selective_re_exports.get(name) {
1170                for source in sources {
1171                    if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1172                        return Some(kind);
1173                    }
1174                }
1175            }
1176            for source in &module.wildcard_re_export_paths {
1177                if let Some(kind) = self.exported_kind_inner(source, name, visited) {
1178                    return Some(kind);
1179                }
1180            }
1181            None
1182        });
1183        visited.remove(&file);
1184        result
1185    }
1186}
1187
1188/// A duplicate or ambiguous re-export inside a single module. Reported by
1189/// [`ModuleGraph::re_export_conflicts`].
1190#[derive(Debug, Clone, PartialEq, Eq)]
1191pub struct ReExportConflict {
1192    pub name: String,
1193    pub sources: Vec<PathBuf>,
1194}
1195
1196/// Why a selective import cannot cross the target module's export boundary.
1197#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1198pub enum SelectiveImportIssueKind {
1199    /// No declaration or transitive export has the requested name.
1200    Missing,
1201    /// The target declares the requested name without exporting it.
1202    Private,
1203}
1204
1205/// A selective import rejected by the target module's public surface.
1206#[derive(Debug, Clone, PartialEq, Eq)]
1207pub struct SelectiveImportIssue {
1208    /// The requested name.
1209    pub name: String,
1210    /// The module path exactly as written in the import statement.
1211    pub module: String,
1212    /// Span anchored to the selective import statement.
1213    pub span: Span,
1214    /// Whether the requested name is absent or private.
1215    pub kind: SelectiveImportIssueKind,
1216}
1217
1218impl SelectiveImportIssue {
1219    /// Stable user-facing explanation shared by CLI and editor projections.
1220    #[must_use]
1221    pub fn message(&self) -> String {
1222        match self.kind {
1223            SelectiveImportIssueKind::Missing => format!(
1224                "imported symbol `{}` does not exist in `{}`",
1225                self.name, self.module
1226            ),
1227            SelectiveImportIssueKind::Private => format!(
1228                "imported symbol `{}` is not exported by `{}` — it is defined there but not `pub`",
1229                self.name, self.module
1230            ),
1231        }
1232    }
1233
1234    /// Stable repair guidance shared by CLI and editor projections.
1235    #[must_use]
1236    pub fn help(&self) -> String {
1237        match self.kind {
1238            SelectiveImportIssueKind::Missing => format!(
1239                "update the import to a symbol exported by `{}`",
1240                self.module
1241            ),
1242            SelectiveImportIssueKind::Private => {
1243                format!(
1244                    "mark `{}` as `pub` in `{}` to export it",
1245                    self.name, self.module
1246                )
1247            }
1248        }
1249    }
1250}
1251
1252fn load_module(
1253    path: &Path,
1254    package_snapshots: &[PackageSnapshot],
1255    source_overrides: Option<&HashMap<PathBuf, String>>,
1256) -> (ModuleInfo, Option<ParsedModuleSource>) {
1257    let source = source_overrides
1258        .and_then(|overrides| overrides.get(&normalize_path(path)).cloned())
1259        .or_else(|| read_module_source(path));
1260    let Some(source) = source else {
1261        return (ModuleInfo::default(), None);
1262    };
1263    let mut lexer = harn_lexer::Lexer::new(&source);
1264    let tokens = match lexer.tokenize() {
1265        Ok(tokens) => tokens,
1266        Err(error) => {
1267            let module = ModuleInfo {
1268                load_error: Some(ModuleLoadError {
1269                    message: error.to_string(),
1270                    span: error.span(),
1271                }),
1272                ..ModuleInfo::default()
1273            };
1274            return (module, None);
1275        }
1276    };
1277    let mut parser = Parser::new(tokens);
1278    let program = match parser.parse() {
1279        Ok(program) => program,
1280        Err(error) => {
1281            let module = ModuleInfo {
1282                load_error: Some(ModuleLoadError {
1283                    message: error.to_string(),
1284                    span: error.span(),
1285                }),
1286                ..ModuleInfo::default()
1287            };
1288            return (module, None);
1289        }
1290    };
1291
1292    let mut module = ModuleInfo::default();
1293    for node in &program {
1294        collect_module_info(path, node, &mut module, package_snapshots);
1295        collect_type_declarations(node, &mut module.type_declarations);
1296        collect_callable_declarations(node, &mut module.callable_declarations);
1297    }
1298    if let Some(stdlib_module) = stdlib_module_from_path(path) {
1299        module.own_exports.extend(
1300            stdlib::builtin_reexports(stdlib_module)
1301                .iter()
1302                .map(|name| (*name).to_string()),
1303        );
1304    }
1305    // Seed the transitive `exports` set from local exports plus selective
1306    // re-export names. Wildcard re-exports are folded in by
1307    // [`resolve_re_exports`] after every module has been loaded.
1308    module.exports.extend(module.own_exports.iter().cloned());
1309    module
1310        .exports
1311        .extend(module.selective_re_exports.keys().cloned());
1312    let parsed = ParsedModuleSource { source, program };
1313    (module, Some(parsed))
1314}
1315
1316/// Extract the stdlib module name when `path` is a `<std>/<name>`
1317/// virtual path, otherwise `None`.
1318fn stdlib_module_from_path(path: &Path) -> Option<&str> {
1319    let s = path.to_str()?;
1320    s.strip_prefix("<std>/")
1321}
1322
1323fn normalize_path(path: &Path) -> PathBuf {
1324    canonical_path(path)
1325}
1326
1327/// Canonicalize `path`, memoized process-wide.
1328///
1329/// Module-graph construction and every per-file graph query canonicalize
1330/// paths to dedupe import-edge spellings, and the check preflight scan
1331/// canonicalizes each visited module per checked file. `Path::canonicalize`
1332/// resolves every component through the kernel, so a whole-tree `harn check`
1333/// used to spend the bulk of its wall clock in path-resolution syscalls
1334/// (`getattrlist` dominated system time). One positive-result memo removes
1335/// the `O(files x import closure)` repetition; failed canonicalizations are
1336/// not memoized (mirroring the bytecode cache's `canonicalize_cached`) so a
1337/// file that appears later still resolves correctly in long-lived processes.
1338/// `<std>/` virtual paths pass through untouched.
1339pub fn canonical_path(path: &Path) -> PathBuf {
1340    use std::sync::OnceLock;
1341    if stdlib_module_from_path(path).is_some() {
1342        return path.to_path_buf();
1343    }
1344    static MEMO: OnceLock<std::sync::Mutex<HashMap<PathBuf, PathBuf>>> = OnceLock::new();
1345    let memo = MEMO.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
1346    if let Some(hit) = memo
1347        .lock()
1348        .expect("canonical path memo lock poisoned")
1349        .get(path)
1350        .cloned()
1351    {
1352        return hit;
1353    }
1354    match path.canonicalize() {
1355        Ok(canonical) => {
1356            memo.lock()
1357                .expect("canonical path memo lock poisoned")
1358                .insert(path.to_path_buf(), canonical.clone());
1359            canonical
1360        }
1361        Err(_) => path.to_path_buf(),
1362    }
1363}
1364
1365#[cfg(test)]
1366#[path = "tests.rs"]
1367mod tests;