xtr 0.1.6

Extract strings from a rust crate to be translated with gettext
use std::fmt;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::mem::swap;
use syn;
use syn::visit::Visit;



#[derive(Debug)]
pub enum Error {
    ParseCannotOpenFile {
        src_path: String,
    },
    ParseSyntaxError {
        src_path: String,
        error: syn::parse::Error,
    },
    LexError {
        src_path: String,
        line: u32,
    },
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Error::ParseCannotOpenFile { ref src_path } => {
                write!(f, "Parsing crate: cannot open file `{}`.", src_path)
            }
            Error::ParseSyntaxError {
                ref src_path,
                ref error,
            } => write!(f, "Parsing crate:`{}`:\n{:?}", src_path, error),
            Error::LexError {
                ref src_path,
                ref line,
            } => write!(f, "{}:{}: Lexing error", src_path, line + 1),
        }
    }
}


pub struct Parser<ModVisitor> {
    current_path: PathBuf, // The current file being parsed
    mod_dir: PathBuf,
    mod_error: Option<Error>, // An error occuring while visiting the modules
    mod_visitor: ModVisitor
}

impl<ModVisitor> Parser<ModVisitor>
 where ModVisitor : FnMut(&PathBuf, &str, &syn::File) -> Result<(), Error>
{

    pub fn with_visitor(mod_visitor: ModVisitor) -> Self {
        Parser {
            current_path: PathBuf::default(),
            mod_dir: PathBuf::default(),
            mod_error: Option::default(),
            mod_visitor
        }
    }

    pub fn parse_crate<P: AsRef<Path>>(&mut self, crate_root: P) -> Result<(), Error> {
        self.parse_mod(crate_root)
    }

    fn parse_mod<P: AsRef<Path>>(&mut self, mod_path: P) -> Result<(), Error> {
        let mut s = String::new();
        let mut f = File::open(&mod_path).map_err(|_| Error::ParseCannotOpenFile {
            src_path: mod_path.as_ref().to_str().unwrap().to_owned(),
        })?;
        f.read_to_string(&mut s)
            .map_err(|_| Error::ParseCannotOpenFile {
                src_path: mod_path.as_ref().to_str().unwrap().to_owned(),
            })?;

        let fi = syn::parse_file(&s).map_err(|x| Error::ParseSyntaxError {
            src_path: "".to_owned(),
            error: x,
        })?;

        let mut current_path = mod_path.as_ref().into();
        let mut mod_dir = mod_path.as_ref().parent().unwrap().into();

        swap(&mut self.current_path, &mut current_path);
        swap(&mut self.mod_dir, &mut mod_dir);

        self.visit_file(&fi);
        if let Some(err) = self.mod_error.take() {
            return Err(err);
        }

        (self.mod_visitor)(&current_path, &s, &fi);

        swap(&mut self.current_path, &mut current_path);
        swap(&mut self.mod_dir, &mut mod_dir);

        Ok(())
    }

     /*
    fn parse_macro(&mut self, tts: TokenStream) {
        let mut last_ident: Option<syn::Ident> = None;
        let mut is_macro = false;
        for t in tts.into_iter() {
            match t {
                TokenTree::Punct(ref p) if p.as_char() == '!'  => is_macro = true,
                TokenTree::Ident(i) => {
                    is_macro = false;
                    last_ident = Some(i);
                }
                TokenTree::Group(d) => {
                    if is_macro && last_ident.as_ref().map_or(false, |i| i == "cpp") {
                        self.handle_cpp(&d.stream())
                    } else if is_macro && last_ident.as_ref().map_or(false, |i| i == "cpp_class") {
                        self.handle_cpp_class(&d.stream())
                    } else {
                        self.parse_macro(d.stream())
                    }
                    is_macro = false;
                    last_ident = None;
                }
                _ => {
                    is_macro = false;
                    last_ident = None;
                }
            }
        }
    }
    */
}

impl<'ast, ModVisitor> Visit<'ast> for Parser<ModVisitor>
 where ModVisitor : FnMut(&PathBuf, &str, &syn::File) -> Result<(), Error>
{
    fn visit_item_mod(&mut self, item: &'ast syn::ItemMod) {
        if self.mod_error.is_some() {
            return;
        }

        if item.content.is_some() {
            let mut parent = self.mod_dir.join(item.ident.to_string());
            swap(&mut self.mod_dir, &mut parent);
            syn::visit::visit_item_mod(self, item);
            swap(&mut self.mod_dir, &mut parent);
            return;
        }

        // Determine the path of the inner module's file
        for attr in &item.attrs {
            match attr.interpret_meta() {
                Some(syn::Meta::NameValue(syn::MetaNameValue {
                    ident: ref id,
                    lit: syn::Lit::Str(ref s),
                    ..
                })) if id == "path" =>
                {
                    let mod_path = self.mod_dir.join(&s.value());
                    return self.parse_mod(mod_path).unwrap_or_else(|err| self.mod_error = Some(err));
                }
                _ => {}
            }
        }

        let mod_name = item.ident.to_string();
        let mut subdir = self.mod_dir.join(mod_name.clone());
        subdir.push("mod.rs");
        if subdir.is_file() {
            return self.parse_mod(subdir).unwrap_or_else(|err| self.mod_error = Some(err));
        }
        let adjacent = self.mod_dir.join(&format!("{}.rs", mod_name));
        if adjacent.is_file() {
            return self.parse_mod(adjacent).unwrap_or_else(|err| self.mod_error = Some(err));
        }

        panic!(
            "No file with module definition for `mod {}` in file {:?}",
            mod_name, self.current_path
        );
    }
}