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}