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