Skip to main content

xidl_parser/hir/
include.rs

1use crate::parser::IncludeResolver;
2use std::collections::BTreeMap;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6/// Splits an include or import path at its trailing `@<version>` selector.
7///
8/// A leading `@` belongs to the path, so `"@xidl/http"` keeps its name, and a
9/// path without a usable selector keeps the `@` it was written with.
10pub fn split_version_selector(path: &str) -> (&str, Option<&str>) {
11    let Some(index) = path.rfind('@').filter(|index| *index > 0) else {
12        return (path, None);
13    };
14    let (target, selector) = path.split_at(index);
15    let selector = selector.trim_start_matches('@');
16    if selector.is_empty() {
17        return (path, None);
18    }
19    (target, Some(selector))
20}
21
22/// Resolves `#include` paths relative to the including file on disk.
23pub struct FsIncludeResolver;
24
25impl IncludeResolver for FsIncludeResolver {
26    fn resolve(
27        &mut self,
28        parent_path: Option<&str>,
29        path: &str,
30    ) -> crate::error::ParserResult<(String, String)> {
31        let parent = parent_path.map(Path::new).unwrap_or_else(|| Path::new("."));
32        let base = parent.parent().unwrap_or_else(|| Path::new("."));
33        let resolved_path = normalize_path(&base.join(path));
34        read_include(&resolved_path)
35    }
36}
37
38/// Resolves `#include` paths against dependency mounts before the filesystem.
39///
40/// Each mount maps a package name, such as `timestamp` or `xidl/std`, to the
41/// directory its library was fetched into. `#include "timestamp/lib.idl"` then
42/// resolves inside that package, while every other include keeps the
43/// filesystem-relative behaviour of [`FsIncludeResolver`].
44pub struct MountIncludeResolver {
45    mounts: BTreeMap<String, PathBuf>,
46}
47
48impl MountIncludeResolver {
49    /// Creates a resolver over `mounts`, keyed by dependency package name.
50    pub fn new(mounts: BTreeMap<String, PathBuf>) -> Self {
51        Self { mounts }
52    }
53
54    /// Finds the longest mount whose name prefixes `path`, with its root.
55    fn mount_for(&self, path: &str) -> Option<(&str, &Path)> {
56        self.mounts
57            .iter()
58            .filter(|(name, _)| {
59                path.strip_prefix(name.as_str())
60                    .is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
61            })
62            .max_by_key(|(name, _)| name.len())
63            .map(|(name, root)| (name.as_str(), root.as_path()))
64    }
65}
66
67impl IncludeResolver for MountIncludeResolver {
68    fn resolve(
69        &mut self,
70        parent_path: Option<&str>,
71        path: &str,
72    ) -> crate::error::ParserResult<(String, String)> {
73        let Some((name, root)) = self.mount_for(path) else {
74            return FsIncludeResolver.resolve(parent_path, path);
75        };
76        let rest = path[name.len()..].trim_start_matches('/');
77        let resolved_path = normalize_path(&root.join(rest));
78        if !resolved_path.is_file() {
79            return Err(crate::error::ParseError::Message(format!(
80                "dependency include '{path}' does not exist: the '{name}' mount at '{}' \
81                 resolves it to '{}'",
82                root.display(),
83                resolved_path.display()
84            )));
85        }
86        read_include(&resolved_path).map_err(|err| {
87            crate::error::ParseError::Message(format!(
88                "dependency include '{path}' could not be read through the '{name}' mount \
89                 at '{}': {err}",
90                root.display()
91            ))
92        })
93    }
94}
95
96/// Reads one resolved include target, naming it when it cannot be loaded.
97fn read_include(resolved_path: &Path) -> crate::error::ParserResult<(String, String)> {
98    if !resolved_path.is_file() {
99        return Err(crate::error::ParseError::Message(format!(
100            "include path '{}' does not exist",
101            resolved_path.display()
102        )));
103    }
104
105    let content = fs::read_to_string(resolved_path).map_err(|err| {
106        crate::error::ParseError::Message(format!(
107            "failed to read include '{}': {err}",
108            resolved_path.display()
109        ))
110    })?;
111
112    Ok((resolved_path.display().to_string(), content))
113}
114
115pub(crate) fn normalize_path(path: &Path) -> PathBuf {
116    let path = if path.is_absolute() {
117        path.to_path_buf()
118    } else {
119        std::env::current_dir()
120            .unwrap_or_else(|_| PathBuf::from("."))
121            .join(path)
122    };
123    fs::canonicalize(&path).unwrap_or(path)
124}