1use super::*;
2
3pub struct Compiler;
4
5impl Compiler {
6 pub fn compile<'src>(loader: &'src Loader, root: &Path) -> RunResult<'src, Compilation<'src>> {
7 let mut asts = HashMap::<PathBuf, Ast>::new();
8 let mut loaded = Vec::new();
9 let mut paths = HashMap::<PathBuf, PathBuf>::new();
10 let mut srcs = HashMap::<PathBuf, &str>::new();
11
12 let mut stack = Vec::new();
13 stack.push(Source::root(root));
14
15 while let Some(current) = stack.pop() {
16 let (relative, src) = loader.load(root, ¤t.path)?;
17 loaded.push(relative.into());
18 let tokens = Lexer::lex(relative, src)?;
19 let mut ast = Parser::parse(
20 current.file_depth,
21 ¤t.import_offsets,
22 ¤t.namepath,
23 &tokens,
24 ¤t.working_directory,
25 )?;
26
27 paths.insert(current.path.clone(), relative.into());
28 srcs.insert(current.path.clone(), src);
29
30 for item in &mut ast.items {
31 match item {
32 Item::Module {
33 absolute,
34 name,
35 optional,
36 relative,
37 ..
38 } => {
39 let parent = current.path.parent().unwrap();
40
41 let relative = relative
42 .as_ref()
43 .map(|relative| Self::expand_tilde(&relative.cooked))
44 .transpose()?;
45
46 let import = Self::find_module_file(parent, *name, relative.as_deref())?;
47
48 if let Some(import) = import {
49 if current.file_path.contains(&import) {
50 return Err(Error::CircularImport {
51 current: current.path,
52 import,
53 });
54 }
55 *absolute = Some(import.clone());
56 stack.push(current.module(*name, import));
57 } else if !*optional {
58 return Err(Error::MissingModuleFile { module: *name });
59 }
60 }
61 Item::Import {
62 relative,
63 absolute,
64 optional,
65 path,
66 } => {
67 let import = current
68 .path
69 .parent()
70 .unwrap()
71 .join(Self::expand_tilde(&relative.cooked)?)
72 .lexiclean();
73
74 if import.is_file() {
75 if current.file_path.contains(&import) {
76 return Err(Error::CircularImport {
77 current: current.path,
78 import,
79 });
80 }
81 *absolute = Some(import.clone());
82 stack.push(current.import(import, path.offset));
83 } else if !*optional {
84 return Err(Error::MissingImportFile { path: *path });
85 }
86 }
87 _ => {}
88 }
89 }
90
91 asts.insert(current.path, ast.clone());
92 }
93
94 let justfile = Analyzer::analyze(&asts, None, &[], &loaded, None, &paths, root)?;
95
96 Ok(Compilation {
97 asts,
98 justfile,
99 root: root.into(),
100 srcs,
101 })
102 }
103
104 fn find_module_file<'src>(
105 parent: &Path,
106 module: Name<'src>,
107 path: Option<&Path>,
108 ) -> RunResult<'src, Option<PathBuf>> {
109 let mut candidates = Vec::new();
110
111 if let Some(path) = path {
112 let full = parent.join(path);
113
114 if full.is_file() {
115 return Ok(Some(full));
116 }
117
118 candidates.push((path.join("mod.just"), true));
119
120 for name in search::JUSTFILE_NAMES {
121 candidates.push((path.join(name), false));
122 }
123 } else {
124 candidates.push((format!("{module}.just").into(), true));
125 candidates.push((format!("{module}/mod.just").into(), true));
126
127 for name in search::JUSTFILE_NAMES {
128 candidates.push((format!("{module}/{name}").into(), false));
129 }
130 }
131
132 let mut grouped = BTreeMap::<PathBuf, Vec<(PathBuf, bool)>>::new();
133
134 for (candidate, case_sensitive) in candidates {
135 let candidate = parent.join(candidate).lexiclean();
136 grouped
137 .entry(candidate.parent().unwrap().into())
138 .or_default()
139 .push((candidate, case_sensitive));
140 }
141
142 let mut found = Vec::new();
143
144 for (directory, candidates) in grouped {
145 let entries = match fs::read_dir(&directory) {
146 Ok(entries) => entries,
147 Err(io_error) => {
148 if io_error.kind() == io::ErrorKind::NotFound {
149 continue;
150 }
151
152 return Err(
153 SearchError::Io {
154 io_error,
155 directory,
156 }
157 .into(),
158 );
159 }
160 };
161
162 for entry in entries {
163 let entry = entry.map_err(|io_error| SearchError::Io {
164 io_error,
165 directory: directory.clone(),
166 })?;
167
168 if let Some(name) = entry.file_name().to_str() {
169 for (candidate, case_sensitive) in &candidates {
170 let candidate_name = candidate.file_name().unwrap().to_str().unwrap();
171
172 let eq = if *case_sensitive {
173 name == candidate_name
174 } else {
175 name.eq_ignore_ascii_case(candidate_name)
176 };
177
178 if eq {
179 found.push(candidate.parent().unwrap().join(name));
180 }
181 }
182 }
183 }
184 }
185
186 if found.len() > 1 {
187 found.sort();
188 Err(Error::AmbiguousModuleFile {
189 found: found
190 .into_iter()
191 .map(|found| found.strip_prefix(parent).unwrap().into())
192 .collect(),
193 module,
194 })
195 } else {
196 Ok(found.into_iter().next())
197 }
198 }
199
200 fn expand_tilde(path: &str) -> RunResult<'static, PathBuf> {
201 Ok(if let Some(path) = path.strip_prefix("~/") {
202 dirs::home_dir()
203 .ok_or(Error::Homedir)?
204 .join(path.trim_start_matches('/'))
205 } else {
206 PathBuf::from(path)
207 })
208 }
209
210 #[cfg(test)]
211 pub fn test_compile(src: &str) -> CompileResult<Justfile> {
212 let tokens = Lexer::test_lex(src)?;
213 let ast = Parser::parse(0, &[], &Namepath::default(), &tokens, &PathBuf::new())?;
214 let root = PathBuf::from("justfile");
215 let mut asts: HashMap<PathBuf, Ast> = HashMap::new();
216 asts.insert(root.clone(), ast);
217 let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();
218 paths.insert(root.clone(), root.clone());
219 Analyzer::analyze(&asts, None, &[], &[], None, &paths, &root)
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use {super::*, temptree::temptree};
226
227 #[test]
228 fn include_justfile() {
229 let justfile_a = r#"
230# A comment at the top of the file
231import "./justfile_b"
232
233#some_recipe: recipe_b
234some_recipe:
235 echo "some recipe"
236"#;
237
238 let justfile_b = r#"import "./subdir/justfile_c"
239
240recipe_b: recipe_c
241 echo "recipe b"
242"#;
243
244 let justfile_c = r#"recipe_c:
245 echo "recipe c"
246"#;
247
248 let tmp = temptree! {
249 justfile: justfile_a,
250 justfile_b: justfile_b,
251 subdir: {
252 justfile_c: justfile_c
253 }
254 };
255
256 let loader = Loader::new();
257
258 let justfile_a_path = tmp.path().join("justfile");
259 let compilation = Compiler::compile(&loader, &justfile_a_path).unwrap();
260
261 assert_eq!(compilation.root_src(), justfile_a);
262 }
263
264 #[test]
265 fn recursive_includes_fail() {
266 let tmp = temptree! {
267 justfile: "import './subdir/b'\na: b",
268 subdir: {
269 b: "import '../justfile'\nb:"
270 }
271 };
272
273 let loader = Loader::new();
274
275 let justfile_a_path = tmp.path().join("justfile");
276 let loader_output = Compiler::compile(&loader, &justfile_a_path).unwrap_err();
277
278 assert_matches!(loader_output, Error::CircularImport { current, import }
279 if current == tmp.path().join("subdir").join("b").lexiclean() &&
280 import == tmp.path().join("justfile").lexiclean()
281 );
282 }
283
284 #[test]
285 fn find_module_file() {
286 #[track_caller]
287 fn case(path: Option<&str>, files: &[&str], expected: Result<Option<&str>, &[&str]>) {
288 let module = Name {
289 token: Token {
290 column: 0,
291 kind: TokenKind::Identifier,
292 length: 3,
293 line: 0,
294 offset: 0,
295 path: Path::new(""),
296 src: "foo",
297 },
298 };
299
300 let tempdir = tempfile::tempdir().unwrap();
301
302 for file in files {
303 if let Some(parent) = Path::new(file).parent() {
304 fs::create_dir_all(tempdir.path().join(parent)).unwrap();
305 }
306
307 fs::write(tempdir.path().join(file), "").unwrap();
308 }
309
310 let actual = Compiler::find_module_file(tempdir.path(), module, path.map(Path::new));
311
312 match expected {
313 Err(expected) => match actual.unwrap_err() {
314 Error::AmbiguousModuleFile { found, .. } => {
315 assert_eq!(
316 found,
317 expected
318 .iter()
319 .map(|expected| expected.replace('/', std::path::MAIN_SEPARATOR_STR).into())
320 .collect::<Vec<PathBuf>>()
321 );
322 }
323 _ => panic!("unexpected error"),
324 },
325 Ok(Some(expected)) => assert_eq!(
326 actual.unwrap().unwrap(),
327 tempdir
328 .path()
329 .join(expected.replace('/', std::path::MAIN_SEPARATOR_STR))
330 ),
331 Ok(None) => assert_eq!(actual.unwrap(), None),
332 }
333 }
334
335 case(None, &["foo.just"], Ok(Some("foo.just")));
336 case(None, &["FOO.just"], Ok(None));
337 case(None, &["foo/mod.just"], Ok(Some("foo/mod.just")));
338 case(None, &["foo/MOD.just"], Ok(None));
339 case(None, &["foo/justfile"], Ok(Some("foo/justfile")));
340 case(None, &["foo/JUSTFILE"], Ok(Some("foo/JUSTFILE")));
341 case(None, &["foo/.justfile"], Ok(Some("foo/.justfile")));
342 case(None, &["foo/.JUSTFILE"], Ok(Some("foo/.JUSTFILE")));
343 case(
344 None,
345 &["foo/.justfile", "foo/justfile"],
346 Err(&["foo/.justfile", "foo/justfile"]),
347 );
348 case(None, &["foo/JUSTFILE"], Ok(Some("foo/JUSTFILE")));
349
350 case(Some("bar"), &["bar"], Ok(Some("bar")));
351 case(Some("bar"), &["bar/mod.just"], Ok(Some("bar/mod.just")));
352 case(Some("bar"), &["bar/justfile"], Ok(Some("bar/justfile")));
353 case(Some("bar"), &["bar/JUSTFILE"], Ok(Some("bar/JUSTFILE")));
354 case(Some("bar"), &["bar/.justfile"], Ok(Some("bar/.justfile")));
355 case(Some("bar"), &["bar/.JUSTFILE"], Ok(Some("bar/.JUSTFILE")));
356
357 case(
358 Some("bar"),
359 &["bar/justfile", "bar/mod.just"],
360 Err(&["bar/justfile", "bar/mod.just"]),
361 );
362 }
363}