1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
use std::path::Path;
use syn::visit_mut::VisitMut;
use syn::ItemMod;
use crate::{Error, FileResolver, InlineError, ModContext};
pub(crate) struct Visitor<'a, R> {
/// The current file's path.
path: &'a Path,
/// Whether this is the root file or not
root: bool,
/// The stack of `mod` entries where the visitor is currently located. This is needed
/// for cases where modules are declared inside inline modules.
mod_context: ModContext,
/// The resolver that can be used to turn paths into `syn::File` instances. This removes
/// a direct file-system dependency so the expander can be tested.
resolver: &'a mut R,
/// A log of module items that weren't expanded.
error_log: Option<&'a mut Vec<InlineError>>,
}
impl<'a, R: FileResolver> Visitor<'a, R> {
/// Create a new visitor with the specified `FileResolver` instance. This will be
/// used by all spawned visitors as we recurse down through the source code.
pub fn new(
path: &'a Path,
root: bool,
error_log: Option<&'a mut Vec<InlineError>>,
resolver: &'a mut R,
) -> Self {
Self {
path,
root,
resolver,
error_log,
mod_context: Default::default(),
}
}
pub fn visit(&mut self) -> Result<syn::File, Error> {
let mut syntax = self.resolver.resolve(self.path)?;
self.visit_file_mut(&mut syntax);
Ok(syntax)
}
}
impl<'a, R: FileResolver> VisitMut for Visitor<'a, R> {
fn visit_item_mod_mut(&mut self, i: &mut ItemMod) {
self.mod_context.push(i.into());
if let Some((_, items)) = &mut i.content {
for item in items {
self.visit_item_mut(item);
}
} else {
// If we find a path that points to a satisfactory file, expand it
// and replace the items with the file items. If something goes wrong,
// leave the file alone.
// candidates is guaranteed to be non-empty by ModContext::relative_to.
let candidates = self.mod_context.relative_to(self.path, self.root);
// Look for the first candidate file that exists.
let first_candidate = candidates
.iter()
.find(|p| self.resolver.path_exists(p))
.unwrap_or_else(|| {
// If no candidate exists, use the last file (which will error out while
// loading).
candidates
.iter()
.last()
.expect("candidates should be non-empty")
});
let mut visitor = Visitor::new(
&first_candidate,
false,
self.error_log.as_mut().map(|v| &mut **v),
self.resolver,
);
match visitor.visit() {
Ok(syn::File { attrs, items, .. }) => {
i.attrs.extend(attrs);
i.content = Some((Default::default(), items));
}
Err(kind) => {
if let Some(ref mut errors) = self.error_log {
errors.push(InlineError::new(self.path, i, first_candidate, kind));
}
}
}
}
self.mod_context.pop();
}
}
#[cfg(test)]
mod tests {
use quote::{quote, ToTokens};
use std::path::Path;
use syn::visit_mut::VisitMut;
use super::Visitor;
use crate::PathCommentResolver;
#[test]
fn ident_in_lib() {
let path = Path::new("./lib.rs");
let mut resolver = PathCommentResolver::default();
let mut visitor = Visitor::new(&path, true, None, &mut resolver);
let mut file = syn::parse_file("mod c;").unwrap();
visitor.visit_file_mut(&mut file);
assert_eq!(
file.into_token_stream().to_string(),
quote! {
mod c {
const PATH: &str = "./c.rs";
}
}
.to_string()
);
}
#[test]
fn path_attr() {
let path = std::path::Path::new("./lib.rs");
let mut resolver = PathCommentResolver::default();
let mut visitor = Visitor::new(&path, true, None, &mut resolver);
let mut file = syn::parse_file(r#"#[path = "foo/bar.rs"] mod c;"#).unwrap();
visitor.visit_file_mut(&mut file);
assert_eq!(
file.into_token_stream().to_string(),
quote! {
#[path = "foo/bar.rs"]
mod c {
const PATH: &str = "./foo/bar.rs";
}
}
.to_string()
);
}
}