cargo_unify/
lib.rs

1#![feature(let_chains)]
2
3#[cfg(test)]
4mod tests;
5
6use std::{
7    fs,
8    path::{Path, PathBuf},
9};
10
11use anyhow::{Context, anyhow};
12
13/// Recursively expand module declerations.
14pub fn expand(file: &str, path: &Path) -> anyhow::Result<String> {
15    let dir = mod_dir(path)?;
16
17    let mod_decls = mod_declarations(file).with_context(|| {
18        format!(
19            "Failed to parse module in: {} for `mod` declarations",
20            path.display()
21        )
22    })?;
23
24    let mut expanded = String::with_capacity(file.len());
25    let mut last_match = 0;
26
27    for (name, pos) in mod_decls {
28        let (mod_content, mod_path) = get_mod(name.as_str(), &dir)?;
29
30        let mut expanded_mod = expand(&mod_content, &mod_path)?;
31        expanded_mod.insert_str(0, " {\n");
32        expanded_mod.push_str("\n} ");
33
34        expanded.push_str(&file[last_match..pos]);
35        expanded.push_str(&expanded_mod);
36
37        last_match = pos + 1;
38    }
39
40    expanded.push_str(&file[last_match..]);
41
42    Ok(expanded)
43}
44
45fn mod_declarations(file: &str) -> anyhow::Result<impl Iterator<Item = (String, usize)>> {
46    let ast: syn::File = syn::parse_file(file)?;
47
48    let decls = ast.items.into_iter().filter_map(|item| {
49        if let syn::Item::Mod(syn::ItemMod { semi, ident, .. }) = item
50            && let Some(syn::token::Semi { spans: [semi] }) = semi
51        {
52            // Position of the semicolon at the end of the module declaration
53            let pos = semi.start();
54            let idx = file
55                .lines()
56                .take(pos.line) // The line is 1-indexed
57                .enumerate()
58                .fold(
59                    pos.line - 1, // to include the line separators
60                    |acc, (i, l)| {
61                        if i == pos.line - 1 {
62                            acc + pos.column
63                        } else {
64                            acc + l.len()
65                        }
66                    },
67                );
68
69            Some((ident.to_string(), idx))
70        } else {
71            None
72        }
73    });
74
75    Ok(decls)
76}
77
78/// Finds a module's directory for the module in the provided path.
79///
80/// If the path file name is `main.rs` or `lib.rs`, it is not considered a module,
81/// and the module directory is the parent directory of the path.
82/// Otherwise, the module directory is the parent directory of the path with the
83/// module name appended.
84///
85/// Examples:
86/// `src/main.rs` -> `src/`
87/// `src/foo.rs`  -> `src/foo/`
88fn mod_dir(path: &Path) -> anyhow::Result<PathBuf> {
89    let dir = path
90        .parent()
91        .with_context(|| format!("Failed to get parent directory of '{}'", path.display()))?;
92
93    let mod_name = path
94        .file_stem()
95        .and_then(|name| name.to_str())
96        .with_context(|| format!("Failed to convert file name '{}' to string", path.display()))?;
97
98    Ok(if ["main", "lib", "mod"].contains(&mod_name) {
99        dir.to_path_buf()
100    } else {
101        dir.join(mod_name)
102    })
103}
104
105fn get_mod(name: &str, parent: &Path) -> anyhow::Result<(String, PathBuf)> {
106    // <name>.rs
107    let file_mod = parent.join(name.to_owned() + ".rs");
108    // <name>/mod.rs
109    let dir_mod = extend_path(parent, &[name, "mod.rs"]);
110
111    for path in [file_mod, dir_mod] {
112        if let Ok(content) = read_path(&path) {
113            return Ok((content, path));
114        }
115    }
116
117    Err(anyhow!("Couldn't find module file for module `{name}`"))
118}
119
120#[inline]
121pub fn read_path(path: &Path) -> anyhow::Result<String> {
122    fs::read_to_string(path).with_context(|| format!("Failed to read path {}", path.display()))
123}
124
125/// Join `base` with `nodes`.
126pub fn extend_path(base: &Path, nodes: &[&str]) -> PathBuf {
127    let mut path = base.to_path_buf();
128
129    for node in nodes {
130        path.push(node);
131    }
132
133    path
134}