Skip to main content

lutra_compiler/
discover.rs

1use std::collections::HashMap;
2use std::ffi::OsStr;
3use std::path::{Path, PathBuf};
4use std::{fs, io};
5
6use crate::error::Error;
7use crate::project;
8
9#[cfg_attr(feature = "clap", derive(clap::Parser))]
10#[derive(Clone)]
11pub struct DiscoverParams {
12    /// Path to a project file
13    #[cfg_attr(feature = "clap", arg(long))]
14    pub project: Option<PathBuf>,
15}
16
17pub fn discover(params: DiscoverParams) -> Result<project::SourceTree, Error> {
18    let Some(a_project_file) = params.project else {
19        return Ok(project::SourceTree::empty());
20    };
21
22    // walk up the module tree
23    tracing::debug!("searching for project root");
24    let mut root_file = a_project_file.canonicalize()?;
25    if root_file.is_dir() {
26        root_file.push("module.lt");
27    }
28    let mut loaded_files = HashMap::new();
29    loop {
30        let file_contents =
31            fs::read_to_string(&root_file).map_err(|io| Error::CannotReadSourceFile {
32                file: root_file.clone(),
33                io,
34            })?;
35
36        let is_submodule = crate::parser::is_submodule(&file_contents).unwrap_or(false);
37
38        loaded_files.insert(root_file.clone(), file_contents);
39        if is_submodule {
40            root_file = parent_module(&root_file).ok_or(Error::CannotFindProjectRoot)?;
41        } else {
42            break;
43        }
44    }
45
46    let root = if root_file.ends_with("module.lt") {
47        root_file.parent().unwrap().to_path_buf()
48    } else {
49        root_file.clone()
50    };
51    tracing::debug!("project root: {}", root.display());
52    let mut project = project::SourceTree::new([], root.clone());
53
54    // walk down the module tree
55    tracing::debug!("loading project files");
56    let target_extension = Some(OsStr::new("lt"));
57    let mut paths_to_load = vec![root_file];
58    while let Some(path) = paths_to_load.pop() {
59        tracing::debug!("  path: {}", path.display());
60        let content = if let Some(c) = loaded_files.remove(&path) {
61            // use file that was read before
62            c
63        } else {
64            // read the file
65            let content = match fs::read_to_string(&path) {
66                Ok(c) => c,
67                Err(e) if path.ends_with("module.lt") && e.kind() == io::ErrorKind::NotFound => {
68                    // subdir/module.lt is allowed not to exist
69                    continue;
70                }
71                Err(e) => {
72                    return Err(Error::CannotReadSourceFile {
73                        file: path.to_path_buf(),
74                        io: e,
75                    });
76                }
77            };
78
79            // but include it only if it is a submodule
80            let is_submodule = crate::parser::is_submodule(&content).unwrap_or(true);
81            if !is_submodule {
82                continue;
83            }
84            content
85        };
86
87        let relative_path = project.get_relative_path(&path).unwrap().to_path_buf();
88        project.insert(relative_path, content);
89
90        // for module files, read the whole dir
91        if path.ends_with("module.lt") {
92            let Some(dir_path) = path.parent() else {
93                continue;
94            };
95            tracing::debug!("  reading dir: {}", dir_path.display());
96            let mut new_paths = Vec::new();
97            for entry in fs::read_dir(dir_path)? {
98                let entry = entry?;
99                let entry_path = entry.path();
100                let metadata = entry.metadata()?;
101
102                if metadata.is_dir() {
103                    new_paths.push(entry.path().join("module.lt"));
104                } else if metadata.is_file()
105                    && entry.file_name() != "module.lt" // we've already read that
106                    && entry_path.extension() == target_extension
107                {
108                    new_paths.push(entry.path());
109                }
110            }
111            new_paths.sort();
112            paths_to_load.extend(new_paths);
113        }
114    }
115
116    Ok(project)
117}
118
119fn parent_module(path: &Path) -> Option<PathBuf> {
120    Some(path.parent()?.join("module.lt"))
121}