tracers-codegen 0.1.0

Contains the compile-time code generation logic which powers the `probe` and `tracers` macros. Do not use this crate directly; see "tracers" for more information.
Documentation
//! This module contains logic that looks into the AST of a Rust source file and finds dependent
//! source files.  By "dependent" is meant source file dependencies within the crate, primarily
//! `mod` statements incorporating submodules, but this will also at some point try to follow
//! `include!` macros where possible.
use failure::{bail, Fallible};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use syn::visit::Visit;

/// The type of source dependency.
#[derive(Serialize, Deserialize, Debug)]
pub(crate) enum SourceDependency {
    /// A `mod` dependency, specifying the name of the module.
    Mod(String),
}

/// Scans an already-parsed AST and finds the source dependencies within the file
pub(crate) fn get_dependencies(contents: &syn::File) -> Vec<SourceDependency> {
    let mut visitor = Visitor::new();

    visitor.visit_file(contents);

    visitor.deps
}

/// Given the path to a source file and a previously-discovered dependency, attempts to resolve
/// that dependency to an existing source file.
pub(crate) fn resolve_dependency(source_path: &Path, dep: &SourceDependency) -> Fallible<PathBuf> {
    match dep {
        SourceDependency::Mod(module_name) => find_module(source_path, &module_name),
    }
}

/// Given the path to a source file containing a `mod` item, and the name of the module, attempts
/// to find the source file which contains that module.  It does this by trying first to find a
/// file `$MODNAME.rs` in the same directory as `source_path`, and failing that will look for
/// `$MODNAME/mod.rs`.  If either of those exises, the path to the file is returned, but no attempt
/// is made to determine if it's valid Rust code.
///
/// If it fails, returns a descriptive error
fn find_module(source_path: &Path, module_name: &str) -> Fallible<PathBuf> {
    let mut path = source_path.to_owned();
    //Pop the file name so we have just the directory path
    path.pop();
    path.push(format!("{}.rs", module_name));
    if path.exists() {
        return Ok(path);
    }
    path.pop();
    path.push(format!("{}/mod.rs", module_name));
    if path.exists() {
        return Ok(path);
    }

    //Else, could not find the module's source code
    bail!("Unable to locate source code for module '{}'", module_name);
}

/// Simple implementation of the `Visit` trait provided by `syn`, to traverse the AST of a single
/// source file, looking for tokens that indicate an external reference
struct Visitor {
    deps: Vec<SourceDependency>,
}

impl Visitor {
    fn new() -> Visitor {
        Visitor { deps: vec![] }
    }
}

impl<'ast> Visit<'ast> for Visitor {
    fn visit_item_mod(&mut self, i: &'ast syn::ItemMod) {
        //First call the default implementation.
        syn::visit::visit_item_mod(self, i);

        //Now look at this mod statement.  If it has an implementation, then we're done.  If not,
        //if it looks something like:
        //```
        //mod foo;
        //```
        //
        //Then it actually works a bit like a #include in C.  Try to find the source file for that
        //module, relative to the current source file, which should be the last element in the
        //`paths` member.
        if i.content == None {
            let mut module_name = i.ident.to_string();

            //Rust allows module names which are also Rust reserved words to be escaped with `r#`,
            //for example:
            //
            //```
            //mod r#static //this is in `static.rs` or `static/mod.rs`
            //```
            let module_name = if module_name.starts_with("r#") {
                module_name.split_off(2)
            } else {
                module_name
            };

            self.deps.push(SourceDependency::Mod(module_name));
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::testdata::*;
    use failure::Error;
    use std::fs::File;
    use std::io::Read;

    fn parse(source_path: &Path) -> Fallible<syn::File> {
        let mut file = File::open(source_path)?;
        let mut src = String::new();
        file.read_to_string(&mut src)?;

        Ok(syn::parse_file(&src)?)
    }

    fn find_sources(entrypoint: &Path) -> (Vec<PathBuf>, Vec<(PathBuf, Error)>) {
        let mut source_paths: Vec<PathBuf> = Vec::new();
        let mut source_errors: Vec<(PathBuf, Error)> = Vec::new();

        match parse(entrypoint) {
            Ok(ast) => {
                source_paths.push(entrypoint.to_owned());

                let deps = get_dependencies(&ast);

                for dep in deps {
                    match resolve_dependency(entrypoint, &dep) {
                        Ok(dep_path) => {
                            let (mut dep_paths, mut dep_errors) = find_sources(&dep_path);
                            source_paths.append(&mut dep_paths);
                            source_errors.append(&mut dep_errors);
                        }
                        Err(e) => source_errors.push((entrypoint.to_owned(), e)),
                    }
                }
            }
            Err(e) => source_errors.push((entrypoint.to_owned(), e)),
        }

        (source_paths, source_errors)
    }

    #[test]
    fn test_case() {
        for case in TEST_CRATES.iter() {
            for target in case.targets.iter() {
                let entrypoint = case.root_directory.join(target.entrypoint);
                let (paths, errors) = find_sources(&entrypoint);

                let mut expected_paths = vec![entrypoint];
                let mut additional_paths: Vec<_> = target
                    .additional_source_files
                    .iter()
                    .map(|p| case.root_directory.join(p))
                    .collect();
                expected_paths.append(&mut additional_paths);

                assert_eq!(expected_paths, paths);

                //Make sure all expected errors were reported, and that no other, unexpected errors
                //were
                let unexpected_errors = errors
                    .iter()
                    .filter(|(file, error_msg)| {
                        //Look in the expected errors; was this one expected?
                        !target
                            .expected_errors
                            .iter()
                            .any(|(expected_file, expected_substring)| {
                                &case.root_directory.join(expected_file) == file
                                    && error_msg.to_string().contains(expected_substring)
                            })
                    })
                    .map(|(file, error_msg)| format!("{}: {}", file.to_str().unwrap(), error_msg))
                    .collect::<Vec<_>>();
                assert_eq!(
                    Vec::<String>::new(),
                    unexpected_errors,
                    "Some unexpected errors were reported"
                );
                let missing_errors = target
                    .expected_errors
                    .iter()
                    .filter(|(expected_file, expected_substring)| {
                        !errors.iter().any(|(file, error_msg)| {
                            &case.root_directory.join(expected_file) == file
                                && error_msg.to_string().contains(expected_substring)
                        })
                    })
                    .map(|(expected_file, expected_substring)| {
                        format!("{}: {}", expected_file, expected_substring)
                    })
                    .collect::<Vec<_>>();
                assert_eq!(
                    Vec::<String>::new(),
                    missing_errors,
                    "Some expected errors were not reported"
                );
            }
        }
    }
}