Skip to main content

sequoia_seq/
resolve.rs

1use std::collections::HashMap;
2use std::path::{Component, Path, PathBuf};
3
4use crate::error::SeqError;
5use crate::ir::{ImportDecl, SeqApp, SeqFile};
6use crate::parse_seq_file;
7
8const MAX_IMPORT_DEPTH: usize = 32;
9
10pub trait SeqSource {
11    fn read(&self, parent: &Path, rel: &str) -> Result<(PathBuf, String), String>;
12
13    fn root(&self, file: &str) -> PathBuf {
14        PathBuf::from(normalize_display(Path::new(file)))
15    }
16}
17
18pub struct FsSource;
19
20impl SeqSource for FsSource {
21    fn read(&self, parent: &Path, rel: &str) -> Result<(PathBuf, String), String> {
22        let full = join_lexical(parent, rel);
23        let canonical = std::fs::canonicalize(&full)
24            .map_err(|e| format!("unable to read '{}': {e}", full.display()))?;
25        let src = std::fs::read_to_string(&canonical)
26            .map_err(|e| format!("unable to read '{}': {e}", full.display()))?;
27        Ok((canonical, src))
28    }
29
30    fn root(&self, file: &str) -> PathBuf {
31        std::fs::canonicalize(file)
32            .unwrap_or_else(|_| PathBuf::from(normalize_display(Path::new(file))))
33    }
34}
35
36pub struct MapSource {
37    files: HashMap<String, String>,
38}
39
40impl MapSource {
41    pub fn new(files: &[(&str, &str)]) -> Self {
42        let files = files
43            .iter()
44            .map(|(p, s)| (normalize_display(Path::new(p)), (*s).to_string()))
45            .collect();
46        Self { files }
47    }
48}
49
50impl SeqSource for MapSource {
51    fn read(&self, parent: &Path, rel: &str) -> Result<(PathBuf, String), String> {
52        let joined = join_lexical(parent, rel);
53        let key = normalize_display(&joined);
54        match self.files.get(&key) {
55            Some(src) => Ok((PathBuf::from(&key), src.clone())),
56            None => Err(format!("brak pliku '{key}'")),
57        }
58    }
59}
60
61pub struct ResolvedImports {
62    pub app: SeqApp,
63
64    pub files: Vec<PathBuf>,
65}
66
67struct Resolver<'a> {
68    source: &'a dyn SeqSource,
69    seen: HashMap<PathBuf, ()>,
70    stack: Vec<(PathBuf, String)>,
71    components: Vec<crate::ir::ComponentDef>,
72    files: Vec<PathBuf>,
73}
74
75pub fn resolve_imports(
76    file: &str,
77    root: SeqFile,
78    source: &dyn SeqSource,
79) -> Result<ResolvedImports, SeqError> {
80    let Some(mut app) = root.app else {
81        return Err(SeqError::new(file, 1, 1, "brak bloku `app Nazwa { }`"));
82    };
83    let root_id = source.root(file);
84    let mut r = Resolver {
85        source,
86        seen: HashMap::new(),
87        stack: vec![(root_id.clone(), file.to_string())],
88        components: root.components,
89        files: vec![root_id],
90    };
91    r.seen.insert(r.files[0].clone(), ());
92    r.collect(file, &root.imports, 1)?;
93    app.components = r.components;
94    app.imports.clear();
95    Ok(ResolvedImports {
96        app,
97        files: r.files,
98    })
99}
100
101impl Resolver<'_> {
102    fn collect(
103        &mut self,
104        parent_display: &str,
105        imports: &[ImportDecl],
106        depth: usize,
107    ) -> Result<(), SeqError> {
108        for imp in imports {
109            if depth > MAX_IMPORT_DEPTH {
110                return Err(SeqError::new(
111                    parent_display,
112                    imp.line,
113                    1,
114                    format!("import: nesting limit exceeded ({MAX_IMPORT_DEPTH})"),
115                ));
116            }
117            let parent = Path::new(parent_display);
118            let (id, src) = self.source.read(parent, &imp.path).map_err(|e| {
119                SeqError::new(
120                    parent_display,
121                    imp.line,
122                    1,
123                    format!("import \"{}\": {e}", imp.path),
124                )
125            })?;
126            let display = normalize_display(&join_lexical(parent, &imp.path));
127            if let Some(pos) = self.stack.iter().position(|(i, _)| i == &id) {
128                let mut names: Vec<String> =
129                    self.stack[pos..].iter().map(|(_, d)| d.clone()).collect();
130                names.push(display.clone());
131                return Err(SeqError::new(
132                    parent_display,
133                    imp.line,
134                    1,
135                    format!("cykliczny import: {}", names.join(" -> ")),
136                ));
137            }
138            if self.seen.contains_key(&id) {
139                continue;
140            }
141            self.seen.insert(id.clone(), ());
142            let seq = parse_seq_file(&display, &src)?;
143            if seq.app.is_some() {
144                return Err(SeqError::new(
145                    parent_display,
146                    imp.line,
147                    1,
148                    format!("imported file '{display}' cannot contain `app`"),
149                ));
150            }
151            self.files.push(id.clone());
152            self.components.extend(seq.components);
153            self.stack.push((id, display.clone()));
154            self.collect(&display, &seq.imports, depth + 1)?;
155            self.stack.pop();
156        }
157        Ok(())
158    }
159}
160
161fn join_lexical(parent: &Path, rel: &str) -> PathBuf {
162    let requested = Path::new(rel);
163    if requested.is_absolute() {
164        requested.to_path_buf()
165    } else {
166        parent
167            .parent()
168            .unwrap_or_else(|| Path::new(""))
169            .join(requested)
170    }
171}
172
173fn normalize_display(path: &Path) -> String {
174    let mut out = String::new();
175    for c in path.components() {
176        match c {
177            Component::CurDir => {}
178            Component::ParentDir => {
179                if out.is_empty() || out.ends_with("..") {
180                    push_display(&mut out, "..");
181                } else if let Some(pos) = out.rfind('/') {
182                    out.truncate(pos);
183                } else {
184                    out.clear();
185                }
186            }
187            Component::RootDir => out.push('/'),
188            Component::Prefix(p) => out.push_str(&p.as_os_str().to_string_lossy()),
189            Component::Normal(s) => push_display(&mut out, &s.to_string_lossy()),
190        }
191    }
192    out
193}
194
195fn push_display(out: &mut String, segment: &str) {
196    if !out.is_empty() && !out.ends_with('/') {
197        out.push('/');
198    }
199    out.push_str(segment);
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn normalize_relative_and_parent() {
208        assert_eq!(normalize_display(Path::new("./ui/card.seq")), "ui/card.seq");
209        assert_eq!(normalize_display(Path::new("a/../b.seq")), "b.seq");
210        assert_eq!(normalize_display(Path::new("../x.seq")), "../x.seq");
211        assert_eq!(normalize_display(Path::new("ui/./card.seq")), "ui/card.seq");
212    }
213
214    #[test]
215    fn join_relative_to_file_dir() {
216        let j = join_lexical(Path::new("dir/Main.seq"), "./ui/card.seq");
217        assert_eq!(normalize_display(&j), "dir/ui/card.seq");
218    }
219}