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
13pub 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 let pos = semi.start();
54 let idx = file
55 .lines()
56 .take(pos.line) .enumerate()
58 .fold(
59 pos.line - 1, |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
78fn 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 let file_mod = parent.join(name.to_owned() + ".rs");
108 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
125pub 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}