Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a way to run a callback on file load #16

Merged
merged 1 commit into from
Jul 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

use proc_macro2::Span;
use std::{
borrow::Cow,
error, fmt, io,
path::{Path, PathBuf},
};
Expand Down Expand Up @@ -75,21 +74,30 @@ impl InlinerBuilder {
/// Parse the source code in `src_file` and return an `InliningResult` that has all modules
/// recursively inlined.
pub fn parse_and_inline_modules(&self, src_file: &Path) -> Result<InliningResult, Error> {
self.parse_internal(src_file, FsResolver::default())
self.parse_internal(src_file, &mut FsResolver::new(|_: &Path, _| {}))
}

fn parse_internal<R: FileResolver + Clone>(
/// Parse the source code in `src_file` and return an `InliningResult` that has all modules
/// recursively inlined. Call the given callback whenever a file is loaded from disk (regardless
/// of if it parsed successfully).
pub fn inline_with_callback(
&self,
src_file: &Path,
on_load: impl FnMut(&Path, String),
) -> Result<InliningResult, Error> {
self.parse_internal(src_file, &mut FsResolver::new(on_load))
}

fn parse_internal<R: FileResolver>(
&self,
src_file: &Path,
resolver: R,
resolver: &mut R,
) -> Result<InliningResult, Error> {
// XXX There is no way for library callers to disable error tracking,
// but until we're sure that there's no performance impact of enabling it
// we'll let downstream code think that error tracking is optional.
let mut errors = Some(vec![]);
let result =
Visitor::<R>::with_resolver(src_file, self.root, errors.as_mut(), Cow::Owned(resolver))
.visit()?;
let result = Visitor::<R>::new(src_file, self.root, errors.as_mut(), resolver).visit()?;
Ok(InliningResult::new(result, errors.unwrap_or_default()))
}
}
Expand Down Expand Up @@ -316,7 +324,7 @@ mod tests {
#[test]
fn happy_path() {
let result = InlinerBuilder::default()
.parse_internal(Path::new("src/lib.rs"), make_test_env())
.parse_internal(Path::new("src/lib.rs"), &mut make_test_env())
.unwrap()
.output;

Expand Down Expand Up @@ -351,7 +359,7 @@ mod tests {
env.register("src/lib.rs", "mod missing;\nmod invalid;");
env.register("src/invalid.rs", "this-is-not-valid-rust!");

let result = InlinerBuilder::default().parse_internal(Path::new("src/lib.rs"), env);
let result = InlinerBuilder::default().parse_internal(Path::new("src/lib.rs"), &mut env);

if let Ok(r) = result {
let errors = &r.errors;
Expand Down Expand Up @@ -431,7 +439,7 @@ mod tests {
env.register("src/empty.rs", "");

let result = InlinerBuilder::default()
.parse_internal(Path::new("src/lib.rs"), env)
.parse_internal(Path::new("src/lib.rs"), &mut env)
.unwrap()
.output;

Expand Down Expand Up @@ -489,7 +497,7 @@ mod tests {
env.register("src/empty.rs", "");

let result = InlinerBuilder::default()
.parse_internal(Path::new("src/lib.rs"), env)
.parse_internal(Path::new("src/lib.rs"), &mut env)
.unwrap()
.output;

Expand Down
30 changes: 22 additions & 8 deletions src/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,34 @@ pub(crate) trait FileResolver {
/// Resolves the given path into a file.
///
/// Returns an error if the file couldn't be loaded or parsed as valid Rust.
fn resolve(&self, path: &Path) -> Result<syn::File, Error>;
fn resolve(&mut self, path: &Path) -> Result<syn::File, Error>;
}

#[derive(Default, Clone)]
pub(crate) struct FsResolver;
#[derive(Clone)]
pub(crate) struct FsResolver<F> {
on_load: F,
}

impl<F> FsResolver<F> {
pub(crate) fn new(on_load: F) -> Self {
Self { on_load }
}
}

impl FileResolver for FsResolver {
impl<F> FileResolver for FsResolver<F>
where
F: FnMut(&Path, String),
{
fn path_exists(&self, path: &Path) -> bool {
path.exists()
}

fn resolve(&self, path: &Path) -> Result<syn::File, Error> {
fn resolve(&mut self, path: &Path) -> Result<syn::File, Error> {
let src = std::fs::read_to_string(path)?;
Ok(syn::parse_file(&src)?)
let res = syn::parse_file(&src);
// Call the callback whether the file parsed successfully or not.
(self.on_load)(path, src);
Ok(res?)
}
}

Expand All @@ -47,7 +61,7 @@ impl FileResolver for TestResolver {
self.files.contains_key(path)
}

fn resolve(&self, path: &Path) -> Result<syn::File, Error> {
fn resolve(&mut self, path: &Path) -> Result<syn::File, Error> {
let src = self.files.get(path).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
Expand All @@ -69,7 +83,7 @@ impl FileResolver for PathCommentResolver {
true
}

fn resolve(&self, path: &Path) -> Result<syn::File, Error> {
fn resolve(&mut self, path: &Path) -> Result<syn::File, Error> {
Ok(syn::parse_file(&format!(
r#"const PATH: &str = "{}";"#,
path.to_str().unwrap()
Expand Down
38 changes: 13 additions & 25 deletions src/visitor.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
use std::borrow::Cow;
use std::path::Path;

use syn::visit_mut::VisitMut;
use syn::ItemMod;

use crate::{Error, FileResolver, FsResolver, InlineError, ModContext};
use crate::{Error, FileResolver, InlineError, ModContext};

pub(crate) struct Visitor<'a, R: Clone> {
pub(crate) struct Visitor<'a, R> {
/// The current file's path.
path: &'a Path,
/// Whether this is the root file or not
Expand All @@ -16,26 +15,19 @@ pub(crate) struct Visitor<'a, R: Clone> {
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: Cow<'a, R>,
resolver: &'a mut R,
/// A log of module items that weren't expanded.
error_log: Option<&'a mut Vec<InlineError>>,
}

impl<'a, R: FileResolver + Default + Clone> Visitor<'a, R> {
/// Create a new visitor with a default instance of the specified `FileResolver` type.
fn new(path: &'a Path, root: bool, error_log: Option<&'a mut Vec<InlineError>>) -> Self {
Self::with_resolver(path, root, error_log, Cow::Owned(R::default()))
}
}

impl<'a, R: FileResolver + Clone> Visitor<'a, R> {
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 with_resolver(
pub fn new(
path: &'a Path,
root: bool,
error_log: Option<&'a mut Vec<InlineError>>,
resolver: Cow<'a, R>,
resolver: &'a mut R,
) -> Self {
Self {
path,
Expand All @@ -53,7 +45,7 @@ impl<'a, R: FileResolver + Clone> Visitor<'a, R> {
}
}

impl<'a, R: FileResolver + Clone> VisitMut for Visitor<'a, R> {
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());

Expand Down Expand Up @@ -82,11 +74,11 @@ impl<'a, R: FileResolver + Clone> VisitMut for Visitor<'a, R> {
.expect("candidates should be non-empty")
});

let mut visitor = Visitor::with_resolver(
let mut visitor = Visitor::new(
&first_candidate,
false,
self.error_log.as_mut().map(|v| &mut **v),
self.resolver.clone(),
self.resolver,
);

match visitor.visit() {
Expand All @@ -106,12 +98,6 @@ impl<'a, R: FileResolver + Clone> VisitMut for Visitor<'a, R> {
}
}

impl<'a> From<&'a Path> for Visitor<'a, FsResolver> {
fn from(path: &'a Path) -> Self {
Visitor::<FsResolver>::new(path, true, None)
}
}

#[cfg(test)]
mod tests {
use quote::{quote, ToTokens};
Expand All @@ -124,7 +110,8 @@ mod tests {
#[test]
fn ident_in_lib() {
let path = Path::new("./lib.rs");
let mut visitor = Visitor::<PathCommentResolver>::new(&path, true, None);
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!(
Expand All @@ -141,7 +128,8 @@ mod tests {
#[test]
fn path_attr() {
let path = std::path::Path::new("./lib.rs");
let mut visitor = Visitor::<PathCommentResolver>::new(&path, true, None);
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!(
Expand Down
48 changes: 48 additions & 0 deletions tests/resolver.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//! Test that syn-inline-mod can resolve this crate's lib.rs properly.

use std::path::Path;
use syn_inline_mod::InlinerBuilder;

#[test]
fn resolve_lib() {
let builder = InlinerBuilder::new();

let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let lib_rs = manifest_dir.join("src/lib.rs");

let mut files_seen = vec![];

let res = builder
.inline_with_callback(&lib_rs, |path, file| {
files_seen.push((path.to_path_buf(), file));
})
.expect("src/lib.rs should parse successfully");
assert!(!res.has_errors(), "result has no errors");

// Ensure that the list of files is correct.
let file_list: Vec<_> = files_seen
.iter()
.map(|(path, _)| {
let rel_path = path
.strip_prefix(manifest_dir)
.expect("path should be relative to manifest dir");
rel_path.to_str().expect("path is valid Unicode")
})
.collect();

// The order visited should be the same as the order in which "mod" statements are listed.
assert_eq!(
file_list,
vec![
"src/lib.rs",
"src/mod_path.rs",
"src/resolver.rs",
"src/visitor.rs",
]
);

for (path, contents) in &files_seen {
let disk_contents = std::fs::read_to_string(path).expect("reading contents failed");
assert_eq!(&disk_contents, contents, "file contents match");
}
}