1use crate::{
2 ast::{Module, Statement},
3 error::BuildError,
4 file::canonicalize_native_path,
5 infrastructure::FileSystem,
6 parse::parse,
7};
8use std::{
9 collections::HashMap,
10 path::{Path, PathBuf},
11};
12
13pub async fn load(
15 file_system: &impl FileSystem,
16 path: &Path,
17) -> Result<HashMap<PathBuf, Module>, BuildError> {
18 let mut paths = vec![(path.to_path_buf(), vec![])];
19 let mut modules = HashMap::new();
20
21 while let Some((path, mut ancestors)) = paths.pop() {
22 let canonical_path = canonicalize_native_path(&path);
23
24 if ancestors.contains(&canonical_path) {
25 return Err(BuildError::CircularModuleDependency);
26 } else if modules.contains_key(&canonical_path) {
27 continue;
28 }
29
30 let module = parse(&file_system.read_file_to_string(&path).await?)?;
31
32 ancestors.push(canonical_path.clone());
33 paths.extend(
34 module
35 .statements()
36 .iter()
37 .filter_map(|statement| match statement {
38 Statement::Include(include) => Some(include.path()),
39 Statement::Submodule(submodule) => Some(submodule.path()),
40 _ => None,
41 })
42 .map(|path| (path.into(), ancestors.clone())),
44 );
45
46 modules.insert(canonical_path, module);
47 }
48
49 Ok(modules)
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55 use crate::infrastructure::{FakeFileSystem, FileError};
56 use pretty_assertions::assert_eq;
57
58 fn create_modules(sources: &[(&str, &str)]) -> HashMap<PathBuf, Module> {
59 sources
60 .iter()
61 .map(|(path, source)| (PathBuf::from(path), parse(source).unwrap()))
62 .collect()
63 }
64
65 fn create_file_system(sources: &[(&str, &str)]) -> FakeFileSystem {
66 let file_system = FakeFileSystem::default();
67
68 for (path, source) in sources {
69 file_system.write_file(path, source);
70 }
71
72 file_system
73 }
74
75 #[tokio::test]
76 async fn load_root_module() {
77 let sources = [("build.ninja", "x = 42\n")];
78
79 assert_eq!(
80 load(&create_file_system(&sources), Path::new("build.ninja")).await,
81 Ok(create_modules(&sources))
82 );
83 }
84
85 #[tokio::test]
86 async fn load_included_module() {
87 let sources = [
88 ("build.ninja", "include foo.ninja\n"),
89 ("foo.ninja", "x = 42\n"),
90 ];
91
92 assert_eq!(
93 load(&create_file_system(&sources), Path::new("build.ninja")).await,
94 Ok(create_modules(&sources))
95 );
96 }
97
98 #[tokio::test]
99 async fn load_child_module() {
100 let sources = [
101 ("build.ninja", "subninja foo.ninja\n"),
102 ("foo.ninja", "x = 42\n"),
103 ];
104
105 assert_eq!(
106 load(&create_file_system(&sources), Path::new("build.ninja")).await,
107 Ok(create_modules(&sources))
108 );
109 }
110
111 #[tokio::test]
112 async fn load_module_included_from_two_modules() {
113 let sources = [
114 ("build.ninja", "include foo.ninja\ninclude bar.ninja\n"),
115 ("foo.ninja", "include baz.ninja\n"),
116 ("bar.ninja", "subninja baz.ninja\n"),
117 ("baz.ninja", "x = 42\n"),
118 ];
119 let file_system = create_file_system(&sources);
120
121 assert_eq!(
122 load(&file_system, Path::new("build.ninja")).await,
123 Ok(create_modules(&sources))
124 );
125 assert_eq!(
126 file_system
127 .read_requests()
128 .iter()
129 .filter(|path| path == &Path::new("baz.ninja"))
130 .count(),
131 1
132 );
133 }
134
135 #[tokio::test]
136 async fn canonicalize_root_module_path() {
137 let source = "x = 42\n";
138 let file_system = create_file_system(&[("./build.ninja", source)]);
139
140 assert_eq!(
141 load(&file_system, Path::new("./build.ninja")).await,
142 Ok(create_modules(&[("build.ninja", source)]))
143 );
144 assert_eq!(
145 file_system.read_requests(),
146 [PathBuf::from("./build.ninja")]
147 );
148 }
149
150 #[tokio::test]
151 async fn canonicalize_included_module_paths() {
152 let root_source = "include ./foo.ninja\nsubninja bar/../foo.ninja\n";
153 let source = "x = 42\n";
154 let file_system = create_file_system(&[
155 ("build.ninja", root_source),
156 ("./foo.ninja", source),
157 ("bar/../foo.ninja", source),
158 ]);
159
160 assert_eq!(
161 load(&file_system, Path::new("build.ninja")).await,
162 Ok(create_modules(&[
163 ("build.ninja", root_source),
164 ("foo.ninja", source)
165 ]))
166 );
167 assert_eq!(file_system.read_requests().len(), 2);
168 }
169
170 #[tokio::test]
171 async fn resolve_module_path_relative_to_working_directory() {
172 let sources = [
173 ("build.ninja", "include foo/foo.ninja\n"),
174 ("foo/foo.ninja", "include bar.ninja\n"),
175 ("bar.ninja", "x = 42\n"),
176 ];
177
178 assert_eq!(
179 load(&create_file_system(&sources), Path::new("build.ninja")).await,
180 Ok(create_modules(&sources))
181 );
182 }
183
184 #[tokio::test]
185 async fn fail_to_read_module() {
186 assert_eq!(
187 load(
188 &create_file_system(&[("build.ninja", "include foo.ninja\n")]),
189 Path::new("build.ninja")
190 )
191 .await,
192 Err(FileError::new("file not found").into())
193 );
194 }
195
196 #[tokio::test]
197 async fn fail_to_read_module_in_missing_directory() {
198 assert_eq!(
199 load(
200 &create_file_system(&[
201 ("build.ninja", "include bar/../foo.ninja\n"),
202 ("foo.ninja", "x = 42\n"),
203 ]),
204 Path::new("build.ninja")
205 )
206 .await,
207 Err(FileError::new("file not found").into())
208 );
209 }
210
211 #[tokio::test]
212 async fn fail_to_include_module_in_itself() {
213 assert_eq!(
214 load(
215 &create_file_system(&[("build.ninja", "include build.ninja\n")]),
216 Path::new("build.ninja")
217 )
218 .await,
219 Err(BuildError::CircularModuleDependency)
220 );
221 }
222
223 #[tokio::test]
224 async fn fail_to_include_modules_in_each_other() {
225 assert_eq!(
226 load(
227 &create_file_system(&[
228 ("build.ninja", "subninja foo.ninja\n"),
229 ("foo.ninja", "subninja ./build.ninja\n"),
230 ]),
231 Path::new("build.ninja")
232 )
233 .await,
234 Err(BuildError::CircularModuleDependency)
235 );
236 }
237}